一、为什么对外接口需要安全设计?

内部接口一般运行在内网环境中,调用方相对可控。但对外接口不一样,它通常具备这些特点:

  • 通过公网域名访问
  • 调用方可能是第三方系统
  • 请求链路更长,经过网关、负载均衡、反向代理等组件
  • 容易被抓包、重放、刷接口、伪造请求
  • 一旦接口被滥用,可能影响系统稳定性和数据安全

所以,一个比较完整的对外接口,至少要考虑下面几类安全措施。


二、常见安全措施

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

签名流程

  1. 客户端准备业务参数
  2. 加入 app_id、timestamp、nonce
  3. 按参数名排序
  4. 拼接成待签名字符串
  5. 使用 AppSecret 生成签名
  6. 服务端使用相同规则重新计算签名
  7. 对比客户端 sign 是否一致

示例待签名字符串

app_id=xxx&nonce=abc123&timestamp=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 Keyrate: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 中的业务参数一起参与计算。


四、服务端校验流程

服务端收到请求后,推荐按照下面顺序处理:

  1. 校验 HTTPS
  2. 获取 AppId
  3. 查询 AppId 是否存在
  4. 判断 AppId 状态
  5. 校验 IP 白名单
  6. 校验时间戳
  7. 校验 nonce 是否重复
  8. 重新计算签名
  9. 判断接口权限
  10. 执行限流策略
  11. 参数合法性校验
  12. 执行业务逻辑
  13. 返回统一响应

五、统一响应格式

建议对外接口使用统一响应格式:

{
    "code": "SUCCESS",
    "message": "success",
    "data": {}
}

错误示例

{
    "code": "INVALID_SIGNATURE",
    "message": "签名错误",
    "data": null
}

常见错误码

错误码说明
SUCCESS成功
INVALID_APP_IDAppId 不存在
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 才算具备比较完整的安全基础。