本文作者:影无双[1]
DApp 最大的吸引力就是用户拥有自己的数据。然而要做到这一点,需要验证用户的 web3 身份(用户的钱包)。这在客户端是很容易的(因为用户可以用 Metamask 提交自己的信息),但是在服务端就没那么容易了。
在这篇文章中,我将概述“钱包登录”按钮的技术实现,类似Showtime[2]或者Foundation[3]的按钮。
从钱包到服务端
第一部分实现非常简单,让用户将钱包连接到我们的前端,并且从获取的钱包地址向服务端发送一个 API 请求。
这里的问题是,任何人都可以用别人的地址向我们发送 API 请求,并且我们无法验证这个地址是否映射到与前端的钱包。
在服务端验证签名
容易忽略的一点,本质上加密钱包只是一个密钥对(私钥和公钥的组合)。当你创建一笔交易,你仅仅是签署了交易参数(以数学方式证明你是创建者)并且将它广播到 ETH 网络上。
幸运的是,交易并不是钱包唯一可以签名的东西。我们可以创建任意一条消息(如Please sign this message to connect to Foundation.
),并且验证签名,以确保验证身份的钱包就是签署消息的钱包。
以太坊签名是以Ethereum Signed Message:
开头的 Keccak (SHA-3)哈希。我们可以在任何程序语言中用 Keccak 和 ECC (椭圆曲线密码学) 库进行验证。
我们需要三样东西来验证:要验证的地址、要签名的消息和签名,我们可以用任何 web3 库获取签名(下面例子用的ethers.js
):
import axios from 'axios'
import { ethers } from 'ethers'
// On production, you should use something like web3Modal
// to support additional wallet providers, like WalletConnect
const web3 = new ethers.providers.Web3Provider(window.ethereum)
const message = "Sign this message to log in to our app"
await axios.post('/api/auth/login', {
address: await web3.getSigner().getAddress(),
signature: await web3.getSigner().signMessage(message),
})
在服务端,我们可以用eth-sig-util
来验证被提交钱包所签名的消息,并且通过 cookie 或者 API token 来验证。
import { recoverPersonalSignature } from 'eth-sig-util'
const message = "Sign this message to log in to our app"
if (address.toLowerCase() !== recoverPersonalSignature({ data: data, sig: signature }).toLowerCase()) {
throw new Error('Authentication failed')
}
// wallet address has been verified, set a cookie (or return a token)
如果你想更好的掌握验证背后是如何工作的,你可以查看 我的签名验证的 PHP 实现[4]
防止签名被利用
我们有一个可以用钱包登录的系统,和一套确保只能本人验证的方法。但是有一个问题,因为我们总是签名相同的消息,任何一个签名都是账户的永久密钥,永不过期。
这意味着,如果有人通过 MITM 攻击或欺骗我们在别的网站签署相同的消息来拦截它,他们将获得不可撤销的永久访问权限。
为了防止这样的事情发生,我们需要确保每次的消息都不同。最简单的方法就是生成一个随机字符串(nonce)包含到消息中。
我们首先需要在服务端生成 nonce ,并将其存储在会话中(因为之后需要它来验证签名):
代码语言:javascript复制import crypto from 'crypto'
export default async function(req, res) {
req.session.nonce = crypto.randomInt(111111, 999999)
res.end(`Hey! Sign this message to prove you have access to this wallet. This won't cost you anything.nnSecurity code (you can ignore this): ${req.session.nonce}`)
}
然后,不是硬编码要签名的消息,而是通过 AJAX 从服务端检索它:
代码语言:javascript复制import axios from 'axios'
import { ethers } from 'ethers'
// On production, you should use something like web3Modal
// to support additional wallet providers, like WalletConnect
const web3 = new ethers.providers.Web3Provider(window.ethereum)
const message = await axios.get('/api/auth/nonce').then(res => res.data)
await axios.post('/api/auth/login', {
address: await web3.getSigner().getAddress(),
signature: await web3.getSigner().signMessage(message),
})
最后,在检查签名之前,我们需要从会话中将 nonce 提取出来,从而重建消息。
工具包
有一些软件包可以处理这些事情。我建议在 Node 上用passport-web3[5],如果你正在用 PHP 和 Laravel ,我建议用 and laravel-web3-login[6]。如果你发现其他语言包,请私信我[7]
原文链接:https://m1guelpf.blog/VBlaOTPAxQNFHjl5n-C3BvhkpizvAui9A4RJDwFiQ3k
参考资料
[1]
影无双: https://learnblockchain.cn/people/58
[2]
Showtime: https://tryshowtime.com
[3]
Foundation: https://foundation.app
[4]
我的签名验证的 PHP 实现: https://github.com/m1guelpf/laravel-web3-login/blob/
[5]
passport-web3: https://github.com/coopermaruyama/passport-web3
[6]
laravel-web3-login: https://github.com/m1guelpf/laravel-web3-login
[7]
私信我: https://twitter.com/m1guelpf