OpenAPI 开放平台
内容管理
- Open API intro
- 腾讯云OpenAPI
- OpenAPI请求
- API密钥管理
- 云API签名过程
- 拼接规范请求串 CanonicalRequest
- 拼接待签名字符串
- 计算签名
- 拼接到Authorization中
- 云API签名失败规范
- Token 和 长期密钥
- 公共参数
- OpenApi设计
- AppId、AppSecret
- sign签名
- timestamp 时间戳保证有效期
- nonce保证一次性,避免重放
- 黑白名单
- 限流、熔断、降级
- 合法性校验 (参数)
对于OpenAPI开放平台的intro和 理解
之前对于OpenAPI的理解只是局限在调用Google的相关接口返回数据… 实际上OpenAPI的设计需要思考很多
Cfeng 最近接触到OpenAPI的内容,需要规范化设计OpenAPI,like 抖音开放平台、腾讯云的OpenAPI,这里分享一下自我见解
Open API intro
Open API 就是开放API,OpenAPI是服务型web项目的一种应用,网站(应用)服务商将自己网站的服务封装为一系列API 开放Open出去, 供第三方开发者使用, OpenAPI 也就是开放网站部分服务API
提供OpenAPI的平台(网站等)就是开放平台
开放平台: 第一种为技术型开放,比如baidu等将需求开放给第三方开发者, 第二种是软件系统(应用)公开 API或者 函数function,使外部程序可以 使用软件系统的资源,增加功能,而不需要更改该软件系统的代码
简单理解, 就是当前软件系统公开的API接口,第三方的开放者可以调用该接口获取当前软件系统的资源
这里就涉及到了鉴权、认证等具体操作流程
腾讯云OpenAPI
Cfeng注册了各个开放平台的账户,查看OpenAPI的设计, 这里就以腾讯云为例,各个平台的设计思路都是差不多的
OpenAPI请求
请求调用: 服务地址, 通信协议, 请求方法, 字符编码
调用腾讯云服务的OpenAPI需要提供服务地址,也就是开放OpenAPI的服务器地址,eg :cvm.tencentcloudapi.com;
通信协议 当然采用安全的HTTPS,高安全性
请求方法: 比如GET和POST【application/json; …】
字符编码: 采用UTF-8
API密钥管理
使用API密钥签名API请求, 腾讯云收到请求,比对签名串,验证通过才会给与资源【腾讯云控制台SecretKey无法直接查看,只能发送短信短暂查看】,密钥就和token一样,如果泄露别人会获得相同的权限造成损失;
给出的密钥保护方式: 将密钥隐藏在环境变量中, 存放在安全位置…
当需要发起请求时,通过变量方式调用,塞入请求头进行签名…
import os
from tencentcloud.common import credential
from tencentcloud.cvm.v20170312 import cvm_client, models
try:
cred = credential.Credential(
os.environ.get("TENCENTCLOUD_SECRET_ID"),
os.environ.get("TENCENTCLOUD_SECRET_KEY"))
client = cvm_client.CvmClient(cred, "ap-guangzhou")
req = models.DescribeInstancesRequest()
resp = client.DescribeInstances(req)
print(resp)
except Exception as err:
print(err)
# 像这里使用python, 这里也是包装过的了,像cred和client直接就会塞入请求头, req指定一个Action, 就可以发起请求获取资源
按照解释,API密钥代表的就是账号身份以及所拥有的权限,使用API 就可以直接操作用户名下的所有的资源【普通用户可能是图形界面的方式,开放者就可以直接调用API获取自己拥有的资源进行操作】
密钥可以直接生成,包含SecretId和SecretKey, 密钥是构建API的重要凭证, 用于调用时生成签名 【密钥可以手动禁用, 并且可以在控制台查看密钥最近使用的时间决定禁用与否】
腾讯云的API会对每一个调用请求进行身份验证,用户需要使用安全凭证,经过特定的步骤对请求进行签名Signature, 需要在请求的公共参数中指定签名结果并以指定的方式发送 request
签名Signature的目的就是验证请求者的身份(确保request是持有密钥的人发送的,防止劫持),以及保护传输数据,【加密】,发送方使用签名算法生成一个Hash值 连同请求一起发送, 服务器收到后以同样的过程计算,并验证hash值,如果被篡改,那么API拒绝此请求
腾讯云中使用的安全凭证就是密钥, 包括SecretId和SecretKey, 每个用户最多两对密钥
SecretId: 标识API调用者身份, 就像用户名
SecretKey: 验证API调用者身份,like 密码
用户需要严格保管好安全凭证,避免泄露
密钥管理中控制台可见的字段为: APPID(生成的两个密钥对的APPID相同)、 密钥(ID和Key)、创建时间,最近访问时间,状态(2: 有效, 3: 禁用 ,4: 已经删除)、操作
云API签名过程
按照OpenAPI业务接口文档,第三方用户可以直接直接访问相关的接口获取资源, 首先访问的就是服务提供方的服务器, 接口也就是服务器上Open的接口
比如tecent云的访问实例:
访问者访问需要提供公共参数和接口参数,访问者的SecretId 为 AKID…, SecretKey为GU5…
curl -X POST https://cvm.tencentcloudapi.com \
-H "Authorization: TC3-HMAC-SHA256 Credential=AKID..../2023-01-01/cvm/tc3_request, SignedHeaders=content-type;host, Signature=22398u89hufahkgjaglkahhgaj
-H "ContentType: application/json; charset=utf-8"
-H "X-TC-Action: DescribeInstances" \
-H "X-TC-Timestamp: 1551113065" \
-H "X-TC-Version: 2017-03-12" \
-H "X-TC-Region: ap-guangzhou" \
-d '{"Limit": 1, "Filters": [{"Values": ["\u672a\u547d\u540d"], "Name": "instance-name"}]}'
按照这个请求,可以看到最重要的就是Authorization, 其中的Credential代表的就是SecretID,标识身份,后面加上请求的Date/service/云固定结尾tc3_request, 后面再跟上signheaders代表参与签名的头,就可以计算签名;
后面的signature就是签名串, 服务端按照同样的过程签名和该signature对比验证是否篡改
签名的过程为:
拼接规范请求串 CanonicalRequest
CanonicalRequest =
HTTPRequestMethod + '\n' + 【请求方法】
CanonicalURI + '\n' + 【URI参数 /】
CanonicalQueryString + '\n' + 【查询字符串,post为空,get为?后部分】
CanonicalHeaders + '\n' + 【参与签名的头部信息,按照key:value\n】
SignedHeaders + '\n' + 【参与签名的头部, 就是key1;key2】
HashedRequestPayload 【请求正文body】比如对正文SHA256哈希,得到signature,这里假设为35e...,Get请求为空】
得到规范的请求串
POST
/
content-type:application/json; charset=utf-8
host:cvm.tencentcloudapi.com
content-type;host
35e9c5b0e3ae67532d3c9f17ead6c90222632e5b1ff7f6e89887f1398934f064
拼接待签名字符串
签名串格式
StringToSign =
Algorithm + \n + 【签名算法: SHA256]
RequestTimestamp + \n + 【请求时间戳】
CredentialScope + \n + 【凭证范围: Data/service/tc3_REQUEST, eg: /2023-01-01/cvm/tc3_request,cvm就是具体的产品】
HashedCanonicalRequest 请求串【规范请求串再次SHA256得到的签名串】
按照该格式,就可以得出一个实例的签名字符串
TC3-HMAC-SHA256
1551113065
2019-02-25/cvm/tc3_request
5ffe6a04c0664d6b969fab9a13bdab201d63ee709638e2749d62a09ca18d7031
计算签名
- 签名密钥【计算得出】
SecretKey = "Gu5t9xGARNpq86cd98joQYCN3*******"
SecretDate = HMAC_SHA256("TC3" + SecretKey, Date)
SecretService = HMAC_SHA256(SecretDate, Service)
SecretSigning = HMAC_SHA256(SecretService, "tc3_request")
SecretKey就是用户自己持有的密钥Key,Date为访问的Date信息,Service就是请求的产品服务,比如cvm
这里的计算密钥过程,使用初始密钥Key 加入Date签名,再加入请求的服务再次签名,再将固定的结尾串tc3_request加入签名 得到一个 派生密钥
- 计算签名
将上面的派生密钥加入到待签名字符串中进行签名
Signature = HexEncode(HMAC_SHA256(SecretSigning, StringToSign))
拼接到Authorization中
Authorization就是用户访问OpenAPI最关键的信息, 服务端不仅需要验证请求者身份和其对应的权限,还需要防止请求篡改
Authorization =
Algorithm + ' ' +
'Credential=' + SecretId + '/' + CredentialScope + ', ' +
'SignedHeaders=' + SignedHeaders + ', ' +
'Signature=' + Signature
Algorithm就是整个签名过程的签名算法,Credential 中包含的就是SecretId和CredentialScope 【date/service/ninetech_request】signedHeaders参与签名的头部, Signature就是签名的结果
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
public class TencentCloudAPITC3Demo {
private final static Charset UTF8 = StandardCharsets.UTF_8;
private final static String SECRET_ID = "AKIDz8krbsJ5yKBZQpn74WFkmLPx3*******";
private final static String SECRET_KEY = "Gu5t9xGARNpq86cd98joQYCN3*******";
private final static String CT_JSON = "application/json; charset=utf-8";
public static byte[] hmac256(byte[] key, String msg) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
mac.init(secretKeySpec);
return mac.doFinal(msg.getBytes(UTF8));
}
public static String sha256Hex(String s) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] d = md.digest(s.getBytes(UTF8));
return DatatypeConverter.printHexBinary(d).toLowerCase();
}
public static void main(String[] args) throws Exception {
String service = "cvm";
String host = "cvm.tencentcloudapi.com";
String region = "ap-guangzhou";
String action = "DescribeInstances";
String version = "2017-03-12";
String algorithm = "TC3-HMAC-SHA256";
String timestamp = "1551113065";
//String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 注意时区,否则容易出错
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
// ************* 步骤 1:拼接规范请求串 *************
String httpRequestMethod = "POST";
String canonicalUri = "/";
String canonicalQueryString = "";
String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n";
String signedHeaders = "content-type;host";
String payload = "{\"Limit\": 1, \"Filters\": [{\"Values\": [\"\\u672a\\u547d\\u540d\"], \"Name\": \"instance-name\"}]}";
String hashedRequestPayload = sha256Hex(payload);
String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
+ canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
System.out.println(canonicalRequest);
// ************* 步骤 2:拼接待签名字符串 *************
String credentialScope = date + "/" + service + "/" + "tc3_request";
String hashedCanonicalRequest = sha256Hex(canonicalRequest);
String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
System.out.println(stringToSign);
// ************* 步骤 3:计算签名 *************
byte[] secretDate = hmac256(("TC3" + SECRET_KEY).getBytes(UTF8), date);
byte[] secretService = hmac256(secretDate, service);
byte[] secretSigning = hmac256(secretService, "tc3_request");
String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
System.out.println(signature);
// ************* 步骤 4:拼接 Authorization *************
String authorization = algorithm + " " + "Credential=" + SECRET_ID + "/" + credentialScope + ", "
+ "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
System.out.println(authorization);
TreeMap<String, String> headers = new TreeMap<String, String>();
headers.put("Authorization", authorization);
headers.put("Content-Type", CT_JSON);
headers.put("Host", host);
headers.put("X-TC-Action", action);
headers.put("X-TC-Timestamp", timestamp);
headers.put("X-TC-Version", version);
headers.put("X-TC-Region", region);
StringBuilder sb = new StringBuilder();
sb.append("curl -X POST https://").append(host)
.append(" -H \"Authorization: ").append(authorization).append("\"")
.append(" -H \"Content-Type: application/json; charset=utf-8\"")
.append(" -H \"Host: ").append(host).append("\"")
.append(" -H \"X-TC-Action: ").append(action).append("\"")
.append(" -H \"X-TC-Timestamp: ").append(timestamp).append("\"")
.append(" -H \"X-TC-Version: ").append(version).append("\"")
.append(" -H \"X-TC-Region: ").append(region).append("\"")
.append(" -d '").append(payload).append("'");
System.out.println(sb.toString());
}
}
云API签名失败规范
签名是可能失败的,统一定义错误码类AuthFailure
SignatureExpire : 签名过期,timestamp 和接收到请求时间超过5min
SecretIdNotFound: 密钥不存在, 可能是密钥书写错误或者被禁用
SignatureFailure: 签名错误, 可能是SecretKey错误,就是内容不符合
TokenFailure: 临时证书Token错误
InvalidSecretId: 密钥非法, 类型错误...
Token 和 长期密钥
按照腾讯云的intro, Token是安全凭证服务申请的临时的凭证,而密钥是长期有效的
而凭证服务也是直接调用系统的API达到,such as:
- 申请扮演角色: AssumeRole 【公共参数Action、Version、Region】,接口私有参数【RoleArn: 角色资源描述, 比如qcs::cam::uin/1234:roleName/testRoleName 、 RoleSessionName: 临时会话名称 、 DurationSeconds: 临时证书有效期,default 7200s 、 Policy 、 ExternId、 Tags.N 、 SourceIdentity】, 输出参数就包括Credentials 临时安全证书(Token: String、 TmpSecretId 临时证书密钥ID、 TmpSecretKey: key)、 ExpiredTime(时间戳过期时间)、Expiration(无效时间iso格式)、 RequestId(唯一请求ID,每次请求都有)
- 申请OIDC角色临时密钥: AssumeRoleWithWebIdentity 【三个公共参数、ProviderId: 身份提供商名称、 webIdentityToken: IdP签发OIDC令牌、 RoleArn: 角色描述名称、 RoleSessionName: 会话名称、 DurationSeconds: 有效期】, 输出参数和上方一致
- 获取当前调用者身份信息 : GetCallerIdentity : 支持主账号长期密钥、以及AssumeRole等临时密钥身份获取, 输入参数就3个公共参数; 输出参数包括 【Arn 、 AccountId: 主账号uin、 UserId: 身份标识、 PrincipalId: 密钥所属账号uin、 Type: 身份类型、 RequestID】
- 获取联合身份临时凭证: GetFederationToken 【三个公共参数, Name: 调用方名称自定义、 Policy( 授予临时证书的CAM策略)、 DurationSeconds】; 输出参数就是👆,临时的一个证书和其失效时间
公共参数
公共参数是标识用户和接口签名的参数, 每一个接口访问都需要携带才能正常发起请求
公共参数需要统一放到HTTP Header请求头中
- Action String X-TC-Action: 操作的接口名称,比如查询服务器DescribeInstances
- Timestamp: Integer X-TC-Timestamp: 当前时间戳,如果与当前时间超过5min则过期
- Version : String X-TC-Version 操作的API版本,比如2023-01
- Authorization: String 标准身份认证字段,包含内容详见上面👆,比如签名算法、签名凭证、参与签名的头部、签名生成的摘要 【使用密钥Secret参与操作】
- Token: String X-TC-Token, 安全凭证服务办法的临时安全凭证中的Token【注意是临时】,使用的时候也就是将SecretKey和ID替换为TmpSecretId和Key, 使用长期密钥Secret时不设置该Token字段
- Language: String(可选),X-TC-Language,接口返回语言,比如zh-CN、en-US
- Region : String (可选) 地域
按照腾讯云的示例,开发者用户查询云服务器示例列表前10个,偏移量Offset=0,Limit = 10
https://cvm.tencentcloudapi.com/?Limit=10&Offset=0
Authorization: TC3-HMAC-SHA256 Credential=AKID********EXAMPLE/2018-10-09/cvm/tc3_request, SignedHeaders=content-type;host, Signature=5da7a33f6993f0614b047e5df4582db9e9bf4672ba50567dba16c6ccf174c474
Content-Type: application/x-www-form-urlencoded
Host: cvm.tencentcloudapi.com
X-TC-Action: DescribeInstances
X-TC-Version: 2017-03-12
X-TC-Timestamp: 1539084154
X-TC-Region: ap-guangzhou
请求头中最主要的就是Authorization, 之后就是Content-type、host和公共参数(Action、version、timestamp、region)
除了公共参数,剩下的就是具体访问接口的具体的参数,类似上面的申请临时证书服务
OpenApi设计
Cfeng结合导师的讲解和腾讯云资料查看,对于OpenAPI的思考暂时如下:
首先需要的是统一、规范的接口标准, 快速接入,统一对外的接口, 最需要保证的就是接口安全性,利用密钥签名实现,同时不只是针对RPA项目,所以需要考虑复用性, 减少对于其他内容的调用,低耦合
AppId、AppSecret
或者是SecretId和SecretKey,id标识用户,key为密码, 接口请求会根据规则生成签名, 服务器会验证签名
ID的生成只需要保证全局唯一即可,对应用户; AppSeret生成也简单,只要和APPID能够关联即可
sign签名
非对称加密算法:例如RSA 私钥加密,公钥解密; 结合摘要算法,摘要后,私钥加密,公钥解密,可以用在签名中
摘要算法:例如MD5 不需要密钥不可逆,不能根据密文反推明文
签名: 数据防止篡改 , 防止身份冒充
- 数据防止篡改: signature数据和计算数据相同,说明没有篡改,开放资源
- 身份防冒充: 比如使用SHA256withRSA, 将数据先进行SHA256计算, 在使用RSA私钥加密,对方解密也是一样,先用RSA公钥解密,再计算
这里按照腾讯云的思路,用户可以申请密钥对,申请成功就拥有SecretId和SecretKey
公私钥 接口提供方生成, 把私钥交给请求方
这里参照腾讯云,同时参照的给出一个模拟代码,就是验证的过程
package openApi;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Hex;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
public class AppUtils {
/**
* key:appId、value:appSecret
*/
static Map<String, String> appMap = Maps.newConcurrentMap();
/**
* 分别保存生成的公私钥对
* key:appId,value:公私钥对
*/
static Map<String, Map<String, String>> appKeyPair = Maps.newConcurrentMap();
public static void main(String[] args) throws Exception {
// 模拟生成appId、appSecret
String appId = initAppInfo();
// 根据appId生成公私钥对
initKeyPair(appId);
// 模拟请求方
String requestParam = clientCall();
// 模拟提供方验证
serverVerify(requestParam);
}
private static String initAppInfo() {
// appId、appSecret生成规则,依据之前介绍过的方式,保证全局唯一即可
String appId = "123456";
String appSecret = "654321";
appMap.put(appId, appSecret);
return appId;
}
private static void serverVerify(String requestParam) throws Exception {
APIRequestEntity apiRequestEntity = JSONObject.parseObject(requestParam, APIRequestEntity.class);
Header header = apiRequestEntity.getHeader();
UserEntity userEntity = JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()), UserEntity.class);
// 首先,拿到参数后同样进行签名
String sign = getSHA256Str(JSONObject.toJSONString(userEntity));
if (!sign.equals(header.getSign())) {
throw new Exception("数据签名错误!");
}
// 从header中获取相关信息,其中appSecret需要自己根据传过来的appId来获取
String appId = header.getAppId();
String appSecret = getAppSecret(appId);
String nonce = header.getNonce();
String timestamp = header.getTimestamp();
// 按照同样的方式生成appSign,然后使用公钥进行验签
Map<String, String> data = Maps.newHashMap();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("sign", sign);
data.put("timestamp", timestamp);
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
if (!rsaVerifySignature(sb.toString(), appKeyPair.get(appId).get("publicKey"), header.getAppSign())) {
throw new Exception("公钥验签错误!");
}
System.out.println();
System.out.println("【提供方】验证通过!");
}
public static String clientCall() {
// 假设接口请求方与接口提供方,已经通过其他渠道,确认了双方交互的appId、appSecret
String appId = "123456";
String appSecret = "654321";
String timestamp = String.valueOf(System.currentTimeMillis());
// 应该为随机数,演示随便写一个
String nonce = "1234";
// 业务请求参数
UserEntity userEntity = new UserEntity();
userEntity.setUserId("1");
userEntity.setPhone("13912345678");
// 使用sha256的方式生成签名
String sign = getSHA256Str(JSONObject.toJSONString(userEntity));
Map<String, String> data = Maps.newHashMap();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("sign", sign);
data.put("timestamp", timestamp);
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
System.out.println("【请求方】拼接后的参数:" + sb.toString());
System.out.println();
// 使用sha256withRSA的方式对header中的内容加签
String appSign = sha256withRSASignature(appKeyPair.get(appId).get("privateKey"), sb.toString());
System.out.println("【请求方】appSign:" + appSign);
System.out.println();
// 请求参数组装
Header header = Header.builder()
.appId(appId)
.nonce(nonce)
.sign(sign)
.timestamp(timestamp)
.appSign(appSign)
.build();
APIRequestEntity apiRequestEntity = new APIRequestEntity();
apiRequestEntity.setHeader(header);
apiRequestEntity.setBody(userEntity);
String requestParam = JSONObject.toJSONString(apiRequestEntity);
System.out.println("【请求方】接口请求参数: " + requestParam);
return requestParam;
}
/**
* 私钥签名
*
* @param privateKeyStr
* @param dataStr
* @return
*/
public static String sha256withRSASignature(String privateKeyStr, String dataStr) {
try {
byte[] key = Base64.getDecoder().decode(privateKeyStr);
byte[] data = dataStr.getBytes();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data);
return new String(Base64.getEncoder().encode(signature.sign()));
} catch (Exception e) {
throw new RuntimeException("签名计算出现异常", e);
}
}
/**
* 公钥验签
*
* @param dataStr
* @param publicKeyStr
* @param signStr
* @return
* @throws Exception
*/
public static boolean rsaVerifySignature(String dataStr, String publicKeyStr, String signStr) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr));
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(dataStr.getBytes());
return signature.verify(Base64.getDecoder().decode(signStr));
}
/**
* 生成公私钥对
*
* @throws Exception
*/
public static void initKeyPair(String appId) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String, String> keyMap = Maps.newHashMap();
keyMap.put("publicKey", new String(Base64.getEncoder().encode(publicKey.getEncoded())));
keyMap.put("privateKey", new String(Base64.getEncoder().encode(privateKey.getEncoded())));
appKeyPair.put(appId, keyMap);
}
private static String getAppSecret(String appId) {
return String.valueOf(appMap.get(appId));
}
@SneakyThrows
public static String getSHA256Str(String str) {
MessageDigest messageDigest;
messageDigest = MessageDigest.getInstance("SHA-256");
byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(hash);
}
}
使用算法就是SHA256
timestamp 时间戳保证有效期
时间戳是一个重要手段,可以防止同一个请求参数被无限使用,比如验证5分钟有效期
long now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
if ((now - Long.parseLong(timestamp)) / 1000 / 60 >= 5) {
throw new Exception("请求过期!");
}
nonce保证一次性,避免重放
接口请求方生成随机数,实现接口一次性有效,避免重放攻击
接口请求方每次请求都会随机生成一个不重复的nonce值,接口提供方可以使用存储容器来存储该nonce, 请求到来查看缓存中是否包含【可以放redis,有效期5min即可, 先校验是否过期,再校验是否重放】
String str = cache.getIfPresent(appId + "_" + nonce);
if (Objects.nonNull(str)) {
throw new Exception("请求失效!");
}
黑白名单
这个针对的就是具体的运维过程了,可以对于某些IP进行黑白名单限制,降低风险
限流、熔断、降级
OpenAPI需要充分考虑限流、熔断、降级问题, 需要约定清楚,保证系统的功能,特别就是限流的大小
合法性校验 (参数)
像腾讯云一样,公共参数、接口参数,哪些参数必填,必填的业务参数、业务参数校验保证合法性
综合来看,OpenAPI起始就是以接口的方式公开系统资源, 需要注意的就是安全性 和 设计的复用性
当前cfeng考虑的就是参照腾讯的设计来设计OpenAPI,使用SHA256, 需要做密钥管理和一个临时的证书管理,目前需要考虑的问题: 发放长期密钥时,如何保证安全性? 长期密钥和token不同时使用,如何进一步保证密钥安全性? 密钥 和token 和refreshtoken的关系?
其实最核心的问题就是: 我给用户发放密钥时(通过HTTP),怎么保证不被其他人盗取
接下来就是开放平台的规范化设计了🌳