如何在自己設計的頁面中調用metamask-2


參考:

1)https://www.colabug.com/3204345.html

2)https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial#utilize-unreal-developers-today

1)智能合約開發 – 如何實現一鍵化登錄 Dapp

https://www.colabug.com/3204345.html

任何有帳戶體系的網站和 app 都會有自己的登錄模塊,有時候還會集成 oauth2 (weibo, weixin,github)一鍵化登錄.開發者肯定也都或多或少的開發過注冊,登錄的功能。那么基於以太坊的 Dapp 中登錄功能會有什么區別呢?本文主要介紹了 Dapp 帳號體系的構成,以及如何基於 Metamask 開發一鍵化登錄的功能。

首先 Dapp 跟普通的網站(app)沒多少區別,完全可以延續之前的帳號體系登錄,注冊。在需要用到以太坊區塊鏈的時候(比如創建交易,支付等)調用錢包或者 MetaMask 插件即可。

當然本身以太坊就有自己的帳號,每個人都可以創建 Address 來和區塊鏈交互,所以如果我們的 Dapp 跟 Address 能夠綁定並實現登錄的話,整體的體驗會好很多。

解決方案是利用私鑰對 payload 加密生成 signature,然后再用

ecdsa_recover 方法對 signature 解密可以拿到對應的公鑰。

 

2)今天從這個實例開始學習,這個實例是在教你怎么在網頁上登錄metamask的例子:

https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial#utilize-unreal-developers-today

Web3.js is a JavaScript interface to the Ethereum blockchain. There are functions to: Get the latest block of the chain (web3.eth.getBlockNumber) Check the current active account on MetaMask (web3.eth.coinbase) Get the balance of any account (web3.eth.getBalance) Send transactions (web3.eth.sendTransaction) Sign messages with the private key of the current account (web3.personal.sign)

However, some functions (like web3.eth.sendTransaction and web3.personal.sign) need the current account to sign some data with its private key. These functions trigger MetaMask to show a confirmation screen(就是怎么再出現一個metamask窗口讓你再次點擊它進行確認), to double-check that the user knows what she or he is signing.

 

Let’s see how to use MetaMask for this. To make a simple test, paste the following line in the DevTools console:

web3.personal.sign(web3.fromUtf8("Hello from Toptal!"), web3.eth.coinbase, console.log);

This command means: Sign my message, converted from utf8 to hex, with the coinbase account (i.e. current account), and as a callback, print the signature. A MetaMask popup will appear, and if you sign it, the signed message will be printed.(好像意思是說,只要你做的操作是需要簽名或交易的,那么這個窗口是會自己彈出來的)

然后就想試試吧

 在頁面的JavaScript中寫:

        window.addEventListener('load', function() { if (!window.web3) {//用來判斷你是否安裝了metamask window.alert('Please install MetaMask first.');//如果沒有會去提示你先去安裝 return; } if (!web3.eth.coinbase) {//這個是判斷你有沒有登錄,coinbase是你此時選擇的賬號 window.alert('Please activate MetaMask first.'); return; } // Checking if Web3 has been injected by the browser (Mist/MetaMask) if (typeof web3 !== 'undefined') {  // Use the browser's ethereum provider web3.personal.sign(web3.fromUtf8("Hello from wanghui!"), web3.eth.coinbase, console.log);

       }
    });

如果你此時使用的瀏覽器是沒有metamask的,那么你就會返回這樣的警告,要求你先安裝metamask:

