深色模式
接口安全设计
概述
后端接口里最容易被混在一起的三个词,是防篡改、防重放和内容加密。它们经常同时出现,但解决的是三类不同问题:内容有没有被改,请求是不是旧包重发,数据在链路上会不会被旁观者看见。
接口设计如果把这三件事都归到“加密”名下,方案通常会开始跑偏:该做签名的地方去做散列,该做幂等的地方只上 TLS,该走浏览器接口的地方又硬塞共享密钥。把原理和适用场景分开看,落地反而更简单。
三类风险分别在防什么
| 能力 | 主要目标 | 常见手段 | 典型接口 |
|---|---|---|---|
| 防篡改 | 确认请求内容未被修改 | 请求签名、消息认证码、数字签名 | 开放平台、Webhook、服务间调用 |
| 防重放 | 防止同一合法请求被重复利用 | timestamp、nonce、幂等键 | 支付、下单、登录、回调 |
| 内容加密 | 防止明文被窃听 | HTTPS、报文加密、字段加密 | 所有外网接口,高敏感数据接口 |
这三种能力有交集,但不能相互替代。请求签名能发现内容被改,不能天然阻止别人把整包旧请求重新发一遍;HTTPS 能保护传输过程里的明文,不能替业务层做幂等。
防篡改:让请求内容可验证
防篡改最常见的做法,是给请求做签名。思路不复杂:客户端先把参与校验的字段按固定规则拼成一个待签名字符串,再用共享密钥或私钥生成签名;服务端按同样规则重新计算一次,结果一致才放行。
一个常见的签名串会包含这些内容:
text
HTTP_METHOD + "\n" +
PATH + "\n" +
QUERY_STRING + "\n" +
BODY_SHA256 + "\n" +
TIMESTAMP + "\n" +
NONCE实现上常见两类方案:
HMAC-SHA256:双方共享同一个密钥,计算快,适合服务端对服务端、商户对平台这类场景。- 非对称签名:客户端私钥签名,服务端公钥验签,适合密钥分发更复杂、签名方较多的体系。
在 Go 里,前者通常会用到 crypto/hmac 和 crypto/sha256,后者会用到 crypto/rsa、crypto/ecdsa 或 crypto/ed25519。
go
func sign(message string, secret []byte) string {
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(message))
return hex.EncodeToString(mac.Sum(nil))
}签名适合用在“调用方能安全持有密钥”的接口上:
- 服务端对服务端接口
- 对外开放平台接口
- 支付、物流、消息通知这类
Webhook回调 - 设备、IoT、桌面客户端接口
浏览器前端直连接口通常不适合把共享密钥放在页面代码里。前端代码和请求过程都暴露在用户环境中,密钥很难保住,所以浏览器接口更常见的主线还是 HTTPS、登录态、CSRF 防护和风控。
防重放:让旧请求失效
重放攻击的麻烦在于:攻击者不需要改内容,只要拿到一整包合法请求,再原样发一次,签名照样可能通过。业务上有副作用的接口,尤其怕这种打法。
防重放通常靠三样东西一起配合:
timestamp:限制请求必须落在一个很短的时间窗口里,例如 1 到 5 分钟。nonce:每个请求携带一次性随机串,服务端只接受第一次出现的值。idempotencyKey或业务单号:对支付、下单、提现这类关键接口再做一次业务幂等。
服务端一般会把 appId + nonce 存到 Redis 一类带过期时间的存储里。第一次写入成功就放行,第二次再看到同一个键,直接判成重放。
go
ok, err := rdb.SetNX(ctx, "nonce:"+appID+":"+nonce, 1, 5*time.Minute).Result()
if err != nil {
return err
}
if !ok {
return errors.New("replayed request")
}这类保护最适合放在会改数据、发消息、扣库存、扣余额、发验证码的接口上:
- 支付、退款、提现
- 下单、取消订单、核销
- 登录、短信验证码、重置密码
- Webhook 回调消费
- 设备控制指令
普通查询类 GET 接口通常不需要为每次请求都做 nonce 去重。它们更关注鉴权、限流和缓存,一股脑上全套防重放,只会让接入方和服务端一起头大。
内容加密:让明文不暴露在链路上
内容加密分两层看。
第一层是传输层加密,也就是 HTTPS。这是所有外网接口的基础配置,不是可选项。它解决的是链路上的机密性、服务端身份验证,以及大部分中间人窃听问题。没有 HTTPS,后面很多安全设计都像在漏风的房子里装防盗门。
第二层是应用层报文加密。它不是替代 HTTPS,而是继续缩小明文暴露范围。常见做法是:
- 客户端随机生成一个对称密钥。
- 用
AES-GCM加密请求体。 - 用服务端公钥加密这个对称密钥。
- 服务端先解开对称密钥,再解密请求体。
这里对称加密负责真正处理大块数据,非对称加密负责安全传递会话密钥。Go 标准库里常用的入口分别是 crypto/aes、crypto/cipher 和 crypto/rsa。
报文加密通常留给这些场景:
- 银行卡号、身份证号、医疗信息这类高敏感数据
- 经过网关、代理、日志链路时,不希望中间节点看到明文
- 金融、政企、强合规接口
- 双方协议明确要求端到端保护的系统对接
普通业务接口只要已经正确使用 HTTPS,大多数时候没必要再给整个请求体额外包一层加密。否则复杂度会上去,排错体验会迅速变差,日志、灰度和联调都跟着受影响。
不同接口场景怎么组合
| 接口场景 | 防篡改 | 防重放 | 内容加密 |
|---|---|---|---|
| 普通查询接口 | 通常不必强制 | 通常不必强制 | HTTPS 必须 |
| 浏览器前端接口 | 不把前端签名当核心手段 | 关键操作可做幂等和风控 | HTTPS 必须 |
| 登录、验证码接口 | 可选 | 建议开启 | HTTPS 必须 |
| 下单、支付、提现接口 | 建议开启 | 必须开启 | HTTPS 必须,高敏感字段可加密 |
| 开放平台、商户接口 | 通常必须 | 通常必须 | HTTPS 必须 |
| Webhook 回调接口 | 通常必须 | 通常必须 | HTTPS 必须 |
| 内部微服务接口 | 视信任边界决定 | 关键写接口建议开启 | 优先 TLS 或 mTLS |
可以把它理解成一个简单判断:
- 接口会改数据、扣钱、发消息,就优先考虑防重放。
- 调用方是服务端、商户系统、设备,而不是浏览器,就很适合加请求签名。
- 数据特别敏感,或者合规明确要求链路中间节点不可见,再考虑报文加密。
Go 服务里的落地顺序
在 Go 项目里,这三层能力通常不会写在业务方法里,而是按职责拆到网关、中间件和基础设施层。
- 先把
HTTPS配好,内部服务之间如果有零信任要求,再加mTLS。 - 对开放接口、回调接口和服务间调用,加一层签名校验中间件。
- 对关键写接口,加
timestamp、nonce和幂等键校验,nonce去重交给Redis之类的外部存储。 - 对极敏感字段,再加
AES-GCM这类报文或字段级加密。 - 密钥不要硬编码在仓库里,交给环境变量、密钥管理系统或部署平台下发。
实现时最容易踩的几个坑也很稳定:
- 把
SHA-256(body)当成签名。摘要不是签名,谁都能算。 - 只验签,不校验
timestamp和nonce。这样照样能被整包重放。 - 以为
HTTPS已经解决了业务幂等。它解决不了“合法请求重复消费”。 - 在浏览器里长期保存共享密钥,然后把“前端签名”当核心防线。
