Ethers极简入门 工具篇: ET02. 实现MetaMask签名授权登录功能 我最近在重新学 ,巩固一下细节,也写一个 ,供小白们使用。 推特:@0xAAScience WTF Academy社群: 官网 wtf.academy | WTF Solidity教程 discord 微信群申请 所有代码和教程开源在github: github.com/WTFAcademy/WTFEthers 这一讲,我们将介绍如何通过 在网页上连接小狐狸钱包,实现MetaMask签名授权后完成登录功能 Metamask Metamask(小狐狸)钱包是以太坊最受欢迎的开源钱包,它可以作为PC端的浏览器插件或移动端(安卓和苹果)的APP使用。 在使用Metamask开发前,你需要先下载它。
我最近在重新学ethers.js,巩固一下细节,也写一个WTF Ethers极简入门,供小白们使用。
WTF Academy社群: 官网 wtf.academy | WTF Solidity教程
| discord
| 微信群申请
所有代码和教程开源在github: github.com/WTFAcademy/WTFEthers
这一讲,我们将介绍如何通过 ethers.js 在网页上连接小狐狸钱包,实现MetaMask签名授权后完成登录功能
Metamask(小狐狸)钱包是以太坊最受欢迎的开源钱包,它可以作为PC端的浏览器插件或移动端(安卓和苹果)的APP使用。
在使用Metamask开发前,你需要先下载它。记住:
一定要在官网上下载: https://metamask.io/download/
一定要在官网上下载: https://metamask.io/download/
一定要在官网上下载: https://metamask.io/download/
重要的事情说三遍。很多多用户钱包被盗,就是下载了盗版的小狐狸钱包之后输入了助记词/私钥。
在安装好metamask钱包后,浏览器会给每个页面注入一个window.ethereum对象,用于和钱包交互。ethers.js提供的 BrowserProvider 封装了一个标准的 Web3 Provider,直接在程序中生成一个provider对象,方便使用:
// 获得provider const provider = new ethers.BrowserProvider(window.ethereum)
之后就像 ethers.js 中的 provider 一样使用就好,下面举几个例子。
首先,我们先写一个简单的HTML页面,因为小狐狸钱包是浏览器插件,必须在浏览器中使用。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <h1 id="header">Sign in with Ethereum</h1> <button class="sign">Sign</button> <h2>钱包地址: <span class="showAccount"></span></h2> <h2>Nonce: <span class="showNonce"></span></h2> <h2>Signature: <span class="showSignature"></span></h2> <h2>是否签名成功: <span class="showSignStatus"></span></h2> </body> </html>

