标题平台对接的数据签名加解密
标题数据签名介绍
接口平台调用第三方接口时会使用私钥对数据签名,第三方务必使用公钥验签,保证接口安全性。
签名规则与流程:
1.筛选,获取所有请求参数后剔除空值参数;
2.排序,将筛选的参数按第一个字符的键ASCII码升序排序,若第一个字符相同则取后一个,以此类推;
3.拼接,将排序后的参数拼接为“参数键=参数值”的格式,并且把这些参数用“&”字符连接起来,此时生成的字符串为待签名字符串;
4.签名,通过私钥对上述待签名字符串进行签名,签名算法为RSA2,请求接口时签名参数为sign;
5.验签,取出所有请求参数后剔除公共参数sign,然后按上述1、2和3的规则处理,通过RSA2算法使用公钥验签。
注意:验签时一定注意剔除公共参数sign,一定注意取出所有的请求参数,防止后续接口增加参数时导致验签失败。
标题加密解密样例代码
生成工具类
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.asymmetric.Sign;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 测试签名与验签
*
* @author 可乐加糖
* @since 2024/6/27
*/
@Slf4j
public class TestSha256WithRsa {
/**
* 测试请求参数的签名与验签
* <p>
* 依赖工具包:<a href="https://github.com/dromara/hutool">hutool</a>
*/
@Test
public void testSignAndVerifyWithHutool() {
// 创建公私钥对
RSA rsa = SecureUtil.rsa();
String privateKeyBase64 = rsa.getPrivateKeyBase64();
log.info("私钥(接口平台不对外暴露):{}", privateKeyBase64);
String publicKeyBase64 = rsa.getPublicKeyBase64();
log.info("公钥(暴露给第三方开发者):{}", publicKeyBase64);
Sign sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, privateKeyBase64, publicKeyBase64);
// 示例请求参数值
Map<String, Object> params = MapUtil.<String, Object>builder()
.put("name", "张三")
.put("age", 18)
.put("gender", "male")
.put("mobile", "15512345678")
.put("address", "")
.put("wechat", null)
.build();
log.info("待签名的参数:{}", params);
// 将参数拼接为字符串
String paramStr = this.buildParamStr(params);
log.info("经过筛选、排序、拼接后的待签名字符串:{}", paramStr);
// 私钥签名,算法为RSA2,即SHA256withRSA
byte[] signBytes = sign.sign(paramStr, StandardCharsets.UTF_8);
String signStr = Base64.encode(signBytes);
log.info("数据签名:{}", signStr);
// 平台方调用第三方接口时,添加公共字段,即签名字段sign
params.put("sign", signStr);
// 第三方验签时,取出签名字段,取出请求参数并拼接为字符串,然后使用公钥验签
String requestSign = (String) params.get("sign");
String requestParams = this.buildParamStr(params);
boolean verify = sign.verify(requestParams.getBytes(StandardCharsets.UTF_8), Base64.decode(requestSign));
log.info("验签结果:{}", verify);
}
/**
* 将参数转换为待签名的字符串
*
* @param params 待签名参数
* @return 待签名字符串
*/
private String buildParamStr(Map<String, Object> params) {
Assert.notEmpty(params, "待签名参数不能为空");
return params.entrySet()
.stream()
// 1.筛选,剔除公共参数和空值参数
.filter(entry -> {
// 剔除公共参数sign
if (StrUtil.equals(entry.getKey(), "sign")) {
return false;
}
// 所有待签名的参数的key和value都不能为null或空串
return ObjectUtil.isAllNotEmpty(entry.getKey(), entry.getValue());
})
// 2.排序,将筛选的参数按第一个字符的键ASCII码升序排序,若第一个字符相同则取后一个,以此类推
.sorted((o1, o2) -> {
String key1 = o1.getKey();
String key2 = o2.getKey();
return key1.compareTo(key2);
})
// 3.拼接,key和value间使用“=”连接,即“key=value”,每组参数间使用“&”连接,即“key1=value1&key2=value2”
.map(entry -> entry.getKey() + "=" + entry.getValue().toString())
.collect(Collectors.joining("&"));
}
}
校验签名方法
// 方式1 verify1(data.getBytes(StandardCharsets.UTF_8), getPublicKey(), sign);
public static boolean verify1(byte[] data, String publicKey, String sign) throws Exception {
byte[] keyBytes = RSAUtils.decryptBASE64(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(pubKey);
signature.update(data);
return signature.verify(RSAUtils.decryptBASE64(sign));
}
// 方式2 verify2(params, getPublicKey(), signStr)
public boolean verify2(Map<String, Object> params,String publicKey,String signStr){
Sign sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, null, publicKey);
String requestParams = this.buildParamStr(params);
boolean verify = sign.verify(requestParams.getBytes(StandardCharsets.UTF_8), Base64.decode(signStr));
return verify;
}
Postman测试
如下图所示
请求体:
{
"deviceid": "113922989",
"startTime": "2024-08-24 12:00:00",
"endTime": "2024-08-25 12:10:00",
"sign": "gFxLUwBlfHjXsiMh6dAJk52dl3LW74NcTC8L5kHAy7es7KIw8Cbj2Sy2VJK9vDwkDU2vKx1J1Y0bTIWqIEgIyvy4DufBfSkyYLOHXfMryNS0dwYHy+O94nlD0vYkML/XqGCcBt99KWWiw8OgfSSRUW0869Kz0c5TzvRxSjodrbc4uRaF/p/Obl9geXEE8rBxlPiFYjVHpZA7ZjkAyMdjXti3atghMFfYu5lkS4AzuyNQ9eijC4XqG3rgpZPrLQAYF4rxBJTkR23seGX5iuNLX9yll1PldlUR6KHG8Z/SDAVQgce/Zm3N198k/YId4odJJVDGZN69eUABULSWSXcFQQ=="
}