OpenApi-Design
在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题 在设计三方接口调用的方案时,需要考虑到安全性和可用性。以下是一种设计方案的概述,其中包括使用API密钥(Access Key/Secret Key)进行身份验证和设置回调地址。
设计方案概述
-
API密钥生成:为每个三方应用生成唯一的API密钥对(AK/SK),其中AK用于标识应用,SK用于进行签名和加密。
AK:Access Key Id,用于标示用户。 SK:Secret Access Key,是用户用于加密认证字符串和用来验证认证字符串的密钥,其中SK必须保密。 通过使用Access Key Id / Secret Access Key加密的方法来验证某个请求的发送者身份。
-
接口鉴权:在进行接口调用时,客户端需要使用AK和请求参数生成签名,并将其放入请求头或参数中以进行身份验证。
淘宝签名和验签:淘宝SDK签名算法 (yuque.com)
-
回调地址设置:三方应用提供回调地址,用于接收异步通知和回调结果。
-
接口API设计:设计接口的URL、HTTP方法、请求参数、响应格式等细节。
权限划分
-
appID:应用的唯一标识
用来标识你的开发者账号的, 即:
用户id, 可以在数据库添加索引,方便快速查找,同一个 appId 可以对应多个appKey + appSecret,达到权限的 -
appKey:公匙(相当于账号)
公开的,调用服务所需要的密钥。是用户的身份认证 标识,用于调用平台可用服务.,可以简单理解成是账号。
-
appSecret:私匙(相当于密码)
签名的密钥,是跟appKey配套使用的,可以简单理解成是密码。
-
token:令牌(过期失效)
使用方法
- 向第三方服务器请求授权时,带上
AppKey和AppSecret(需存在服务器端) - 第三方服务器验证
appKey和appSecret在数据库、缓存中有没有记录 - 如果有,生成一串唯一的字符串(
token令牌),返回给服务器,服务器再返回给客户端 - 后续客户端每次请求都需要带上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 也需要添加数据库索引, 方便快速查找
- 可以生成两对appKey和appSecret。一个用于删除,一个用于读写,达到权限的细粒度划分。如 : appKey1 + appSecect1 只有删除权限 但是 appKey2+appSecret2 有读写权限… 这样你就可以把对应的权限 放给不同的开发者。其中
简化的场景:
- 第一种场景:通常用于开放性接口,像地图api,
会省去app_id和app_key,此时相当于三者相等,合而为一appId = appKey = appSecret, 。这种模式下,带上app_id的目的仅仅是统计某一个用户调用接口的次数而已了。 - 第二种场景: 当每一个用户有且仅有一套权限配置 可以
去掉 appKey, , 直接将app_id = app_key, 每个用户分配一个appId+ appSecret就够了`.
也可以
可以采用签名(signature)的方式: 当调用方向服务提供方法发起请求时,带上(
appKey、时间戳timeStamp、随机数nonce、签名sign) 签名sign 可以使用 (AppSecret + 时间戳 + 随机数)使用sha1、md5生成,服务提供方收到后,生成本地签名和收到的签名比对,如果一致,校验成功
签名流程

签名规则
-
分配
appId(开发者标识)和appSecret(密钥),给 不同的调用方可以直接通过平台线上申请,也可以线下直接颁发。appId是全局唯一的,每个appId将对应一个客户,密钥appSecret需要高度保密。
-
加入
timeStamp(时间戳),以服务端当前时间为准,单位为ms ,5分钟内数据有效时间戳的目的就是为了减轻DOS攻击。防止请求被拦截后一直尝试请求接口。服务器端设置时间戳阀值,如果
服务器时间 减 请求时间戳超过阀值,表示签名超时,接口调用失败。 -
加入
临时流水号nonce,至少为10位 ,有效期内防重复提交。随机值nonce 主要是为了
增加签名sign的多变性,也可以保护接口的幂等性,相邻的两次请求nonce不允许重复,如果重复则认为是重复提交,接口调用失败。- 针对查询接口,流水号只用于日志落地,便于后期日志核查。
- 针对办理类接口需校验流水号在有效期内的唯 一性,以避免重复请求。
通过在接口签名请求参数加上
时间戳timeStamp+随机数nonce可以防止 ”重放攻击“-
时间戳(timeStamp): 以服务端当前时间为准,服务端要求客户端发过来的时间戳,必须是最近60秒内(假设值,自己定义)的。 这样,即使这个请求即使被截取了,也只能在60s内进行重放攻击。
-
随机数(nonce): 但是,即使设置了时间戳,攻击者还有60s的攻击时间呢! 所以我们需要在客户端请求中再加上一个随机数(中间黑客不可能自己修改随机数,因为有参数签名的校验呢), 服务端会对一分钟内请求的随机数进行检查,如果有两个相同的,基本可以判定为重放攻击。 因为正常情况下,在短时间内(比如60s)连续生成两个相同nonce的情况几乎为0
服务端“第一次”在接收到这个nonce的时候做下面行为:
- 去redis中查找是否有key为
nonce: {nonce}的数据 - 如果没有,则创建这个key,把这个key失效的时间和验证timestamp失效的时间一致,比如是60s。
- 如果有,说明这个key在60s内已经被使用了,那么这个请求就可以判断为重放请求。
-
加入签名字段
sign,获取调用方传递的签名信息。通过在接口签名请求参数加上 时间戳appId + sign 解决身份验证和防止 ”参数篡改“
- 请求携带参数appId和Sign,只有拥有合法的身份appId和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题。
- 即使请求参数被劫持,由于获取不到appSecret(仅作本地加密使用,不参与网络传输),也无法伪造合法的请求。
以上字段放在请求头中。
API接口设计
根据你的具体需求和业务场景,以下是一个简单示例的API接口设计:
1. 获取资源列表接口
- URL:
/api/resources - HTTP 方法: GET
- 请求参数:
- page (可选): 页码
- limit (可选): 每页限制数量
- 响应:
- 成功状态码: 200 OK
- 响应体: 返回资源列表的JSON数组
2. 创建资源接口
- URL:
/api/resources - HTTP 方法: POST
- 请求参数:
- name (必填): 资源名称
- description (可选): 资源描述
- 响应:
- 成功状态码: 201 Created
- 响应体: 返回新创建资源的ID等信息
3. 更新资源接口
- URL:
/api/resources/{resourceId} - HTTP 方法: PUT
- 请求参数:
- resourceId (路径参数, 必填): 资源ID
- name (可选): 更新后的资源名称
- description (可选): 更新后的资源描述
- 响应:
- 成功状态码: 200 OK
4. 删除资源接口
- URL:
/api/resources/{resourceId} - HTTP 方法: DELETE
- 请求参数:
- resourceId (路径参数, 必填): 资源ID
- 响应:
- 成功状态码: 204 No Content
安全性考虑
为了确保安全性,可以采取以下措施:
- 使用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(时间戳)结合起来,可以有效地防止重放攻击。下面是实现此功能的最佳实践:
-
生成Nonce和Timestamp:
- Nonce应该是一个随机的、唯一的字符串,可以使用UUID或其他随机字符串生成算法来创建。
- Timestamp表示请求的时间戳,通常使用标准的Unix时间戳格式(以秒为单位)。
-
在每个请求中包含Nonce和Timestamp:
- 将生成的Nonce和Timestamp作为参数添加到每个请求中,可以通过URL参数、请求头或请求体的方式进行传递。
- 确保Nonce和Timestamp在每个请求中都是唯一且正确的。
-
服务器端验证Nonce和Timestamp:
- 在服务器端接收到请求后,首先验证Nonce和Timestamp的有效性。
- 检查Nonce是否已经被使用过,如果已经被使用过,则可能是重放攻击,拒绝该请求。
- 检查Timestamp是否在合理的时间范围内,如果超出预定的有效期,则认为请求无效。
-
存储和管理Nonce:
- 为了验证Nonce是否已经被使用过,服务器需要存储已经使用过的Nonce。
- 可以使用数据库、缓存或其他持久化存储方式来管理Nonce的状态。
- 需要定期清理过期的Nonce,以防止存储占用过多的资源。
-
设置有效期:
- 为了限制请求的有效时间范围,可以设置一个合理的有效期。
- 根据实际需求和业务场景,选择适当的有效期,例如几分钟或几小时。
通过使用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参数“集合”越来越大的问题。
public class SignAuthInterceptor implements HandlerInterceptor {
private RedisTemplate<String, String> redisTemplate;
private String key;
public SignAuthInterceptor(RedisTemplate<String, String> redisTemplate, String key) {
this.redisTemplate = redisTemplate;
this.key = key;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// 获取时间戳
String timestamp = request.getHeader("timestamp");
// 获取随机字符串
String nonceStr = request.getHeader("nonceStr");
// 获取签名
String signature = request.getHeader("signature");
// 判断时间是否大于xx秒(防止重放攻击)
long NONCE_STR_TIMEOUT_SECONDS = 60L;
if (StrUtil.isEmpty(timestamp) || DateUtil.between(DateUtil.date(Long.parseLong(timestamp) * 1000), DateUtil.date(), DateUnit.SECOND) > NONCE_STR_TIMEOUT_SECONDS) {
throw new BusinessException("invalid timestamp");
}
// 判断该用户的nonceStr参数是否已经在redis中(防止短时间内的重放攻击)
Boolean haveNonceStr = redisTemplate.hasKey(nonceStr);
if (StrUtil.isEmpty(nonceStr) || Objects.isNull(haveNonceStr) || haveNonceStr) {
throw new BusinessException("invalid nonceStr");
}
// 对请求头参数进行签名
if (StrUtil.isEmpty(signature) || !Objects.equals(signature, this.signature(timestamp, nonceStr, request))) {
throw new BusinessException("invalid signature");
}
// 将本次用户请求的nonceStr参数存到redis中设置xx秒后自动删除
redisTemplate.opsForValue().set(nonceStr, nonceStr, NONCE_STR_TIMEOUT_SECONDS, TimeUnit.SECONDS);
return true;
}
private String signature(String timestamp, String nonceStr, HttpServletRequest request) throws UnsupportedEncodingException {
Map<String, Object> params = new HashMap<>(16);
Enumeration<String> enumeration = request.getParameterNames();
if (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String value = request.getParameter(name);
params.put(name, URLEncoder.encode(value, CommonConstants.UTF_8));
}
String qs = String.format("%s×tamp=%s&nonceStr=%s&key=%s", this.sortQueryParamString(params), timestamp, nonceStr, key);
log.info("qs:{}", qs);
String sign = SecureUtil.md5(qs).toLowerCase();
log.info("sign:{}", sign);
return sign;
}
/**
* 按照字母顺序进行升序排序
*
* @param params 请求参数 。注意请求参数中不能包含key
* @return 排序后结果
*/
private String sortQueryParamString(Map<String, Object> params) {
List<String> listKeys = Lists.newArrayList(params.keySet());
Collections.sort(listKeys);
StrBuilder content = StrBuilder.create();
for (String param : listKeys) {
content.append(param).append("=").append(params.get(param).toString()).append("&");
}
if (content.length() > 0) {
return content.subString(0, content.length() - 1);
}
return content.toString();
}
}
注册拦截器指定拦截接口
对敏感数据进行加密传输
Java如何事项api接口数据加密传输交互使用TLS(传输层安全)协议可以保证通信过程中的数据加密和完整性。以下是一些基本步骤:
- 在服务器上配置TLS证书(包括公钥和私钥)。
- 客户端和服务器之间建立TLS连接。客户端向服务器发送HTTPS请求。
- 在TLS握手期间,客户端和服务器协商加密算法和密钥交换方法。
- 握手成功后,客户端和服务器之间的所有数据传输都会经过加密处理。
具体的实现取决于所使用的编程语言和框架。以下是使用Java的示例代码,演示如何使用TLS进行加密传输:
// 创建SSLContext对象
SSLContext sslContext = SSLContext.getInstance("TLS");
// 初始化SSLContext,加载证书和私钥
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(new FileInputStream("keystore.jks"), "password".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "password".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
// 创建HttpsURLConnection连接
URL url = new URL("https://api.example.com/endpoint");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());
// 设置其他请求参数、发送请求、处理响应等
这段代码中,我们创建了一个SSLContext对象并初始化它,加载了服务器的证书和私钥。然后,通过HttpsURLConnection对象,设置了TLS的安全套接字工厂,并与指定的URL建立了HTTPS连接。
请注意,你需要将实际的证书和私钥文件(通常是.jks格式)替换为真实的文件路径,并提供正确的密码。
以上代码只是一个简单的示例,实际部署时可能需要根据具体要求进行更多配置。确保在项目中遵循最佳实践和安全建议,并与相应的开发和运维团队合作,以确保三方接口的安全性。
AK和SK生成方案
参考各大云服务厂商如何获取aksk: 华为云创建访问密钥(AK和SK),使用说明
开发一个三方接口,并提供给客户使用,可以考虑以下方法来生成AK(Access Key)和SK(Secret Key):
-
设计API密钥管理系统:创建一个API密钥管理系统,用于生成和管理AK和SK。这个系统可以是一个独立的服务器应用或与你的主应用集成在一起。
-
生成AK和SK:
- 在API密钥管理系统中,为每个客户生成唯一的AK和SK。
- AK通常是一个公开的标识符,用于标识客户的身份。可以使用随机字符串、UUID等方式生成。
- SK是一个保密的私钥,用于生成身份验证签名和加密访问令牌。可以使用随机字符串、哈希函数等方式生成,并确保其足够安全。
-
存储和管理AK和SK:
- 将生成的AK和SK存储在数据库或其他持久化存储中,并与客户的其他相关信息关联起来。
- 需要实施适当的权限控制和安全措施,以确保只有授权的用户可以访问和管理AK和SK。
- 可以考虑对SK进行加密处理,以增加安全性。
-
提供API密钥分发机制:
- 客户可以通过你提供的界面、API或者自助注册流程来获取他们的AK和SK。
- 在分发过程中,确保以安全的方式将AK和SK传递给客户。例如,使用加密连接或其他安全通道进行传输。
-
安全性和最佳实践:
- 强烈建议对API密钥管理系统进行安全审计,并根据最佳实践来保护和管理AK和SK。
- 定期轮换AK和SK ,以增加安全性并降低潜在风险。
- 在设计接口时,使用AK和SK进行身份验证和权限控制,以防止未经授权的访问。
请注意,上述步骤提供了一般性的指导,具体实现可能因你的应用程序需求、技术栈和安全策略而有所不同。确保遵循安全最佳实践,并参考相关的安全文档和建议,以确保生成的AK和SK的安全性和可靠性。
CREATE TABLE api_credentials (
id INT AUTO_INCREMENT PRIMARY KEY,
app_id VARCHAR(255) NOT NULL,
access_key VARCHAR(255) NOT NULL,
secret_key VARCHAR(255) NOT NULL,
valid_from DATETIME NOT NULL,
valid_to DATETIME NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
allowed_endpoints VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
这个表包含以下字段:
| 列名 | 描述 |
|---|---|
| id | 主键,自增的唯一标识符。 |
| app_id | 应用程序ID或标识符,用于关联AKSK与特定应用程序。 |
| access_key | 访问密钥(AK),用于标识客户身份。 |
| secret_key | 秘密密钥(SK),用于生成签名和进行身份验证。 |
| valid_from | AKSK有效期起始时间。 |
| valid_to | AKSK有效期结束时间。 |
| enabled | 是否启用该AKSK,1表示启用,0表示禁用。 |
| allowed_endpoints | 逗号分隔的允许访问的接口/端点列表。 |
| created_at | 记录创建时间。 |
在实际使用中,你可能需要根据具体需求对字段进行调整或添加索引以提高性能。此外,还可以考虑添加其他字段来满足你的应用程序的需求,例如描述、所属用户等。
请注意,具体的设计可能会因你的应用程序需求和使用 场景而有所不同。确保在实施前仔细考虑你的业务要求,并遵循良好的数据库设计原则和最佳实践。
API接口设计补充

1. 使用POST作为接口请求方式
一般调用接口最常用的两种方式就是GET和POST。两者的区别也很明显,GET请求会将参数暴露在浏览器URL中,而且对长度也有限制。为了更高的安全性,所有接口都采用POST方式请求。
2. 客户端IP白名单
ip白名单是指将接口的访问权限对部分ip进行开放来避免其他ip进行访问攻击。
- 设置ip白名单缺点就是当你的客户端进行迁移后,就需要重新联系服务提供者添加新的ip白名单。
- 设置ip白名单的方式很多,除了传统的防火墙之外,spring cloud alibaba提供的组件sentinel也支持白名单设置。
- 为了降低api的复杂度,推荐使用防火墙规则进行白名单设置。