一、为什么对外接口需要安全设计?
内部接口一般运行在内网环境中,调用方相对可控。但对外接口不一样,它通常具备这些特点:
- 通过公网域名访问
- 调用方可能是第三方系统
- 请求链路更长,经过网关、负载均衡、反向代理等组件
- 容易被抓包、重放、刷接口、伪造请求
- 一旦接口被滥用,可能影响系统稳定性和数据安全
所以,一个比较完整的对外接口,至少要考虑下面几类安全措施。
二、常见安全措施
1. HTTPS 传输加密
对外接口必须使用 HTTPS。如果接口使用 HTTP 明文传输,请求参数、响应内容都可能被中间人抓取。HTTPS 在 HTTP 和 TCP 之间增加了 TLS 加密层,可以保证数据在公网传输过程中的机密性和完整性。
生产环境建议:
- 强制 HTTPS
- HTTP 自动跳转 HTTPS
- 使用有效证书
- 禁用过旧 TLS 版本
- 证书到期前自动续期
- 网关、Nginx、负载均衡统一处理 HTTPS
HTTPS 解决的是”公网传输链路安全”,但它并不能完全替代接口签名。
2. AppId / AppSecret 身份认证
对外接口不应该是谁都能调用。常见做法是给每个接入方分配一组身份凭证:
AppId:调用方身份标识
AppSecret:调用方密钥
调用方每次请求接口时,需要携带 AppId。服务端根据 AppId 找到对应的 AppSecret,再进行签名校验、权限判断和限流控制。
AppId / AppSecret 注意事项:
- AppId 可以公开传输
- AppSecret 不能出现在 URL、日志、前端代码中
- AppSecret 应支持定期轮换
- 不同接入方使用不同密钥
- 密钥泄露后可以单独禁用,不影响其他接入方
3. 请求签名
请求签名的目的:
- 请求确实来自合法调用方
- 请求参数没有被篡改
- 内部链路转发过程中数据没有被恶意修改
常见签名参数:app_id、timestamp、nonce、sign
签名流程:
- 客户端准备业务参数
- 加入 app_id、timestamp、nonce
- 按参数名排序
- 拼接成待签名字符串
- 使用 AppSecret 生成签名
- 服务端使用相同规则重新计算签名
- 对比客户端 sign 是否一致
示例待签名字符串:
app_id=xxx&nonce=abc123×tamp=1710000000000&user_id=10001&key=AppSecret
签名算法建议:
- 推荐:HMAC-SHA256
- 不推荐:单纯 MD5
- 不建议把 AppSecret 直接暴露在明文参数中
- 签名字段不要参与再次签名
伪代码:
String signContent = buildSignContent(params);
String serverSign = hmacSha256(signContent, appSecret);
if (!serverSign.equals(requestSign)) {
return error("INVALID_SIGNATURE", "签名错误");
}
4. 时间戳机制
即使请求被加密、签名,攻击者仍然可能抓到一整包请求,然后反复重放。所以请求中需要加入时间戳。
timestamp=1710000000000
服务端校验客户端时间与服务端时间差,例如只允许 5 分钟内的请求有效。
伪代码:
long interval = 5 * 60 * 1000;
long clientTime = request.getTimestamp();
long serverTime = System.currentTimeMillis();
if (Math.abs(serverTime - clientTime) > interval) {
return error("REQUEST_EXPIRED", "请求已过期");
}
注意:建议使用
Math.abs,避免客户端时间比服务端时间快时绕过校验。
5. Nonce 防重放
仅有时间戳还不够。如果攻击者在 5 分钟窗口内重复发送同一个请求,时间戳仍然是有效的。所以还需要增加 nonce 随机字符串。
nonce=abc123xyz
服务端可以把 app_id + nonce 存入 Redis,并设置短期过期时间,例如 5 分钟。
伪代码:
String key = "api:nonce:" + appId + ":" + nonce;
Boolean success = redis.setIfAbsent(key, "1", 5, TimeUnit.MINUTES);
if (!success) {
return error("REPLAY_REQUEST", "重复请求");
}
这样同一个 nonce 在有效时间内只能使用一次。
6. 接口权限控制
不是所有调用方都应该拥有所有接口权限。可以针对 AppId 配置接口权限:
AppId A:允许调用 /open/v1/device/report
AppId B:允许调用 /open/v1/user/query
AppId C:只允许调用只读接口
服务端在签名通过后,还需要判断该 AppId 是否有权限访问当前接口。
建议设计一张接入方权限表,维护:AppId、可访问接口、IP 白名单、调用频率、状态、生效时间、过期时间。
7. IP 白名单
如果第三方系统有固定出口 IP,可以配置 IP 白名单。这样即使 AppSecret 泄露,攻击者也无法从其他 IP 调用接口。
注意:
- IP 白名单不能替代签名
- 如果对方出口 IP 经常变化,维护成本会比较高
- 需要正确获取真实客户端 IP,避免被伪造
X-Forwarded-For
8. 限流机制
对外接口必须有限流。即使调用方是合法用户,也可能因为程序 Bug、重试风暴或恶意调用导致系统压力过大。
常见限流维度:按 AppId、IP、接口、用户维度、全局限流
常见算法:计数器限流、滑动窗口限流、令牌桶限流、漏桶限流
- 单机应用可以使用本地限流
- 分布式系统建议使用 Redis + Lua 实现统一限流
示例 Redis Key:rate:app:{app_id}:api:{api_path}
9. 黑名单机制
当某个 AppId 出现异常行为时,需要支持快速封禁。
常见状态设计:
| 状态 | 说明 |
|---|---|
| INIT | 初始化 |
| NORMAL | 正常 |
| DISABLED | 禁用 |
| BLACKLIST | 黑名单 |
| EXPIRED | 已过期 |
伪代码:
if (app.status == BLACKLIST || app.status == DISABLED) {
return error("APP_DISABLED", "应用不可用");
}
黑名单可以存储在数据库,也可以同步到 Redis 或配置中心,便于快速生效。
10. 参数合法性校验
接口安全不仅是认证和签名,参数校验也非常重要。
常见校验:必填校验、类型校验、长度校验、格式校验、枚举值校验、数值范围校验、JSON 结构校验、特殊字符校验、业务规则校验
示例:
if (StringUtils.isBlank(deviceId)) {
return error("INVALID_PARAM", "deviceId 不能为空");
}
if (amount < 0) {
return error("INVALID_PARAM", "数值不能小于 0");
}
对外接口的错误信息也要注意,不要暴露过多内部细节。
三、推荐请求格式
一个比较通用的对外 API 请求可以设计成这样:
POST /open/v1/device/report HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-App-Id: app_xxx
X-Timestamp: 1710000000000
X-Nonce: abc123xyz
X-Sign: xxxxxx
请求体:
{
"device_id": "D10001",
"event_type": "ONLINE",
"event_time": 1710000000000,
"extra_info": {
"version": "1.0.0"
}
}
签名时可以把 Header 中的认证字段和 Body 中的业务参数一起参与计算。
四、服务端校验流程
服务端收到请求后,推荐按照下面顺序处理:
- 校验 HTTPS
- 获取 AppId
- 查询 AppId 是否存在
- 判断 AppId 状态
- 校验 IP 白名单
- 校验时间戳
- 校验 nonce 是否重复
- 重新计算签名
- 判断接口权限
- 执行限流策略
- 参数合法性校验
- 执行业务逻辑
- 返回统一响应
五、统一响应格式
建议对外接口使用统一响应格式:
{
"code": "SUCCESS",
"message": "success",
"data": {}
}
错误示例:
{
"code": "INVALID_SIGNATURE",
"message": "签名错误",
"data": null
}
常见错误码:
| 错误码 | 说明 |
|---|---|
| SUCCESS | 成功 |
| INVALID_APP_ID | AppId 不存在 |
| APP_DISABLED | 应用不可用 |
| INVALID_SIGNATURE | 签名错误 |
| REQUEST_EXPIRED | 请求已过期 |
| REPLAY_REQUEST | 重复请求 |
| NO_PERMISSION | 无接口权限 |
| RATE_LIMITED | 请求过于频繁 |
| INVALID_PARAM | 参数错误 |
| INTERNAL_ERROR | 系统异常 |
六、生产环境建议
对外接口上线前,建议至少具备这些能力:
- 全站 HTTPS
- AppId / AppSecret 认证
- HMAC-SHA256 签名
- timestamp 防过期请求
- nonce 防重放
- AppId 维度限流
- IP 白名单
- 黑名单机制
- 接口权限控制
- 完整请求日志
- 敏感字段脱敏
- 统一错误码
- 监控告警
- 密钥轮换机制
七、总结
一个安全的对外 API,不能只依赖 HTTPS。
- HTTPS → 传输安全
- AppId / AppSecret → 调用方身份
- 签名 → 请求完整性
- timestamp + nonce → 重放攻击
- 限流和黑名单 → 接口滥用
- 参数校验 → 输入安全
对外接口的安全设计,本质上是在回答几个问题:
谁在调用?有没有权限?请求有没有被改过?请求是不是过期或重复?调用频率是否正常?参数是否合法?异常时能不能快速止损?
把这些问题都考虑清楚,一个对外 API 才算具备比较完整的安全基础。