设计方案概述

API密钥生成:为每个三方应用生成唯一的API密钥对(AK/SK),其中AK用于标识应用,SK用于进行签名和加密。

AK:Access Key Id,用于标示用户。
SK:Secret Access Key,是用户用于加密认证字符串和用来验证认证字符串的密钥,其中SK必须保密。
通过使用Access Key Id / Secret Access Key加密的方法来验证某个请求的发送者身份。

接口鉴权:在进行接口调用时,客户端需要使用AK和请求参数生成签名,并将其放入请求头或参数中以进行身份验证。

回调地址设置:三方应用提供回调地址,用于接收异步通知和回调结果。

接口API设计:设计接口的URL、HTTP方法、请求参数、响应格式等细节。

权限划分

appID:应用的唯一标识

用来标识你的开发者账号的, 即:用户id, 可以在数据库添加索引,方便快速查找,同一个 appId 可以对应多个 appKey+appSecret,达到权限的

appKey:公匙(相当于账号)

公开的,调用服务所需要的密钥。是用户的身份认证标识,用于调用平台可用服务.,可以简单理解成是账号。

appSecret:私匙(相当于密码)

签名的密钥,是跟appKey配套使用的,可以简单理解成是密码。

token:令牌(过期失效)

为什么 要有appKey + appSecret 这种成对出现的机制呢,?

  • 因为要加密, 通常用在首次验证(类似登录场景), 用 appKey(标记要申请的权限有哪些) + appSecret(密码, 表示你真的拥有这个权限)来申请一个token, 就是我们经常用到的 accessToken(通常拥有失效时间), 后续的每次请求都需要提供accessToken 表明验证权限通过。

现在有了统一的appId,此时如果针对同一个业务要划分不同的权限,比如同一功能,某些场景需要只读权限,某些场景需要读写权限。这样提供一个appId和对应的秘钥appSecret就没办法满足需求。 此时就需要根据权限进行账号分配,通常使用appKey和appSecret。

  • 由于 appKey 和 appSecret 是成对出现的账号, 同一个 appId 可以对应多个 appKey+appSecret, 这样平台就为不同的appKey+appSecret对分配不一样的权限,
    • 可以生成两对appKey和appSecret。一个用于删除,一个用于读写,达到权限的细粒度划分。如 : appKey1 + appSecect1 只有删除权限 但是 appKey2+appSecret2 有读写权限… 这样你就可以把对应的权限 放给不同的开发者。其中权限的配置都是直接跟appKey 做关联的, appKey 也需要添加数据库索引, 方便快速查找

签名流程

三方接口调用设计方案

签名规则

分配appId(开发者标识)appSecret(密钥),给 不同的调用方

可以直接通过平台线上申请,也可以线下直接颁发。appId是全局唯一的,每个appId将对应一个客户,密钥appSecret需要高度保密。

加入timeStamp(时间戳),以服务端当前时间为准,单位为ms ,5分钟内数据有效

时间戳的目的就是为了减轻DOS攻击。防止请求被拦截后一直尝试请求接口。服务器端设置时间戳阀值,如果服务器时间 减 请求时间戳超过阀值,表示签名超时,接口调用失败。

加入临时流水号nonce,至少为10位 ,有效期内防重复提交。

随机值nonce 主要是为了增加签名sign的多变性,也可以保护接口的幂等性,相邻的两次请求nonce不允许重复,如果重复则认为是重复提交,接口调用失败。

  • 针对查询接口,流水号只用于日志落地,便于后期日志核查。
  • 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。

加入签名字段sign,获取调用方传递的签名信息。

通过在接口签名请求参数加上 时间戳appId + sign 解决身份验证和防止 ”参数篡改“
1.请求携带参数appId和Sign,只有拥有合法的身份appId和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题。
2.即使请求参数被劫持,由于获取不到appSecret(仅作本地加密使用,不参与网络传输),也无法伪造合法的请求。

