天天看點

內建Web3.0身份錢包MetaMask以太坊一鍵登入(Tornado6+Vue.js3)

作者:劉悅技術分享

上世紀九十年代,海灣戰争的時候,一位美軍軍官擔心他們的五角大樓會被敵人的一枚飛彈幹掉,進而導緻在全球的美軍基地處于癱瘓狀态。這時候,有一位天才的科學家說,最好的中心就是沒有中心。是的,這就是最樸素的去中心化思想,于是網際網路出現了。一個沒有網際網路的時代是無法想象的,網際網路的核心就是把一個資訊分成若幹的小件,用不同的途徑傳播出去,怎麼友善怎麼走。

三十年後的今天,去中心化身份逐漸被廣泛采用。使用者的部分線上活動在鍊上是公開的,可通過加密錢包搜尋到,使用者在鍊上創造、貢獻、賺取和擁有的東西,都反映了他們的喜好,也逐漸積累成該使用者的身份和辨別。

當我們的使用者厭倦了傳統的電子郵件/密碼注冊流程時,他們會選擇Google、GitHub等社交登入方式,這種方式雖然節約了使用者的時間,但登入資訊也會被第三方平台記錄,也就是說我們用平台賬号做了什麼,平台都會一目了然,甚至還會對我們的行為進行分析、畫像。那麼有沒有一種登入方式,它的所有資訊都隻儲存在用戶端和後端,并不牽扯三方平台授權,最大化的保證使用者隐私呢?Web3.0給我們提供了一種選擇:MetaMask。

MetaMask

MetaMask是用于與以太坊區塊鍊進行互動的軟體加密貨币錢包。MetaMask允許使用者通過浏覽器插件或移動應用程式通路其以太坊錢包,然後可以使用這些擴充程式與去中心化應用程式進行互動。當然了,首先需要擁有一個MetaMask錢包,進入https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn

安裝metamask浏覽器插件:

內建Web3.0身份錢包MetaMask以太坊一鍵登入(Tornado6+Vue.js3)

随後點開插件,建立賬号,記錄密碼、錢包位址、以及助記詞等資訊。

安裝好插件之後,我們就可以利用這個插件和網站應用做互動了。

錢包登入流程

登入邏輯和傳統的三方登入還是有差異的,傳統三方登入一般是首先跳轉三方平台進行授權操作,随後三方平台将code驗證碼傳回給登入平台,登入平台再使用code請求三方平台換取token,再通過token請求使用者賬号資訊,而錢包登入則是先在前端通過Web3.js浏覽器插件中儲存的私鑰對錢包位址進行簽名操作,随後将簽名和錢包位址發送到後端,後端利用Web3的庫用同樣的算法進行驗簽操作,如果驗簽通過,則将錢包資訊存入token,并且傳回給前端。

內建Web3.0身份錢包MetaMask以太坊一鍵登入(Tornado6+Vue.js3)

前端簽名操作

首先需要下載下傳前端的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("驗證失敗");
                        }
                 });



                }           

這裡将目前賬戶的錢包位址和簽名傳遞給後端,如圖所示:

內建Web3.0身份錢包MetaMask以太坊一鍵登入(Tornado6+Vue.js3)

完整頁面代碼:

<!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 BaseHandlerfrom 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方法對簽名進行反編譯操作,如果反編譯後的錢包位址和前端傳過來的錢包位址吻合,那麼說明目前賬戶的身份驗證通過:

內建Web3.0身份錢包MetaMask以太坊一鍵登入(Tornado6+Vue.js3)

當驗簽通過之後,利用錢包位址在背景建立賬号,随後将錢包位址、token等資訊傳回給前端,前端将其儲存在stroage中即可。

結語

沒錯,将至已至,未來已來,是時候将Web3.0區塊鍊技術融入産品了,雖然有些固有的思維方式依然在人們的腦海揮之不去,但世界卻在時不我待地變化着,正是:青山遮不住,畢竟東流去!項目開源在https://github.com/zcxey2911/Tornado6_Vuejs3_Edu ,與君共觞。