然后我们写一些javascript脚本嵌入进去。
引入 ethers.js 包,获取页面中的按钮和文本变量,给按钮加一个监听器,被点击时会触发 onClickHandler() 函数。
import {ethers} from "https://cdn-cors.ethers.io/lib/ethers-5.6.9.esm.min.js"; const signButton = document.querySelector('.sign'); const showAccount = document.querySelector('.showAccount'); const showNonce = document.querySelector('.showNonce'); const showSignature = document.querySelector('.showSignature'); const showSignStatus = document.querySelector('.showSignStatus'); signButton.addEventListener(`click`, onClickHandler)
接下来我们创建一个变量模拟后台数据库,保存着用户的信息
//假设这是后台的数据库,users表里保存了user对象,user对象包含用户的地址和关联的nonce //{"address": 用户地址, "nonce": 返回给前端的随机数nonce} export const users = {}
写模拟后台接口的方法:通过地址获取后端生成的随机数 nonce,用于签名
/** * 通过地址获取后端生成的随机数 nonce,用于签名 * @param address 用户地址 * @returns {number} 返回随机数 nonce * * 这个方法充当后台服务,从后台中获取需要签名的数据 */ function auth(address) { let user = users[address] if (!user) { user = { address, nonce: Math.floor(Math.random() * 10000000) } users[address] = user } else { const nonce = Math.floor(Math.random() * 10000000) user.nonce = nonce users[address] = user } return user.nonce }
写模拟后台接口的方法:验证用户签名是否正确
/** * 验证用户签名是否正确 * @param address 用户地址 * @param signature 签名数据 * @returns {boolean} 返回签名是否正确 * * 这个方法充当后台服务,后台验证签名正确后,就返回相关登录态数据,完成登录流程 */ function verify(address, signature) { let signValid = false console.log(`address: ${address}`) //从数据库中取出nonce let nonce = users[address].nonce console.log(`nonce: ${nonce}`) //验证对nonce进行签名的地址 const decodedAddress = ethers.verifyMessage(nonce.toString(), signature.toString()) console.log(`decodedAddress: ${decodedAddress}`) //比较地址和签名的地址是否一致 if (address.toLowerCase() === decodedAddress.toLowerCase()) { signValid = true //出于安全原因,更改nonce,防止下次直接使用相同的nonce进行登录 users[address].nonce = Math.floor(Math.random() * 10000000) } return signValid }
我们写onClickHandler() 函数的内容,首先连接metamask,创建provider变量,获取后端需要签名的接口数据,拿到数据后进行签名,再次请求后端验证签名是否正确
// 前端签名流程 async function onClickHandler() { console.log("连接钱包") // 获得provider const provider = new ethers.Web3Provider(window.ethereum) // 读取钱包地址 const accounts = await provider.send("eth_requestAccounts", []); const account = accounts[0] console.log(`钱包地址: ${account}`) showAccount.innerHTML = account; //从后台获取需要进行签名的数据 const nonce = auth(account); showNonce.innerHTML = nonce; console.log(`获取后台需要签名的数据: ${nonce}`) //签名 const signer = await provider.getSigner() const signature = await signer.signMessage(nonce.toString()) showSignature.innerHTML = signature; //去后台验证签名,完成登录 const signStatus = verify(account, signature); showSignStatus.innerHTML = signStatus; }
在本地运行时,你需要安装http-server包,你可以用下面这个命令通过 npm 它。
npm install --global http-server
然后切换到相应的文件夹,输入下面命令来运行这个网页。注意,直接点击.html文件是不能运行的。
http-server .
如果你看到下面的输出,说明运行成功,打开下面给出的网址就可以打开网页了。
Starting up http-server, serving . Available on: http://127.0.0.1:8080 http://10.6.8.162:8080
打开当前教程页面
Starting up http-server, serving . Available on: http://127.0.0.1:8080/ET02_SignInWithEthereum/index.html http://10.6.8.162:8080/ET02_SignInWithEthereum/index.html

最大的用处就是通过这种登录方式让 以太坊地址(EVM)用户 登录到 中心化平台
如何使用Metamask进行一键式登录流程
一键式登录流程的基本思想是,通过使用私钥对一段数据进行签名,可以很容易地通过加密方式证明帐户的所有权。如果您设法签署由我们的后端生成的精确数据,那么后端将认为您是该钱包地址的所有者。因此,我们可以构建基于消息签名的身份验证机制,并将用户的钱包地址作为其标识符。
在项目设计中,整个登录流程如何工作
address 和 address对应的nonceaddress,在 nonce 字段中生成随机字符串address的noncePS:这里的nonce可以是一个需要签名的字符串,不是说一定是随机数也可以是类似这样的;很多项目sign都不是单独的一个nonce,通常会带上一些其他内容
类似这样
Sign up with Ethereum to the app. URI: https://app.xxxx.com Version: 1 Chain ID: 1 Nonce: 6971359 Issued At: 2023-02-16T06:03:49.534Z
Java 后端项目
maven导入web3j
<dependency> <groupId>org.web3j</groupId> <artifactId>core</artifactId> <version>4.5.16</version> </dependency>
Java main方法示例代码
import org.web3j.crypto.ECDSASignature; import org.web3j.crypto.Hash; import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import org.web3j.utils.Numeric; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Arrays; public class SignInWithEthereum { public static void main(String[] args) { String address = "0x4adbe2009cff6a1e9d280d28815c49e91b8ebad0"; String nonce = "3618473"; String signature = "0x9a38fb504315869609ef2e948b1a80f670e6ff725d16b5ae443b118eb2d108bc3c659c2417d96bbff240b44d4f1078fde73b72f83cc71e4e726640bc19a9c2a91c"; String MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"; try { String digest = Hash.sha3( Numeric.toHexStringNoPrefix( (MESSAGE_PREFIX + nonce.length() + nonce).getBytes(StandardCharsets.UTF_8))); byte[] signatureBytes = Numeric.hexStringToByteArray(signature); byte v = signatureBytes[64]; if (v < 27) { v += 27; } byte[] r = (byte[]) Arrays.copyOfRange(signatureBytes, 0, 32); byte[] s = (byte[]) Arrays.copyOfRange(signatureBytes, 32, 64); Sign.SignatureData signatureData = new Sign.SignatureData(v, r, s); int header = 0; for (byte b : signatureData.getV()) { header = (header << 8) + (b & 0xFF); } if (header < 27 || header > 34) { System.out.println("false"); } int recId = header - 27; BigInteger key = Sign.recoverFromSignature( recId, new ECDSASignature( new BigInteger(1, signatureData.getR()), new BigInteger(1, signatureData.getS())), Numeric.hexStringToByteArray(digest)); if (key == null) { System.out.println("false"); } String signAddress = ("0x" + Keys.getAddress(key)).trim(); System.out.println("signAddress:" + signAddress); if (address.toLowerCase().equals(signAddress.toLowerCase())) { System.out.println("true"); } } catch (Exception e) { System.out.println("false"); } } }
Java 封装的工具类
import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.web3j.crypto.ECDSASignature; import org.web3j.crypto.Hash; import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import org.web3j.crypto.Sign.SignatureData; import org.web3j.utils.Numeric; public class EthersUtils { private static final String MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"; public static String verifyMessage(String message, String signature) { return EthersUtils.recoverAddress(EthersUtils.hashMessage(message), signature); } public static String hashMessage(String message) { return Hash.sha3( Numeric.toHexStringNoPrefix( (EthersUtils.MESSAGE_PREFIX + message.length() + message).getBytes(StandardCharsets.UTF_8))); } public static String recoverAddress(String digest, String signature) { SignatureData signatureData = EthersUtils.getSignatureData(signature); int header = 0; for (byte b : signatureData.getV()) { header = (header << 8) + (b & 0xFF); } if (header < 27 || header > 34) { return null; } int recId = header - 27; BigInteger key = Sign.recoverFromSignature( recId, new ECDSASignature( new BigInteger(1, signatureData.getR()), new BigInteger(1, signatureData.getS())), Numeric.hexStringToByteArray(digest)); if (key == null) { return null; } return ("0x" + Keys.getAddress(key)).trim(); } private static SignatureData getSignatureData(String signature) { byte[] signatureBytes = Numeric.hexStringToByteArray(signature); byte v = signatureBytes[64]; if (v < 27) { v += 27; } byte[] r = (byte[]) Arrays.copyOfRange(signatureBytes, 0, 32); byte[] s = (byte[]) Arrays.copyOfRange(signatureBytes, 32, 64); return new SignatureData(v, r, s); } }
这一讲,我们介绍如何通过 ethers.js 在网页上连接小狐狸钱包,实现MetaMask签名授权后完成登录功能,用户一个钱包地址完成一个登录中心化网站的登录流程