MetaMask SpringBoot实现前后端一键登录
1、介绍
主要流程是利用MetaMask获取地址,然后签名后传到后端,后端进行验签然后返回JWT Token给前端进行登录,这里后端使用的是SpringBoot
2、安装MetaMask插件
要使用MetaMask首先要安装插件,本文使用的是Google浏览器。插件地址进行安装
右上角这个图标就是插件列表
3、签名
MetaMask 目前有六种签名方法,您可能想知道这些方法的历史。研究这些方法的历史可以为分散标准的出现提供一些指导教训。我们目前的五种方法是:
- eth_sign
- personal_sign
- signTypedData(目前与 相同signTypedData_v1)
- signTypedData_v1
- signTypedData_v3
- signTypedData_v4
来自于MetaMask文档内容,这点很重要,由于我使用的是Java验签,目前我在网上没找到 signTypedData_v4的验签代码。所以本文使用的是personal_sign方式签名
4、前端代码
因为用的是MetaMask对注入的代码,所以js ,vue都可以用以下代码
1、首先初始化登录,获取用户的账户地址
window.ethereum.request({
method: 'eth_requestAccounts'
}).then(
accounts => {
this.ethAccount = accounts[0]
// 2、拿到地址
console.log("地址:" + this.ethAccount);
}
)
2、根据返回的地址,用从地址后端拿到一个流水号nonce
axios.post("http://localhost:8089/api/app/user/metamask/verify", {
"address": this.ethAccount
}).then(res => {
this.nonces = res.data.data;
})
3、拿到流水号进行类型为personal_sign的签名,MetaMask会自动调起授权窗口,点击签名按钮会返回签名,以下代码:console.log(“签名->” + res); 处
ethereum.request({
method: 'personal_sign',
params: [from, this.nonces],
}).then((res) => {
this.sign = res;
// 点击窗口签名后这里返回
console.log("签名->" + res);
})
4、拿到签名后我们就可以传地址和签名给后端进行验签,后端验签成功返回JWT Token,就登录成功了
axios.post("http://localhost:8089/api/app/user/metamask/login", {
"chainId": window.ethereum.chainId,
"address": this.ethAccount,
"signature": this.sign
}).then(
res => {
if (res.data.verify) {
localStorage.setItem("token", res.data.token);
localStorage.setItem("expire", Date.now() + 3600000);
localStorage.setItem("userName", this.ethAccount);
console.log("登录成功");
} else {
console.log("登录失败,请重新登录");
}
}
)
前端完整代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<body>
</body>
<script>
// 1、首先初始化登录,获取用户的账户地址
window.ethereum.request({
method: 'eth_requestAccounts'
}).then(
accounts => {
this.ethAccount = accounts[0]
// 2、拿到地址
console.log("地址:" + this.ethAccount);
}
).then(
() => {
axios.post("http://localhost:8089/api/app/user/metamask/verify", {
"address": this.ethAccount
}).then(res => {
this.nonces = res.data.data;
}).then(() => {
console.log("nonces-->" + this.nonces)
console.log("chainId-->" + window.ethereum.chainId)
const from = this.ethAccount;
ethereum.request({
method: 'personal_sign',
params: [from, this.nonces],
}).then((res) => {
this.sign = res;
console.log("签名请求参数->"+this.nonces)
console.log("签名->" + res);
}).then(
() => {
axios.post("http://localhost:8089/api/app/user/metamask/login", {
"chainId": window.ethereum.chainId,
"address": this.ethAccount,
"signature": this.sign
}).then(
res => {
if (res.data.verify) {
localStorage.setItem("token", res.data.token);
localStorage.setItem("expire", Date.now() + 3600000);
localStorage.setItem("userName", this.ethAccount);
console.log("登录成功");
} else {
console.log("登录失败,请重新登录");
}
}
)
})
})
})
</script>
</html>
5、后端SpringBoot代码
1、获取流水号接口
首先拿到地址,根据地址存储nonce到缓存,之这里使用到了Redis 缓存
/**
* metamask login verify 缓存 过期时间15分钟
*/
private static final long METAMASK_VERIFY_EXPIRED = 15 * 60 * 1000;
/**
* metamask 登录流水号获取
*
* @param param 参数
* @return {@link ApiResult}<{@link ?}>
*/
@PostMapping("metamask/verify")
public ApiResult<?> metamaskVerify(@RequestBody MetaMaskVerifyParam param){
// 流水号
String nonce = CommonUtil.randomUUID16();
redisUtil.set(String.format(UserField.USER_META_MASK_LOGIN_NONCE, param.getAddress()),nonce,METAMASK_VERIFY_EXPIRED);
return ApiResult.ok("",nonce);
}
2、登录验签
controller层:
/**
* metamask登录
*
* @param param 参数
* @return {@link ApiResult}<{@link ?}>
*/
@PostMapping("metamask/login")
public ApiResult<?> metamaskLogin(MetaMaskLoginParam param){
return usersService.metamaskLogin(param);
}
service层:
部分业务代码就不提出来了。主要就是MetaMaskUtil.validate这个验签逻辑,验签成功就返回Token
@Override
public ApiResult<?> metamaskLogin(MetaMaskLoginParam param) {
String address = param.getAddress();
String signature = param.getSignature();
// 根据地址获取流水号
String cacheKey = String.format(UserField.USER_META_MASK_LOGIN_NONCE, address);
String message = redisUtil.get(cacheKey);
if (StrUtil.isBlank(message)) {
return ApiResult.fail("metamask message 验签失败");
}
redisUtil.deleteKeys(Collections.singleton(cacheKey));
// 签名,流水号,地址验签
boolean validate = MetaMaskUtil.validate(signature, message, address);
if (validate) {
// 获取绑定的用户
UsersBindWechat usersBindWechat = usersBindWechatService.getByOpenId(address,LoginTypeEnum.META_MASK);
if (usersBindWechat != null) {
Users users = this.getUserById(usersBindWechat.getUserId());
if (users != null) {
AuthResult login = this.login(users, LoginTypeEnum.FAST_WECHAT);
return ApiResult.ok("",login.getAccess_token());
}
}
return getOpenId(address,LoginTypeEnum.META_MASK);
}
return ApiResult.fail("metamask 验签失败");
}
签名工具类MetaMaskUtil:
maven引入 web3j:
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.9.4</version>
</dependency>
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.util.Arrays;
/**
* metamask 工具类
*
* @date 2022/11/01
*/
public class MetaMaskUtil {
/**
* 自定义的签名消息都以以下字符开头
* 参考 eth_sign in https://github.com/ethereum/wiki/wiki/JSON-RPC
*/
public static final String PERSONAL_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n";
public static void main(String[] args) {
// 数据经过处理,使用你自己的地址是可以的
//签名的地址
String address = "0x46301032f02f89e14edf7fb368aa14f71d15242e";
//签名后的数据
String signature = "0x455ebc38758dce3b3ee459a951b92f19545e98be21b2c3d4ddf45246bf2f23612ce710926649b04cdedf90bfba48fc5e18a05f27fd626ea20a12bb28db4779a31c";
//签名原文
String message = "slodUidS1qV5BLIP";
Boolean result = validate(signature,message,address);
System.out.println(result);
}
/**
* 对签名消息,原始消息,账号地址三项信息进行认证,判断签名是否有效
* @param signature
* @param message
* @param address
* @return
*/
public static boolean validate(String signature, String message, String address) {
boolean match = false;
try {
//参考 eth_sign in https://github.com/ethereum/wiki/wiki/JSON-RPC
// eth_sign
// The sign method calculates an Ethereum specific signature with:
// sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))).
//
// By adding a prefix to the message makes the calculated signature recognisable as an Ethereum specific signature.
// This prevents misuse where a malicious DApp can sign arbitrary data (e.g. transaction) and use the signature to
// impersonate the victim.
String prefix = PERSONAL_MESSAGE_PREFIX + message.length();
byte[] msgHash = Hash.sha3((prefix + message).getBytes());
byte[] signatureBytes = Numeric.hexStringToByteArray(signature);
byte v = signatureBytes[64];
if (v < 27) {
v += 27;
}
Sign.SignatureData sd = new Sign.SignatureData(
v,
Arrays.copyOfRange(signatureBytes, 0, 32),
Arrays.copyOfRange(signatureBytes, 32, 64));
String addressRecovered = null;
// Iterate for each possible key to recover
for (int i = 0; i < 4; i++) {
BigInteger publicKey = Sign.recoverFromSignature(
(byte) i,
new ECDSASignature(new BigInteger(1, sd.getR()), new BigInteger(1, sd.getS())),
msgHash);
if (publicKey != null) {
addressRecovered = "0x" + Keys.getAddress(publicKey);
if (addressRecovered.equals(address)) {
match = true;
break;
}
}
}
return match;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
总结
以上就是MetaMask一键登录的应用,主要是验签方式要正确。有问题可以在评论区给我留言。