最近开发DAPP项目需要使用以太坊账号,与微信/QQ/微博等OAuth认证登陆类似。
老外写的这个认证流程不错,流程与代码都非常清晰,使用node.js实现
One-Click Login with Blockchain: a MetaMask Tutorial
github:https://github.com/amaurymartiny/login-with-metamask-demo
认证流程
- Step 1: Modify the User Model (Back-end)
- Step 2: Generate Nonces (Back-end)
- Step 3: User Fetches Their Nonce (Front-end)
- Step 4: User Signs the Nonce (Front-end)
- Step 5: Signature Verification (Back-end)
- Step 6: Change the Nonce (Back-end)
因为项目后端为java开发,这里使用Java来对签名消息做认证。
- Injected web3(Metamask web3js)
这里主要讲下后端Step 5: Signature Verification,其他步骤都比较简单
后端使用
- Spring MVC
- web3j(https://github.com/web3j/web3j)
集成web3j
web3j提供了springboot的集成方式,https://github.com/web3j/web3j-spring-boot-starter
<dependency>
<groupId>org.web3j</groupId>
<artifactId>web3j-spring-boot-starter</artifactId>
<version>1.6.0</version>
</dependency>
签名消息认证工具
本文最重要的部分,也是作者参考资料文献尝试了好多遍之后的成果,任何消息处理的步骤不对都会导致认证失败。
package com.bc.utils;
import org.web3j.crypto.*;
import org.web3j.utils.Numeric;
import org.web3j.crypto.Sign.SignatureData;
import java.math.BigInteger;
import java.util.Arrays;
/**
* 以太坊签名消息校验工具
*/
public class CryptoUtils {
/**
* 以太坊自定义的签名消息都以以下字符开头
* 参考 eth_sign in https://github.com/ethereum/wiki/wiki/JSON-RPC
*/
public static final String PERSONAL_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n";
/**
* 对签名消息,原始消息,账号地址三项信息进行认证,判断签名是否有效
*
* @param signature
* @param message
* @param address
* @return
*/
public static boolean validate(String signature, String message, String address) {
//参考 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;
}
SignatureData sd = new SignatureData(
v,
Arrays.copyOfRange(signatureBytes, 0, 32),
Arrays.copyOfRange(signatureBytes, 32, 64));
String addressRecovered = null;
boolean match = false;
// 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;
}
}
Controller样例
import org.web3j.crypto.WalletUtils;
import com.bc.utils.CryptoUtils;
@RequestMapping(value = "/oauth/metamask", method = RequestMethod.POST)
@ResponseBody
public ResponseMessage login(@RequestBody HashMap requestObject) throws InterruptedException {
final String publicAddress = (String) requestObject.get("publicAddress");
final String signature = (String) requestObject.get("signature");
final String message = (String) requestObject.get("message");
// 地址合法性校验
if (!WalletUtils.isValidAddress(publicAddress)) {
// 不合法直接返回错误
return Result.error("地址格式非法!");
}
// 校验签名信息
if (!CryptoUtils.validate(signature, message, publicAddress)) {
return Result.error("签名校验失败!");
}
// 校验通过,publicAddress 相当于就是OAuth的openid, 根据该账号做其他业务处理
...
//
// JWT token
...
}
大功告成!
其他问题
正式环境使用小狐狸的话还要考虑以下几点:
- 小狐狸是否连接到了你想要的区块链网络
- 页面侦测小狐狸的账号切换,切换之后应用作相应处理