青山不遮，毕竟东流，集成Web3.0身份钱包MetaMask以太坊一键登录(Tornado6+Vue.js3)
Tags: webdev, programming, tutorial, beginners
Published: 2022-06-01 01:07:54

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

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

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

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

MetaMask
--------

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

安装metamask浏览器插件：

![](https://v3u.cn/v3u/Public/js/editor/attached/20220531170510_40241.png)

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

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

钱包登录流程
------

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

![](https://v3u.cn/v3u/Public/js/editor/attached/20220531190527_32835.png)

前端签名操作
------

首先需要下载前端的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("验证失败");  
                        }  
                 });  
  
  
  
                }
```

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

![](https://v3u.cn/v3u/Public/js/editor/attached/20220531180518_60721.png)

完整页面代码：

```
<!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问题](https://v3u.cn/a_id_119)。

后端我们使用异步框架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方法对签名进行反编译操作，如果反编译后的钱包地址和前端传过来的钱包地址温和，那么说明当前账户的身份验证通过：

![](https://v3u.cn/v3u/Public/js/editor/attached/20220531190531_18895.png)

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

结语
--

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

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