写点什么

青山不遮,毕竟东流,集成 Web3.0 身份钱包 MetaMask 以太坊一键登录 (Tornado6+Vue.js3)

  • 2022 年 5 月 31 日
  • 本文字数:6696 字

    阅读完需:约 22 分钟

青山不遮,毕竟东流,集成Web3.0身份钱包MetaMask以太坊一键登录(Tornado6+Vue.js3)

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_213


上世纪九十年代,海湾战争的时候,一位美军军官担心他们的五角大楼会被敌人的一枚导弹干掉,从而导致在全球的美军基地处于瘫痪状态。这时候,有一位天才的科学家说,最好的中心就是没有中心。是的,这就是最朴素的去中心化思想,于是互联网出现了。一个没有互联网的时代是无法想象的,互联网的核心就是把一个信息分成若干的小件,用不同的途径传播出去,怎么方便怎么走。


三十年后的今天,去中心化身份逐渐被广泛采用。用户的部分在线活动在链上是公开的,可通过加密钱包搜索到,用户在链上创造、贡献、赚取和拥有的东西,都反映了他们的喜好,也逐渐积累成该用户的身份和标识。


当我们的用户厌倦了传统的电子邮件/密码注册流程时,他们会选择 Google、GitHub 等社交登录方式,这种方式虽然节约了用户的时间,但登录信息也会被第三方平台记录,也就是说我们用平台账号做了什么,平台都会一目了然,甚至还会对我们的行为进行分析、画像。那么有没有一种登录方式,它的所有信息都只保存在客户端和后端,并不牵扯三方平台授权,最大化的保证用户隐私呢?Web3.0 给我们提供了一种选择:MetaMask。

MetaMask

MetaMask 是用于与以太坊区块链进行交互的软件加密货币钱包。MetaMask 允许用户通过浏览器插件或移动应用程序访问其以太坊钱包,然后可以使用这些扩展程序与去中心化应用程序进行交互。当然了,首先需要拥有一个 MetaMask 钱包,进入https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn


安装 metamask 浏览器插件:



随后点开插件,创建账号,记录密码、钱包地址、以及助记词等信息。


安装好插件之后,我们就可以利用这个插件和网站应用做交互了。

钱包登录流程

登录逻辑和传统的三方登录还是有差异的,传统三方登录一般是首先跳转三方平台进行授权操作,随后三方平台将 code 验证码返回给登录平台,登录平台再使用 code 请求三方平台换取 token,再通过 token 请求用户账号信息,而钱包登录则是先在前端通过 Web3.js 浏览器插件中保存的私钥对钱包地址进行签名操作,随后将签名和钱包地址发送到后端,后端利用 Web3 的库用同样的算法进行验签操作,如果验签通过,则将钱包信息存入 token,并且返回给前端。


前端签名操作

首先需要下载前端的 Web3.0 操作库,https://docs.ethers.io/v4/,随后集成到登录页面中:


<script src="{{ static_url("js/ethers-v4.min.js") }}"></script>  <script src="{{ static_url("js/axios.js") }}"></script>  <script src="{{ static_url("js/vue.js") }}"></script>
复制代码


这里我们基于 Vue.js 配合 Axios 使用。


接着声明登录激活方法:


sign_w3:function(){                        that = this;                      ethereum.enable().then(function () {        this.provider = new ethers.providers.Web3Provider(web3.currentProvider);        this.provider.getNetwork().then(function (result) {          if (result['chainId'] != 1) {                console.log("Switch to Mainnet!")            } else { // okay, confirmed we're on mainnet                this.provider.listAccounts().then(function (result) {                  console.log(result);                  this.accountAddress = result[0]; // figure out the user's Eth address                  this.provider.getBalance(String(result[0])).then(function (balance) {                      var myBalance = (balance / ethers.constants.WeiPerEther).toFixed(4);                      console.log("Your Balance: " + myBalance);                  });                    // get a signer object so we can do things that need signing                  this.signer = provider.getSigner();                    var rightnow = (Date.now()/1000).toFixed(0)          var sortanow = rightnow-(rightnow%600)            this.signer.signMessage("Signing in to "+document.domain+" at "+sortanow, accountAddress, "test password!")              .then((signature) => {               that.handleAuth(accountAddress,signature);              });                    console.log(this.signer);              })          }      })  })                    },
复制代码


通过使用 signMessage 方法返回签名,这里加签过程中使用基于时间戳的随机数防止未签名,当前端签名生成好之后,立刻异步请求后台接口:


//检查验证                  handleAuth:function(accountAddress, signature){                          this.myaxios("/checkw3/","post",{"public_address":accountAddress,"signature":signature}).then(data =>{                            if(data.errcode==0){                              alert("欢迎:"+data.public_address);                              localStorage.setItem("token",data.token);                              localStorage.setItem("email",data.public_address);                              window.location.href = "/";                          }else{                              alert("验证失败");                          }                   });                        }
复制代码


这里将当前账户的钱包地址和签名传递给后端,如图所示:



完整页面代码:


<!DOCTYPE html>  <html lang="en">    <head>      <meta charset="utf-8">      <title>Edu</title>      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover">      <link rel="stylesheet" href="{{ static_url("css/min.css") }}" >      <link rel="icon" href="/static/img/favicon.68cbf4197b0c.png">      <script src="{{ static_url("js/ethers-v4.min.js") }}"></script>      <script src="{{ static_url("js/axios.js") }}"></script>      <script src="{{ static_url("js/vue.js") }}"></script>  </head>    <body>      <div>            {% include "head.html" %}           <div id="app"  class="container main-content">     <div class="row justify-content-center">  <div class="col-md-10 col-lg-8 article">  <div class="article-body page-body mx-auto" style="max-width: 400px;">  <h1 class="text-center mb-4">Sign-in</h1>  <div class="socialaccount_ballot">  <div class="text-center mb-3">  <ul class="list-unstyled">      <li>  <a @click="sign_w3()" title="GitHub" class="socialaccount_provider github btn btn-secondary btn-lg w-100" href="JavaScript:void(0)">Connect With <strong>Meta Mask</strong></a>  </li>  <li>  <a title="GitHub" class="socialaccount_provider github btn btn-secondary btn-lg w-100" href="https://github.com/login/oauth/authorize?client_id=249b69d8f6e63efb2590&redirect_uri=http://localhost:8000/github_back/">Connect With <strong>GitHub</strong></a>  </li>  </ul>  </div>  <div class="text-center text-muted my-3">— or —</div>  </div>    <div class="form-group">  <div id="div_id_login" class="form-group">  <label for="id_login" class=" requiredField">  Email<span class="asteriskField">*</span>  </label>  <div class="">  <input type="email" v-model="email" placeholder="" autocomplete="email" autofocus="autofocus" class="textinput textInput form-control" >  </div>  </div>  </div>  <div class="form-group">  <div id="div_id_password" class="form-group">  <label for="id_password" class=" requiredField">  Password<span class="asteriskField">*</span>  </label>  <div class="">    <input type="password" v-model="password" placeholder="" autocomplete="current-password" minlength="8" maxlength="99" class="textinput textInput form-control" >  </div>  </div>  </div>    <div class="text-center">  <button  class="btn btn-primary btn-lg text-wrap px-5 mt-2 w-100" name="jsSubmitButton" @click="sign_on">Sign-In</button>  </div>        </div>  </div>  </div>                  </div>         {% include "foot.html" %}        </div>        <script>            const App = {              data() {                  return {                      email:"",                      password:"",                        provider:null,                      accountAddress:"",                      signer:null                  };              },              created: function() {                },              methods: {                  //metamask登录                  sign_w3:function(){                        that = this;                      ethereum.enable().then(function () {        this.provider = new ethers.providers.Web3Provider(web3.currentProvider);        this.provider.getNetwork().then(function (result) {          if (result['chainId'] != 1) {                console.log("Switch to Mainnet!")            } else { // okay, confirmed we're on mainnet                this.provider.listAccounts().then(function (result) {                  console.log(result);                  this.accountAddress = result[0]; // figure out the user's Eth address                  this.provider.getBalance(String(result[0])).then(function (balance) {                      var myBalance = (balance / ethers.constants.WeiPerEther).toFixed(4);                      console.log("Your Balance: " + myBalance);                  });                    // get a signer object so we can do things that need signing                  this.signer = provider.getSigner();                    var rightnow = (Date.now()/1000).toFixed(0)          var sortanow = rightnow-(rightnow%600)            this.signer.signMessage("Signing in to "+document.domain+" at "+sortanow, accountAddress, "test password!")              .then((signature) => {               that.handleAuth(accountAddress,signature);              });                    console.log(this.signer);              })          }      })  })                    },                  //检查验证                  handleAuth:function(accountAddress, signature){                          this.myaxios("/checkw3/","post",{"public_address":accountAddress,"signature":signature}).then(data =>{                            if(data.errcode==0){                              alert("欢迎:"+data.public_address);                              localStorage.setItem("token",data.token);                              localStorage.setItem("email",data.public_address);                              window.location.href = "/";                          }else{                              alert("验证失败");                          }                   });                        },                  sign_on:function(){                    if(this.email == ""){                      alert("邮箱不能为空");                      return false;                  }                    if(this.password == ""){                      alert("密码不能为空");                      return false;                  }                    //登录                  this.myaxios("/user_signon/","get",{"email":this.email,"password":this.password}).then(data =>{                        if(data.errcode != 0){                        alert(data.msg);                                            }else{                      alert(data.msg);                      localStorage.setItem("token",data.token);                      localStorage.setItem("email",data.email);                      window.location.href = "/";                         //localStorage.removeItem("token")                      }                     });                     },                          },          };  const app = Vue.createApp(App);  app.config.globalProperties.myaxios = myaxios;  app.config.globalProperties.axios = axios;  app.config.compilerOptions.delimiters = ['${', '}']  app.mount("#app");        </script>    </body>    </html>
复制代码

Tornado 后端验签:

有人说,既然钱包私钥是存储在浏览器中,也就是保存在客户端,那签名已经通过私钥生成了,为什么还要过一遍后端呢?这不是多此一举吗?事实上,攻击者完全可能获取到前端生成的所有信息,所以签名一定必须得是后端提供,或者至少有一步后端验证,比如著名的微信小程序获取openid问题


后端我们使用异步框架 Tornado,配合 web3 库进行调用,首先安装依赖:


pip3 install tornado==6.1  pip3 install web3==5.29.1
复制代码


随后创建异步视图方法:




from tornado.web import url import tornado.web from tornado import httpclient from .base import BaseHandler
from web3.auto import w3 from eth_account.messages import defunct_hash_message import time class CheckW3(BaseHandler): async def post(self): public_address = self.get_argument("public_address") signature = self.get_argument("signature") domain = self.request.host if ":" in domain: domain = domain[0:domain.index(":")] now = int(time.time()) sortanow = now-now%600 original_message = 'Signing in to {} at {}'.format(domain,sortanow) print("[+] checking: "+original_message) message_hash = defunct_hash_message(text=original_message) signer = w3.eth.account.recoverHash(message_hash, signature=signature) if signer == public_address: try: user = await self.application.objects.get(User,email=public_address) except Exception as e: user = await self.application.objects.create(User,email=public_address,password=create_password("third"),role=1) myjwt = MyJwt() token = myjwt.encode({"id":user.id}) self.finish({"msg":"ok","errcode":0,"public_address":public_address,"token":token}) else: self.finish({"msg":"could not authenticate signature","errcode":1})
复制代码


这里通过 recoverHash 方法对签名进行反编译操作,如果反编译后的钱包地址和前端传过来的钱包地址温和,那么说明当前账户的身份验证通过:



当验签通过之后,利用钱包地址在后台创建账号,随后将钱包地址、token 等信息返回给前端,前端将其保存在 stroage 中即可。

结语

没错,将至已至,未来已来,是时候将 Web3.0 区块链技术融入产品了,虽然有些固有的思维方式依然在人们的脑海挥之不去,但世界却在时不我待地变化着,正是:青山遮不住,毕竟东流去!项目开源在https://github.com/zcxey2911/Tornado6_Vuejs3_Edu ,与君共觞。


原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_213

发布于: 16 小时前阅读数: 28
用户头像

专注技术,凝聚意志,解决问题 v3u.cn 2020.12.21 加入

还未添加个人简介

评论

发布
暂无评论
青山不遮,毕竟东流,集成Web3.0身份钱包MetaMask以太坊一键登录(Tornado6+Vue.js3)_vue.js_刘悦的技术博客_InfoQ写作社区