背景
腾讯在推后台请求接口切换身份鉴权方式,背景大概是这样:
目前的接口请求身份校验方式使用的是access_token校验方式,该方式存在如下问题和风险:
a、token有效期2小时,需要接入方维护票据
b、ip白名单校验,当请求方更改出口ip时需要及时同步我们,否则会无法请求我们的接口
c、密钥或access_token泄露,都会导致安全风险
现在希望升级身份校验方式,升级为RSA身份签名方式,可以避免上述问题。
签名分析
目前业务中用到的签名验证方式有三种:
md5加密
1、请求参数加md5加盐加密,这个是最简单的实现,根据参数排序,再拼上secret_key,用md5加密生成。双方约定好秘钥就可以校验签名。
优点:实现简单
缺点:md5可以通过暴力破解,如果像前段时间的俄罗斯把国防部会议密码设置成1234就很容易破解了- -
下面是参考例子:
java
public static String md5Encode(String input, String secret_key) throws Exception{
JSONObject json = JSON.parseObject(input);
TreeMap<String, String> data = new TreeMap<String, String>();
for (String key : json.keySet()) {
if ("sign".equals(key)) {
//sign不参与签名
continue;
}
data.put(key, json.getString(key));
}
List<String> params = new ArrayList<String>();
// 重组参数
for (String key : data.keySet()) {
String value = String.format("%s=%s", key, data.get(key));
params.add(value);
}
// 组合参数和签名 secret_key
String temp = URLEncoder.encode(StringUtils.join(params, "&").toLowerCase() + "&key=" + secret_key);
String result = encode(temp);
return new String(result);
}
token认证
2、请求获取token接口,保存到redis中,token过期重新请求token,访问其他接口需要带着token参数验证。
优点: token定时刷新,就算token泄露也只会有一段时间的安全风险。
缺点: token容易泄露,可以通过token+ md5加盐方式校验数据
RSA鉴权
3、使用非对称加密算法RSA签名,生成公钥与私钥,公钥提供出去验证签名,私钥自己保存用于加密签名,生成秘钥链接: www.metools.info/code/c80.ht...
操作步骤:
第一步:与该接口负责人确认,需要参与签名计算的url 参数
第二步:参与签名计算的url参数,按照字母序以key1=value1&key2=value2 排列拼接得到:签名body
第三步:计算body sha256签名
第四步:计算RSA签名
sign = RSASSA-PKCS1-V1_5_SHA256(pviKey, sign)
第五步:urlencode and base64encode
sign = urlencode(base64encode(sign))
第六步:url上带上sign结果
优点:安全性较高
缺点:性能较慢,算法较复杂
测试代码:
java
1、添加依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
2、测试代码(仅供参考)
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
/**
* 签名工具
*/
public class SignUtils {
/**
* 生成私钥对象
*
* @param pkcs1Base64Key PKCS#1格式私钥
* @return
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
public static PrivateKey getPrivateKey(String pkcs1Base64Key) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
byte[] pkcs1Bytes = Base64.getDecoder().decode(pkcs1Base64Key);
AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(PKCSObjectIdentifiers.pkcs_1);
ASN1Object asn1Object = ASN1ObjectIdentifier.fromByteArray(pkcs1Bytes);
PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo(algorithmIdentifier, asn1Object);
byte[] pkcs8Bytes = privateKeyInfo.getEncoded();
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec privateKeySpec = new PKCS8EncodedKeySpec(pkcs8Bytes);
return keyFactory.generatePrivate(privateKeySpec);
}
/**
* 计算签名
*
* @param message 参与签名的数据
* @param privateKey 私钥对象
* @return
* @throws Exception
*/
public static String signMessage(String message, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(message.getBytes(StandardCharsets.UTF_8));
return URLEncoder.encode(Base64.getEncoder().encodeToString(signature.sign()), StandardCharsets.UTF_8);
}
// 测试
public static void main(String[] args) throws Exception {
// PKCS#1格式私钥
String privateKeyString = """
-----BEGIN RSA PRIVATE KEY-----
xxx
-----END RSA PRIVATE KEY-----
""";
privateKeyString = privateKeyString.replace("\n", "")
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replace(" ", "");
// 构建私钥对象(可在初始化时创建,不用每次生成)
PrivateKey privateKey = getPrivateKey(privateKeyString);
// 参与签名数据
String message = "Q-UA=QV=1&PR=VIDEO&PT=TEST&CHID=10011";
// 构建签名
String sign = signMessage(message, privateKey);
System.out.println(sign);
}
}
上面的例子是腾讯给的,没有用过,应该也是可以的。java网上很多例子默认是支持PKCS8格式的,如果想要用PKCS1,可以添加以下代码
java
导入
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
static{
java.security.Security.addProvider( new org.bouncycastle.jce.provider.BouncyCastleProvider() );
}
问题分析
在接入RSA签名的时候遇到了一点问题,RSA对比md5和token方式还是复杂一点。在选择秘钥生成的时候可以选择秘钥长度和秘钥格式,这里要与合作方规定好,我们规定的是长度1024,格式PKCS1。
在对接文档里面看到:RSA签名目前支持「PKCS1v15」和「RSAPSS」两种鉴权模式,推荐使用后者,安全性更高。
我又看了一下这两个模式是什么东西。。java的参数好像没有这个选择,一般java都是用SHA256withRSA,后面发现这个底层好像就是PKCS1v15
总结
看腾讯的对接文档是真的快乐,基本demo都写好了,只需要接入。大家可以按需使用合适的签名方式。除了MD5加盐、Token验证、RSA加密这三种还有其他常见的签名方式吗?评论区可以交流。