调用方实际调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private Map<String, Object> buildDownstreamRequest(Map<String, Object> solution, String faid) {
long timestamp = System.currentTimeMillis();
String appKey = reportProperties.getAppKey().trim();
String appSecret = reportProperties.getAppSecret().trim();
String signData = EcologicalSolutionSm2SignUtil.buildSignData(appKey, appSecret, timestamp);
String signature = EcologicalSolutionSm2SignUtil.sign(appSecret, signData);

Map<String, Object> body = new LinkedHashMap<>();
body.put("appKey", appKey);
body.put("timestamp", String.valueOf(timestamp));
body.put("signature", signature);
body.put("solutionId", toStringValue(solution.get("solutionId")));
body.put("orgCode", toStringValue(solution.get("orgCode")));
body.put("solutionYear", parseSolutionYear(solution.get("solutionYear")));
body.put("solutionName", toStringValue(solution.get("solutionName")));
putIfNotBlank(body, "solutionCode", solution.get("solutionCode"));
body.put("isInvolveEnterprise", toStringValue(solution.get("isInvolveEnterprise")));
body.put("sourceCode", toStringValue(solution.get("sourceCode")));
body.put("sourceName", toStringValue(solution.get("sourceName")));
putIfNotBlank(body, "solutionSourceTypeCode", solution.get("solutionSourceTypeCode"));
putIfNotBlank(body, "solutionCheckReason", solution.get("solutionCheckReason"));
body.put("solutionCheckCategoryCode", toStringValue(solution.get("solutionCheckCategoryCode")));
body.put("solutionStartDate", toStringValue(solution.get("solutionStartDate")));
body.put("solutionEndDate", toStringValue(solution.get("solutionEndDate")));
putIfNotBlank(body, "solutionDesc", solution.get("solutionDesc"));
putIfNotBlank(body, "solutionFormulateDeptCode", solution.get("solutionFormulateDeptCode"));
putIfNotBlank(body, "solutionFormulateDeptName", solution.get("solutionFormulateDeptName"));
putIfNotBlank(body, "inspectSolutionDomain", solution.get("inspectSolutionDomain"));

List<Map<String, Object>> cooperateOrgList = ecologicalDataProcessMapper.selectCooperateOrgByFaid(faid);
if (CollectionUtils.isNotEmpty(cooperateOrgList)) {
body.put("solutionCooperateOrgList", cooperateOrgList);
}

List<Map<String, Object>> solutionItemList = ecologicalDataProcessMapper.selectSolutionItemByFaid(faid);
if (CollectionUtils.isNotEmpty(solutionItemList)) {
body.put("solutionItemList", solutionItemList);
}

List<Map<String, Object>> checkObjectList = ecologicalDataProcessMapper.selectCheckObjectByFaid(faid);
if (CollectionUtils.isNotEmpty(checkObjectList)) {
body.put("checkObjectList", checkObjectList);
}

List<Map<String, Object>> checkPersonList = ecologicalDataProcessMapper.selectCheckPersonByFaid(faid);
if (CollectionUtils.isNotEmpty(checkPersonList)) {
body.put("checkPersonList", checkPersonList);
}

return body;
}

安全性考虑

为了确保安全性,可以采取以下措施:

  • 使用HTTPS协议进行数据传输,以保护通信过程中的数据安全。
  • 在请求中使用AK和签名进行身份验证,并对请求进行验签,在服务端进行校验和鉴权,以防止非法请求和重放攻击。
  • 对敏感数据进行加密传输,例如使用TLS加密算法对敏感数据进行加密。

以上是一个简单的设计方案和API接口设计示例。具体的实现细节可能因项目需求而有所不同。在实际开发中,还要考虑错误处理、异常情况处理、日志记录等方面。

防止重放攻击和对敏感数据进行加密传输都是保护三方接口安全的重要措施。

防止重放攻击

抓取报文原封不动重复发送如果是付款接口,或者购买接口就会造成损失,因此需要采用防重放的机制来做请求验证,如请求参数上加上timestamp时间戳+nonce随机数

重放攻击是指黑客通过抓包的方式,得到客户端的请求数据及请求连接,重复的向服务器发送请求的行为。

时间戳(tamp) + 数字签名(sign), 也就是说每次发送请求时多传两个参数,分别为 tamp 和 sign。

数字签名的作用是为了确保请求的有效性。因为签名是经过加密的,只有客户端和服务器知道加密方式及密钥(key),所以第三方模拟不了。我们通过对sign的验证来判断请求的有效性,如果sign验证失败则判定为无效的请求,反之有效。 但是数字签名并不能阻止重放攻击,因为黑客可以抓取你的tamp和sign(不需做任何修改),然后发送请求。这个时候就要对时间戳进行验证。

时间戳的作用是为了确保请求的时效性。我们将上一次请求的时间戳进行存储,在下一次请求时,将两次时间戳进行比对。如果此次请求的时间戳和上次的相同或小于上一次的时间戳,则判定此请求为过时请求,无效。因为正常情况下,第二次请求的时间肯定是比上一次的时间大的,不可能相等或小于。

如果修改了时间戳来满足时间的时效性,sign验签就不通过了。

注:如果客户端是js,一定要对js做代码混淆,禁止右键等。

1. 使用Nonce和Timestamp

在请求中添加唯一的Nonce(随机数)和Timestamp(时间戳),并将其包含在签名计算中。服务端在验证签名时,可以检查Nonce和Timestamp的有效性,并确保请求没有被重放。

