MetaMask SpringBoot实现前后端一键登录

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一键登录的应用,主要是验签方式要正确。有问题可以在评论区给我留言。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
UniApp是一个基于Vue.js的跨平台应用开发框架,可以用于开发iOS、Android、H5、小程序等多个平台的应用程序。而MetaMask是一个用于与以太坊区块链交互的浏览器插件。在UniApp中使用MetaMask插件可以实现与以太坊区块链的交互,比如进行以太币的转账、查询以太坊上的智能合约等操作。 在UniApp中使用MetaMask插件需要进行一些步骤。首先,要检测用户是否安装了MetaMask插件,可以使用detectEthereumProvider()函数来进行检测。如果检测到安装了MetaMask插件,可以调用startApp()函数来初始化应用程序。在startApp()函数中,会判断当前提供者是否为window.ethereum,如果不是则提示用户安装了多个钱包,加载失败。如果是window.ethereum提供的提供者,则进行登录操作。 需要注意的是,虽然在这个过程中会使用到连接到以太坊区块链的工具MetaMask,但实际上这个登录过程并不需要区块链,只需要MetaMask的加密功能。但由于MetaMask的广泛应用,现在是介绍此登录流程的好时机。 所以,在UniApp中使用MetaMask插件可以实现与以太坊区块链的交互,开发者可以根据需求使用MetaMask插件进行以太币的转账、查询智能合约等操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [uniapp连接到MetaMask钱包插件](https://blog.csdn.net/qq_47381208/article/details/127221503)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [第二十九课 如何实现MetaMask签名授权后DAPP一键登录功能?](https://blog.csdn.net/wangdenghui2005/article/details/83450493)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值