【Cfeng Work】 Open API的intro和 梳理

OpenAPI 开放平台


对于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 私钥加密,公钥解密; 结合摘要算法,摘要后,私钥加密,公钥解密,可以用在签名中

img

摘要算法:例如MD5 不需要密钥不可逆,不能根据密文反推明文

签名: 数据防止篡改 , 防止身份冒充

  • 数据防止篡改: signature数据和计算数据相同,说明没有篡改,开放资源
  • 身份防冒充: 比如使用SHA256withRSA, 将数据先进行SHA256计算, 在使用RSA私钥加密,对方解密也是一样,先用RSA公钥解密,再计算

这里按照腾讯云的思路,用户可以申请密钥对,申请成功就拥有SecretId和SecretKey

公私钥 接口提供方生成, 把私钥交给请求方

img

这里参照腾讯云,同时参照的给出一个模拟代码,就是验证的过程

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),怎么保证不被其他人盗取

接下来就是开放平台的规范化设计了🌳

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值