防止重放攻击是在三方接口中非常重要的安全措施之一。使用Nonce(一次性随机数)和Timestamp(时间戳)结合起来,可以有效地防止重放攻击。下面是实现此功能的最佳实践:

  1. 生成Nonce和Timestamp:
    • Nonce应该是一个随机的、唯一的字符串,可以使用UUID或其他随机字符串生成算法来创建。
    • Timestamp表示请求的时间戳,通常使用标准的Unix时间戳格式(以秒为单位)。
  2. 在每个请求中包含Nonce和Timestamp:
    • 将生成的Nonce和Timestamp作为参数添加到每个请求中,可以通过URL参数、请求头或请求体的方式进行传递。
    • 确保Nonce和Timestamp在每个请求中都是唯一且正确的。
  3. 服务器端验证Nonce和Timestamp:
    • 在服务器端接收到请求后,首先验证Nonce和Timestamp的有效性。
    • 检查Nonce是否已经被使用过,如果已经被使用过,则可能是重放攻击,拒绝该请求。
    • 检查Timestamp是否在合理的时间范围内,如果超出预定的有效期,则认为请求无效。
  4. 存储和管理Nonce:
    • 为了验证Nonce是否已经被使用过,服务器需要存储已经使用过的Nonce。
    • 可以使用数据库、缓存或其他持久化存储方式来管理Nonce的状态。
    • 需要定期清理过期的Nonce,以防止存储占用过多的资源。
  5. 设置有效期:
    • 为了限制请求的有效时间范围,可以设置一个合理的有效期。
    • 根据实际需求和业务场景,选择适当的有效期,例如几分钟或几小时。

通过使用Nonce和Timestamp来防止重放攻击,可以保护三方接口免受恶意重放请求的影响。以上是实现该功能的最佳实践,但具体的实现方法可能因应用程序和技术栈的不同而有所差异。确保在设计和实施安全措施时考虑到应用程序的特定需求和风险模型。

2. 添加过期时间

在请求中添加一个过期时间字段(例如,token的有效期),并在服务端验证请求的时间戳是否在有效期内。超过过期时间的请求应被拒绝。

防篡改、防重放攻击拦截器

每次HTTP请求,都需要加上timestamp参数,然后把timestamp和其他参数一起进行数字签名。HTTP请求从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间相比较,是否超过了60s,如果超过了则认为是非法的请求。 一般情况下,从抓包重放请求耗时远远超过了60s,所以此时请求中的timestamp参数已经失效了,如果修改timestamp参数为当前的时间戳,则signature参数对应的数字签名就会失效,因为不知道签名秘钥,没有办法生成新的数字签名。 但这种方式的漏洞也是显而易见的,如果在60s之后进行重放攻击,那就没办法了,所以这种方式不能保证请求仅一次有效 nonce的作用

nonce的意思是仅一次有效的随机字符串,要求每次请求时,该参数要保证不同。我们将每次请求的nonce参数存储到一个“集合”中,每次处理HTTP请求时,首先判断该请求的nonce参数是否在该“集合”中,如果存在则认为是非法请求。 nonce参数在首次请求时,已经被存储到了服务器上的“集合”中,再次发送请求会被识别并拒绝。 nonce参数作为数字签名的一部分,是无法篡改的,因为不知道签名秘钥,没有办法生成新的数字签名。 这种方式也有很大的问题,那就是存储nonce参数的“集合”会越来越大。 nonce的一次性可以解决timestamp参数60s(防止重放攻击)的问题,timestamp可以解决nonce参数“集合”越来越大的问题。

对敏感数据进行加密传输

使用TLS(传输层安全)协议可以保证通信过程中的数据加密和完整性。以下是一些基本步骤:

  1. 在服务器上配置TLS证书(包括公钥和私钥)。
  2. 客户端和服务器之间建立TLS连接。客户端向服务器发送HTTPS请求。
  3. 在TLS握手期间,客户端和服务器协商加密算法和密钥交换方法。
  4. 握手成功后,客户端和服务器之间的所有数据传输都会经过加密处理。

AK和SK生成方案

开发一个三方接口,并提供给客户使用,可以考虑以下方法来生成AK(Access Key)和SK(Secret Key):

  1. 设计API密钥管理系统:创建一个API密钥管理系统,用于生成和管理AK和SK。这个系统可以是一个独立的服务器应用或与你的主应用集成在一起。
  2. 生成AK和SK:
    • 在API密钥管理系统中,为每个客户生成唯一的AK和SK。
    • AK通常是一个公开的标识符,用于标识客户的身份。可以使用随机字符串、UUID等方式生成。
    • SK是一个保密的私钥,用于生成身份验证签名和加密访问令牌。可以使用随机字符串、哈希函数等方式生成,并确保其足够安全。
  3. 存储和管理AK和SK:
    • 将生成的AK和SK存储在数据库或其他持久化存储中,并与客户的其他相关信息关联起来。
    • 需要实施适当的权限控制和安全措施,以确保只有授权的用户可以访问和管理AK和SK。
    • 可以考虑对SK进行加密处理,以增加安全性。
  4. 提供API密钥分发机制:
    • 客户可以通过你提供的界面、API或者自助注册流程来获取他们的AK和SK。
    • 在分发过程中,确保以安全的方式将AK和SK传递给客户。例如,使用加密连接或其他安全通道进行传输。
  5. 安全性和最佳实践:
    • 强烈建议对API密钥管理系统进行安全审计,并根据最佳实践来保护和管理AK和SK。
    • 定期轮换AK和SK,以增加安全性并降低潜在风险。
    • 在设计接口时,使用AK和SK进行身份验证和权限控制,以防止未经授权的访问。

作者:jimmyhus
链接:https://juejin.cn/post/7268667384440504360
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。