如何在本身設計的頁面中調用metamask-2

參考:

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

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

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

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

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

首先 Dapp 跟普通的網站(app)沒多少區別,徹底能夠延續以前的賬號體系登陸,註冊。在須要用到以太坊區塊鏈的時候(好比建立交易,支付等)調用錢包或者 MetaMask 插件便可。react

固然自己以太坊就有本身的賬號,每一個人均可以建立 Address 來和區塊鏈交互,因此若是咱們的 Dapp 跟 Address 可以綁定並實現登陸的話,總體的體驗會好不少。git

解決方案是利用私鑰對 payload 加密生成 signature,而後再用github

ecdsa_recover 方法對 signature 解密能夠拿到對應的公鑰。web

 

2)今天從這個實例開始學習,這個實例是在教你怎麼在網頁上登陸metamask的例子:數據庫

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

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) ); };

 

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

相關文章
相關標籤/搜索