警告:這里如果要判斷用戶有沒有安裝metamask,那個判斷語句是:

        window.addEventListener('load', function() { if (!window.web3) {//用來判斷你是否安裝了metamask window.alert('Please install MetaMask first.');//如果沒有會去提示你先去安裝 return; }
是if (!window.web3) 而不是if (!web3),在沒有安裝metamask的瀏覽器中,web3會報錯:
ReferenceError: Can't find variable: web3
window.web3的返回值是undefined

 

當然,這里的寫法也可以是下面的這種:

var Web3 = require('web3');
getWeb3 = new Promise(function(resolve) {
    window.addEventListener('load', function() {
        var results;
        var web3 = window.web3;//將window.web3賦值為web3,這樣當沒有安裝metamask並沒解鎖的時候window.web3的返回值為undefined if (typeof web3 !== 'undefined') {
            // Use Mist/MetaMask's provider.
            web3 = new Web3(web3.currentProvider);
            results = {
                web3: web3
            };
            console.log('Injected web3 detected.');
            resolve(results);
        } else {
            alert('請安裝MetaMask插件並解鎖您的以太坊賬戶');
        }
    })
});
var web3;
getWeb3.then(function(results) {
    web3 = results.web3;
});

 

當判斷出你安裝了metamask后,你就能直接用web3了,可以不用window.web3了

 因為在已經安裝了metamask的瀏覽器中查看

        window.addEventListener('load', function() { console.log(window.web3); console.log(web3); });

發現這兩個值得到的內容其實是一樣的,結果:

 

當想訪問這個網站前,此時如果metamask沒有登錄的話,就會先彈出這樣的警告:

 

然后根據上面的操作,我們可以看見其實我還是進入了這個頁面的,但是后面會改一下,讓其沒能進入該頁面。這樣上面就判斷完了用戶的安裝與登錄metamask的情況

 

這時候一訪問這個頁面,那個確認簽名的metamask頁面果然是出來了:

然后當我們點擊sign后,就會看見相應的簽名信息就出來了,用於確認用戶的確自己授權進入了我們這個頁面進行交易,留在我們網站作為一個憑證

 

A final note about this section: MetaMask injects web3.js into your current browser, but there are actually other standalone browsers which also inject web3.js, like Mist, for example. However, in my opinion, MetaMask offers today the best UX and simplest transition for regular users to explore dapps.

How the Login Flow Works

We will make one assumption: That all users visiting our front-end web page have MetaMask installed(就是用戶都已經安裝了metamask). With this assumption, we will show how a passwordless (不需要密碼)cryptographically-secure login flow works.

Step 1: Modify the User Model (Back-end)

First of all, our User model needs to have two new required fields: publicAddress and nonce. Additionally, publicAddress needs to be unique. You can keep the usual username, email, and password fields—especially if you want to implement your MetaMask login parallely to an email/password login—but they are optional.

就是在User模塊中有兩個值:publicAddress和nonce.publicAddress是一個獨一無二的值,你也可以有常見的username、email和password等值,尤其是你想要實現metamask登錄的方式並且也能夠使用郵箱/密碼登錄的方式進行登錄

The signup process will also slightly differ, as publicAddress will be a required field on signup, if the user wishes to use a MetaMask login. Rest assured, the user will never need to type their publicAddress manually, since it can be fetched via web3.eth.coinbase.

如果用戶希望使用metamask去登錄的話,那么publicAddress是必須的值;當然,用戶並不需要手動去輸入publicAddress,網站能夠自己通過接口web3.eth.coinbase來獲得它

Step 2: Generate Nonces (Back-end)

For each user in the database, generate a random string in the nonce field. For example, nonce can be a big random integer.

Step 3: User Fetches Their Nonce (Front-end)

In our front-end JavaScript code, assuming MetaMask is present, we have access to window.web3. We can therefore call web3.eth.coinbase to get the current MetaMask account’s public address.

首先我們假設已經安裝並使用了metamask,那就有了window.web3的接口,因此我們就能夠調用web3.eth.coinbase去等到目前賬號的address

When the user clicks on the login button, we fire an API call to the back end to retrieve the nonce associated with their public address. Something like a route with a filter parameter GET /api/users?publicAddress=${publicAddress} should do. Of course, since this is an unauthenticated API call, the back end should be configured to only show public information (including nonce) on this route.

當用戶點擊了登錄的按鈕,我們將通過API接口調用后端去檢索與該address相關的nonce,即從數據庫中調取,訪問route為GET /api/users?publicAddress=${publicAddress} (就是去查看有沒有與這個address相關的nonce,說明它之前登錄過)。因為這是一個還沒有授權的API調用(即沒有新的nonce)那么后端在只會返回一些公共信息(包括nonce)

If the previous request doesn’t return any result, it means that the current public address hasn’t signed up yet. We need to first create a new account via POST /users, passing publicAddress in the request body. On the other hand, if there’s a result, then we store its nonce.

如果之前的調用沒有返回任何數據,那么就說明這個address之前還沒有注冊過,我們需要創建賬號並傳遞address(然后后端就會存儲這個address並生成一個nonce發回前端)。如果有數據,那我們將存儲這個nonce,給下一步簽名使用

Step 4: User Signs the Nonce (Front-end)

Once the front end receives nonce in the response of the previous API call, it runs the following code:

web3.personal.sign(nonce, web3.eth.coinbase, callback);

This will prompt MetaMask to show a confirmation popup for signing the message. The nonce will be displayed in this popup, so that the user knows she or he isn’t signing some malicious data.

When she or he accepts it, the callback function will be called with the signed message (called signature) as an argument. The front end then makes another API call to POST /api/authentication, passing a body with both signature and publicAddress.

一旦前端從之前的API調用中收到nonce,那么他將調用web3.personal.sign。這將會提示metamask去彈出一個簽名消息的確認窗口。在窗口上將會展示nonce,所以使用者將會知道他沒有簽署什么奇怪的數據。

它用戶接受后,將簽署信息當作變量的回調函數將會被調用。前端將會使用另一個API調用 POST /api/authentication,傳遞簽名和address

Step 5: Signature Verification (Back-end)

When the back end receives a POST /api/authentication request, it first fetches the user in the database corresponding to the publicAddressgiven in the request body. In particular it fetches the associated nonce.

Having the nonce, the public address, and the signature, the back end can then cryptographically verify that the nonce has been correctly signed by the user. If this is the case, then the user has proven ownership of the public address, and we can consider her or him authenticated. A JWT or session identifier can then be returned to the front end.

當后端收到請求  POST /api/authentication 后,它首先根據請求上的publicAddress去數據庫中尋找相應的用戶。特別是得到相關的nonce

有了nonce,address,signature,后端就可以進行核查nonce到底是不是被這個用戶簽名的。如果是,用戶則證明了它對address的擁有,將對其進行授權

 

Step 6: Change the Nonce (Back-end)

To prevent the user from logging in again with the same signature (in case it gets compromised), we make sure that the next time the same user wants to log in, she or he needs to sign a new nonce. This is achieved by generating another random nonce for this user and persisting it to the database.

Et voilà! This is how we manage a nonce-signing passwordless login flow.

為了防止用戶使用相同的簽名,我們要保證下一次同樣的用戶想要登錄時,它需要簽署一個新的nonce。這通過為用戶生成一個新的隨機nonce來實現,並將其保存在數據庫中

Why the Login Flow Works

Authentication, by definition, is really only the proof of ownership of an account. If you uniquely identify your account using a public address, then it’s cryptographically trivial to prove you own it.

To prevent the case where a hacker gets hold of one particular message and your signature of it (but not your actual private key), we enforce the message to sign to be:

  1. Provided by the back end, and
  2. Regularly changing

We changed it after each successful login in our explanation, but a timestamp-based mechanism could also be imagined.

就是整個大概的意思就是,你注冊時將會生成一個nonce,並與address對應存儲在數據庫中(當然在這里會同時進行簽名,即sign);然后后面你想要登錄了,你就傳遞address去數據庫得到相應的nonce,並對nonce進行簽名,將nonce,address,signature傳到后端去驗證你的簽名的正確行來確認你為該用戶

這里用於核查的標准就是每次私鑰進行簽名的nonce都不一樣,而且都是由后端提供的,然后由ecdsa_recover (nonce,signature)方法來對簽名得到公鑰,與傳遞來的公鑰兩相對比來進行核查簽名

Let’s Build It Together

In this section, I’ll go through the six steps above, one by one. I’ll show some snippets of code for how we can build this login flow from scratch, or integrate it in an existing back end, without too much effort.

I created a small demo app for the purpose of this article. The stack I’m using is the following:

  • Node.js, Express, and SQLite (via the Sequelize ORM) to implement a RESTful API on the back end. It returns a JWT on successful authentication.
  • React single-page application on the front-end.

I try to use as few libraries as I can. I hope the code is simple enough so that you can easily port it to other tech stacks.

The whole project can be seen in this GitHub repository. A demo is hosted here.

代碼的實現為:

login-with-metamask-demo/frontend/src/Login/Login.js

import React, { Component } from 'react'; import Web3 from 'web3'; import './Login.css'; let web3 = null; // Will hold the web3 instance class Login extends Component { state = { loading: false // Loading button state,一開始設metamask連接狀態為false  }; handleAuthenticate = ({ publicAddress, signature }) => fetch(`${process.env.REACT_APP_BACKEND_URL}/auth`, {//19 調用后台 body: JSON.stringify({ publicAddress, signature }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }).then(response => response.json()); handleClick = () => {//3 const { onLoggedIn } = this.props;//React中的每一個組件,都包含有一個屬性(props),屬性主要是從父組件傳遞給子組件的,在組件內部,我們可以通過this.props獲取屬性對象
                                                                                   //就是點擊頁面按鈕時傳來的屬性對象 if (!window.web3) {//4 先檢查是否安裝了metamask window.alert('Please install MetaMask first.'); return; } if (!web3) {//5 檢查metamask是否連接上了網絡 // We don't know window.web3 version, so we use our own instance of web3 // with provider given by window.web3 web3 = new Web3(window.web3.currentProvider); } if (!web3.eth.coinbase) {//6 檢查metamask是否登錄 window.alert('Please activate MetaMask first.'); return; } const publicAddress = web3.eth.coinbase.toLowerCase(); this.setState({ loading: true });//到這里metamask就連接上了,狀態為true // Look if user with current publicAddress is already present on backend fetch( `${ process.env.REACT_APP_BACKEND_URL }/users?publicAddress=${publicAddress}` //7 去后端查看這個address是否之前是否已經注冊過了 ) .then(response => response.json()) // If yes, retrieve it. If no, create it. .then(//10 如果不為0,說明之前注冊過,那就得到users[0] = (nonce,publicAddress,username);如果users.length為0,則create it,調用this.handleSignup(publicAddress) users => (users.length ? users[0] : this.handleSignup(publicAddress)) ) // Popup MetaMask confirmation modal to sign message .then(this.handleSignMessage)//15 然后這時候的address在數據庫上都生成的自己的數據,所以可以對得到的nonce進行簽名了 // Send signature to backend on the /auth route .then(this.handleAuthenticate)//18 進行簽名的核查 // Pass accessToken back to parent component (to save it in localStorage) .then(onLoggedIn) .catch(err => { window.alert(err); this.setState({ loading: false }); }); }; handleSignMessage = ({ publicAddress, nonce }) => {//16 然后就使用私鑰和nonce來進行簽名 return new Promise((resolve, reject) => web3.personal.sign( web3.fromUtf8(`I am signing my one-time nonce: ${nonce}`), publicAddress, (err, signature) => { if (err) return reject(err); return resolve({ publicAddress, signature });//17 得到publicAddress, signature } ) ); }; handleSignup = publicAddress => fetch(`${process.env.REACT_APP_BACKEND_URL}/users`, {//11 訪問后端,發送address body: JSON.stringify({ publicAddress }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }).then(response => response.json());//14 得到創建的用戶的信息 render() {//1 const { loading } = this.state;//得到狀態false return (//返回頁面 <div> <p> Please select your login method.<br />For the purpose of this demo, only MetaMask login is implemented. </p> <button className="Login-button Login-mm" onClick={this.handleClick}>//2 點擊進行登錄 {loading ? 'Loading...' : 'Login with MetaMask'} </button> <button className="Login-button Login-fb" disabled> Login with Facebook </button> <button className="Login-button Login-email" disabled> Login with Email </button> </div> ); } } export default Login;

login-with-metamask-demo/backend/src/services/users/routes.js

import jwt from 'express-jwt'; import express from 'express'; import config from '../../config'; import * as controller from './controller'; const router = express.Router(); /** GET /api/users */ router.route('/').get(controller.find);//8 查找現在進行登錄的address在數據庫中的情況 /** GET /api/users/:userId */ /** Authenticated route */ router.route('/:userId').get(jwt({ secret: config.secret }), controller.get); /** POST /api/users */ router.route('/').post(controller.create);//12 創建新address的相應數據庫數據 /** PATCH /api/users/:userId */ /** Authenticated route */ router .route('/:userId') .patch(jwt({ secret: config.secret }), controller.patch); export default router;

login-with-metamask-demo/backend/src/services/auth/routes.js

import express from 'express'; import * as controller from './controller'; const router = express.Router(); /** POST /api/auth */ router.route('/').post(controller.create);//20 export default router;

 

login-with-metamask-demo/backend/src/services/users/controller.js

import db from '../../db'; const User = db.models.User;//數據庫中的User表 export const find = (req, res, next) => {//9 查看address在的行的數據users的所有信息,其實就是為了得到nonce // If a query string ?publicAddress=... is given, then filter results const whereClause = req.query && req.query.publicAddress && { where: { publicAddress: req.query.publicAddress } }; return User.findAll(whereClause) .then(users => res.json(users)) .catch(next); }; export const get = (req, res, next) => { // AccessToken payload is in req.user.payload, especially its `id` field // UserId is the param in /users/:userId // We only allow user accessing herself, i.e. require payload.id==userId if (req.user.payload.id !== +req.params.userId) { return res.status(401).send({ error: 'You can can only access yourself' }); } return User.findById(req.params.userId) .then(user => res.json(user)) .catch(next); }; export const create = (req, res, next) =>//13 創建一個nonce,address = req.body,username的數據放在數據庫中 User.create(req.body) .then(user => res.json(user)) .catch(next); export const patch = (req, res, next) => { // Only allow to fetch current user if (req.user.payload.id !== +req.params.userId) { return res.status(401).send({ error: 'You can can only access yourself' }); } return User.findById(req.params.userId) .then(user => { Object.assign(user, req.body); return user.save(); }) .then(user => res.json(user)) .catch(next); };

login-with-metamask-demo/backend/src/models/user.model.js

import Sequelize from 'sequelize'; export default function(sequelize) {//13 在生成數據時,nonce是使用了Math.random()來隨機生成的,username不設置則為空 const User = sequelize.define('User', { nonce: { allowNull: false, type: Sequelize.INTEGER.UNSIGNED, defaultValue: () => Math.floor(Math.random() * 10000) // Initialize with a random nonce  }, publicAddress: { allowNull: false, type: Sequelize.STRING, unique: true, validate: { isLowercase: true } }, username: { type: Sequelize.STRING, unique: true } }); }

login-with-metamask-demo/backend/src/services/auth/controller.js

import ethUtil from 'ethereumjs-util'; import jwt from 'jsonwebtoken'; import config from '../../config'; import db from '../../db'; const User = db.models.User; export const create = (req, res, next) => { const { signature, publicAddress } = req.body; if (!signature || !publicAddress)//21 查看是否傳遞了所需的數據 return res .status(400) .send({ error: 'Request should have signature and publicAddress' }); return ( User.findOne({ where: { publicAddress } }) //22 在數據庫中查找該數據的相關信息 //////////////////////////////////////////////////// // Step 1: Get the user with the given publicAddress //////////////////////////////////////////////////// .then(user => { if (!user) return res.status(401).send({ error: `User with publicAddress ${publicAddress} is not found in database` }); return user; }) //////////////////////////////////////////////////// // Step 2: Verify digital signature //////////////////////////////////////////////////// .then(user => {//23 然后通過從數據庫中得到nonce來得知簽名的消息內容為 const msg = `I am signing my one-time nonce: ${user.nonce}`; // We now are in possession of msg, publicAddress and signature. We // can perform an elliptic curve signature verification with ecrecover const msgBuffer = ethUtil.toBuffer(msg);//24 然后進行下面的驗證 const msgHash = ethUtil.hashPersonalMessage(msgBuffer);//對消息進行hash const signatureBuffer = ethUtil.toBuffer(signature); const signatureParams = ethUtil.fromRpcSig(signatureBuffer);//將簽名分成v,r,s const publicKey = ethUtil.ecrecover(//調用ecrecover來從簽名中恢復公鑰 msgHash, signatureParams.v, signatureParams.r, signatureParams.s ); const addressBuffer = ethUtil.publicToAddress(publicKey);//然后將公鑰轉為address,是buffer格式的 const address = ethUtil.bufferToHex(addressBuffer);//轉成16進制格式 // The signature verification is successful if the address found with // ecrecover matches the initial publicAddress if (address.toLowerCase() === publicAddress.toLowerCase()) {//然后將得到的address的值域數據庫中的比較,如果相等則返回用戶信息user,否則報錯 return user; } else { return res .status(401) .send({ error: 'Signature verification failed' }); } }) //////////////////////////////////////////////////// // Step 3: Generate a new nonce for the user //////////////////////////////////////////////////// .then(user => {25 驗證完成后要更新nonce的內容 user.nonce = Math.floor(Math.random() * 10000); return user.save(); }) //////////////////////////////////////////////////// // Step 4: Create JWT(看本博客Json Web Token是干什么) ////////////////////////////////////////////////////  .then(//26 相當於實現了一個類似session的功能,這里則是使用token user => new Promise((resolve, reject) => // https://github.com/auth0/node-jsonwebtoken  jwt.sign(//得到token { payload: { id: user.id, publicAddress } }, config.secret, null, (err, token) => { if (err) { return reject(err); } return resolve(token); } ) ) )//將上面形成的token傳回前端 .then(accessToken => res.json({ accessToken })) .catch(next) ); };

 

注意:這個有個不好的點就是他至今還不能在手機端實現


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM