基于钱包实现免注册登录
钱包是去中心的,账号由用户掌控,因此基于钱包实现免注册登录是更安全友好的一种方式, 保护用户隐私的同时,也方便业务的开发使用。
TIP
使用钱包免注册登录的好处之一是去中心化的账户系统坚固性、加密安全性以及节省了传统开发中需要使用的运营商服务(如:短信验证码、邮箱验证等)成本。
1. 链接钱包获取地址信息并校验
使用钱包账号 address 作为用户 id,address 可通过钱包 API 直接获取。参考 DApp 开发 及 Provider APIs
。
获取到钱包地址之后需要使用 ethers
库 getAddress
方法校验地址有效性。
js
import { useWeb3ModalAccount } from "@web3modal/ethers/react";
import { getAddress } from "ethers";
const { address, isConnected } = useWeb3ModalAccount();
const checksumAddress = getAddress(address);
import { useWeb3ModalAccount } from "@web3modal/ethers/react";
import { getAddress } from "ethers";
const { address, isConnected } = useWeb3ModalAccount();
const checksumAddress = getAddress(address);
2.Nonce 处理
服务端也应该校验 address 有效性。
可使用 string + random num 的策略生成随机的被签名数据:
js
import * as ethUtil from "ethereumjs-util";
if (!ethUtil.isValidChecksumAddress(address)) {
throw new HttpException("Invalid address", HttpStatus.BAD_REQUEST);
}
const nonceMessage = `Verify account ownership, checksumAddress: ${address}, nonce: ${Math.random()
.toString(36)
.substring(2, 15)}`;
import * as ethUtil from "ethereumjs-util";
if (!ethUtil.isValidChecksumAddress(address)) {
throw new HttpException("Invalid address", HttpStatus.BAD_REQUEST);
}
const nonceMessage = `Verify account ownership, checksumAddress: ${address}, nonce: ${Math.random()
.toString(36)
.substring(2, 15)}`;
前端请求服务器,上报 address,获取服务端分配的与 address 对应的 nonce。
js
const nonce = await getNonceApi.request({ address: checksumAddress });
const nonce = await getNonceApi.request({ address: checksumAddress });
3. 签署与校验
服务端编写登录逻辑,校验地址、签名、nonce 的合法性
js
import * as ethUtil from "ethereumjs-util";
const validateSignature = async (nonce, signature, publicAddress) => {
const msgBf = ethUtil.toBuffer(Buffer.from(nonce));
const msgHash = ethUtil.hashPersonalMessage(msgBf);
const signPrm = ethUtil.fromRpcSig(signature);
const publicKey = ethUtil.ecrecover(msgHash, signPrm.v, signPrm.r, signPrm.s);
const addressBf = ethUtil.publicToAddress(publicKey);
const address = ethUtil.bufferToHex(addressBf);
return address.toLowserCase() === publicAddress.toLowerCase();
};
if (!(await validateSignature(nonce, signature, address))) {
throw new HttpException("Invalid signature", HttpStatus.BAD_REQUEST);
}
// DB Query address
// JWT logic ...
import * as ethUtil from "ethereumjs-util";
const validateSignature = async (nonce, signature, publicAddress) => {
const msgBf = ethUtil.toBuffer(Buffer.from(nonce));
const msgHash = ethUtil.hashPersonalMessage(msgBf);
const signPrm = ethUtil.fromRpcSig(signature);
const publicKey = ethUtil.ecrecover(msgHash, signPrm.v, signPrm.r, signPrm.s);
const addressBf = ethUtil.publicToAddress(publicKey);
const address = ethUtil.bufferToHex(addressBf);
return address.toLowserCase() === publicAddress.toLowerCase();
};
if (!(await validateSignature(nonce, signature, address))) {
throw new HttpException("Invalid signature", HttpStatus.BAD_REQUEST);
}
// DB Query address
// JWT logic ...
前端接收 nonce 后调用钱包签名方法,使用签名结果 signature 和 address 进行登录。
js
import { BrowserProvider } from "ethers";
async function onSignMessage(msg) {
const provider = new BrowserProvider(walletProvider);
const signer = await provider.getSigner();
const signature = await signer.signMessage(msg);
return signature;
}
const signature = onSignMessage(nonce);
const jwt = await loginApi.request({
address: checksumAddress,
signature,
nonce,
});
// updateLoginState() ...
import { BrowserProvider } from "ethers";
async function onSignMessage(msg) {
const provider = new BrowserProvider(walletProvider);
const signer = await provider.getSigner();
const signature = await signer.signMessage(msg);
return signature;
}
const signature = onSignMessage(nonce);
const jwt = await loginApi.request({
address: checksumAddress,
signature,
nonce,
});
// updateLoginState() ...
之后就可以使用 jwt 进行前后端通讯。