· Jessy Huang · Engineering · 23 min read
接口明明带了 Token,为什么还是会被重放?一文讲透 HMAC、时间戳、Nonce 和 Redis TTL
有些安全问题,第一次听的时候很抽象。
比如这句:
“接口虽然做了 token 鉴权,但还是可能被重放攻击。”
很多人看到这里,第一反应都是:
“啊?都已经带 token 了,怎么还能有问题?”
这事不怪人困惑,因为在大多数业务开发语境里,token 几乎已经快等于“安全”了。前端带上 token,请求发到后端,后端验一下,能过就说明“你是你”。于是大家天然会觉得,这不就已经够了吗?
但现实往往比这个复杂一点。
如果把接口请求想象成“送快递”,那很多系统其实只做了这一步:
- 快递员出示了工牌,门卫确认“你是本公司的人”
但门卫并没有检查:
- 包裹有没有被拆过
- 包裹是不是昨天的旧件
- 这份包裹是不是刚刚已经送过一次了
而接口安全的很多问题,恰恰就出在后面这几项。
今天我们就用一篇尽量不枯燥的方式,把几个经常一起出现的概念讲清楚:
Token到底解决什么问题HMAC为什么能防篡改时间戳为什么还不够Nonce和Redis TTL为什么常常要一起上- 一套更稳妥的请求鉴权方案应该怎么搭
如果你最近在做开放平台、支付接口、回调验签,或者只是单纯想搞明白“为什么接口安全总是又签名又时间戳又 Redis”,这篇文章会比较适合你。
一、先说结论:只有 Token,通常不够
token 最擅长解决的问题,其实只有一个:
“你是谁?”
比如用户登录之后,服务端发给客户端一个 token,后续请求都带着它:
Authorization: Bearer xxxxx服务端收到之后,做一层校验:
- token 是否存在
- token 是否过期
- token 对应的是哪个用户
如果没问题,就放行。
这套机制非常常见,也非常有用。问题在于,它主要解决的是“身份识别”,不是“请求本身是否安全”。
换句话说,token 能告诉后端:
这请求大概率是张三发来的。
但它不一定能告诉后端:
这请求的内容没有被改过。
这请求不是一分钟前抓包抓来的旧请求。
这请求不是别人拿着同一份数据连续重放了三十次。
而很多线上事故,就发生在“我们以为 token 已经把安全问题都解决了”的那一刻。
二、一个很典型的误会:认证通过,不代表请求可信
想象一个接口:
POST /api/pay请求体长这样:
{
"userId": 1001,
"amount": 100
}客户端带着 token 发给服务端。服务端一看:
- token 合法
- 用户身份没问题
于是扣款 100 元。
现在问题来了。
如果攻击者拿到这份完整请求,他不一定需要“破解”什么东西,只要能把这份请求原样再发一次,就可能造成第二次扣款。
这就是重放攻击最朴素的样子:
我不伪造,我就重复使用一份合法请求。
而如果攻击者还能改参数,比如把 amount=100 改成 amount=1000,那又是另一类问题:
请求篡改。
所以你会发现,接口安全至少有三层不同的问题:
- 调用者是谁
- 请求有没有被改
- 请求是不是旧的、重复的
token 主要解决第 1 个问题。
后面两个,通常要靠别的机制。
三、Token、HMAC、时间戳、Nonce、Redis TTL,分别像什么?
如果只背名词,这几个东西很容易越学越绕。
换成一个门禁场景,会直观很多。
1. Token:门禁卡
你来到公司大门口,先刷一下卡。
门卫看见系统显示:
- 工号 10086
- 市场部
- 卡还没过期
于是确认:你是公司的人。
这就是 token 的核心作用:
证明调用方身份。
但是,门禁卡只能证明“你是谁”,不能证明“你手里这份文件没被掉包”,也不能证明“你今天没拿昨天那张作废单子来混进去”。
2. HMAC:文件上的防拆封条
现在你除了刷卡,还带了一份文件。
门卫发现,这份文件外面贴了一个特殊封条。这个封条不是随便谁都能做,只有公司和你知道制作方法。
如果文件内容被改过,封条就对不上。
这就是 HMAC 的味道。
它的核心作用不是“证明你是谁”,而是:
证明请求内容没有被篡改,而且这份签名是知道密钥的人算出来的。
说白一点:
- token 像“工牌”
- HMAC 像“封条”
工牌管身份,封条管内容完整性。
3. 时间戳:保质期
如果一份文件上写着:
仅限 5 分钟内有效
那即使这份文件是真的、封条也是真的,过了时效也不能认。
这就是 timestamp 的作用:
给请求加一个有效时间窗口。
它能解决的问题是:
- 把一份很久以前抓到的请求,今天再拿来用,不行
但它还不能完全解决:
- 在这 5 分钟之内,我把同一份请求重复发十次
所以只有时间戳,还是不够。
4. Nonce:一次性取号单
去银行办事的时候,你拿到一个排队号。
这个号今天这一次有效,而且理论上只该用一次。
如果有人拿着同一个号反复插队,工作人员就会觉得不对劲。
这就是 nonce 的定位:
给每次请求一个唯一编号。
这个编号往往是随机字符串,比如:
8f6c2d1a9b3e4f77只要每次请求都不同,服务端就有机会判断:
这个请求是不是“第一次出现”。
5. Redis TTL:门卫的临时登记本
问题来了。
就算请求里带了 nonce,如果服务端不记下来,它也没法知道这个 nonce 之前有没有出现过。
所以还需要一个“登记本”。
这个登记本通常就是 Redis。
服务端每收到一个通过校验的请求,就记一笔:
token + nonce- 或者
appId + nonce - 设置一个过期时间,比如 5 分钟
如果 5 分钟内又看到同一个 nonce,就说明这不是第一次来了。
这就是 Redis TTL 在防重放里的角色:
不是用来验身份,而是用来短期去重。
它像门卫桌上那本临时登记簿:
- 刚登记过的人,短时间内不能拿同一张票再进一次
- 过了时限,记录自动失效,不用永久保存
四、为什么 HMAC 能防篡改,但单独还防不了重放?
这是一个很容易混淆的点。
很多同学学到 HMAC 时,会觉得:
“我都签名了,请求还不安全吗?”
HMAC 的确很有用,但它解决的是:
请求内容有没有被改。
比如原始请求是:
{
"orderId": "A1001",
"amount": 100
}客户端按约定规则把请求内容做成签名串,再用密钥算出签名。
服务端收到后,用同样规则重算一遍,如果一致,就说明:
- 内容没被改
- 这份签名大概率来自知道密钥的人
但注意一个关键事实:
“没被改过”不等于“没被重复使用过”。
攻击者完全可以把一份“合法且没被改过”的请求,原封不动重放一遍。
这时候:
- HMAC 还是对的
- token 还是对的
如果没有时间戳、nonce、Redis 去重,这份请求依然可能被执行第二次。
所以:
HMAC 防篡改很强,但防重放通常要靠“时间戳 + nonce + 服务端存储”一起配合。
五、时间戳为什么也不够?
有人会说:
“那我加时间戳不就完了吗?只允许 5 分钟内有效。”
这比没有强很多,但依然不彻底。
原因很简单。
假设攻击者在第 1 分钟抓到一份合法请求,而你的时间窗口是 5 分钟。
那接下来的 4 分钟里,他仍然可以把这份请求反复发出去。
从服务端视角看:
- 请求没过期
- 签名没问题
- token 没问题
于是服务端会觉得一切正常。
这就像电影院检票时只看“今天的票”,但不撕票根。
你这张票今天确实有效,可如果没人记录“已经进过场了”,那你理论上可以反复拿它进去。
所以时间戳更像是:
缩短攻击窗口
而不是:
彻底解决重放问题
真正要把重放挡住,还是得靠唯一标识 + 服务端判重。
六、为什么很多方案最后都长成了“Token + HMAC + 时间戳 + Nonce + Redis TTL”?
因为这几样东西刚好各管一摊,拼起来很完整。
你可以把它理解成一套多层门禁:
Token:确认是谁在调用HMAC:确认请求内容没被改时间戳:确认这不是很久之前的旧请求Nonce:确认每次请求都有唯一编号Redis TTL:确认这个唯一编号没被用过
这套组合不算最重,但很平衡。
它不像双向证书那样接入复杂,也不像“只校验 token”那样防护太薄,所以在很多工程场景里都非常实用。
尤其是这些地方:
- 开放平台接口
- 支付、扣款、退款相关接口
- 第三方回调验签
- 内部高敏感服务调用
七、一个请求在这套方案里,到底经历了什么?
我们把过程拆开来看,就不神秘了。
客户端这边做什么?
客户端发请求前,通常会准备这些东西:
token:标识调用方timestamp:当前时间nonce:一串随机数- 请求参数本身
- 用密钥对这些内容做 HMAC 签名
最后把它们一起发给服务端。
比如请求头可能长这样:
X-Token: app-token-123
X-Timestamp: 1760000000
X-Nonce: 8f6c2d1a9b3e4f77
X-Signature: 3b7a1f...服务端这边做什么?
服务端收到请求后,大致会按这个顺序检查:
token合不合法timestamp有没有过期nonce之前有没有见过signature能不能验通过
如果都通过,才执行业务逻辑。
你会发现,它像一层层筛子:
- 第一层看身份
- 第二层看时效
- 第三层看是否重复
- 第四层看内容完整性
最后才轮到真正的业务代码。
这其实也是很多成熟系统的思路:
把风险尽量挡在业务逻辑外面。
八、签名到底签什么?这是最容易翻车的地方
很多人以为“用了 HMAC”就结束了,真正麻烦的其实是:
你到底拿什么去签?
如果客户端和服务端对“待签名字符串”的理解不一样,再安全的算法也没用。
比如下面这些细节,都会影响签名结果:
- Query 参数顺序不同
- JSON 字段顺序不同
- 有没有多余空格
- 中文编码方式是否一致
- Body 是原文签,还是先做 hash 再签
所以工程里通常会先定义一套“规范化规则”。
比如约定按这个顺序拼接:
HTTP_METHOD
PATH
QUERY_STRING_SORTED
BODY_SHA256
TOKEN
TIMESTAMP
NONCE这样双方只要遵守同一套规则,就能稳定验签。
这部分像什么呢?
像两个人约好密码本的页码规则。
不是你们的数学不行,而是你翻第 18 页、我翻第 81 页,那永远对不上。
所以很多接口签名问题,本质不是“算法错了”,而是“规范没对齐”。
九、Redis TTL 为什么几乎是防重放的标配?
因为它太适合做这件事了。
防重放需要的不是一个“永久档案馆”,而是一个“短期记忆”:
- 我只需要知道这份请求在最近几分钟内有没有出现过
- 再久远的记录,其实没必要一直存着
Redis 正好满足这个特点:
- 读写快
- 支持高并发
- 可以天然设置 TTL
- 很适合做短期去重
比如服务端验签通过后,写入一条记录:
SET replay:token:nonce 1 EX 300 NX这条命令的意思可以简单理解成:
- 如果这个 key 不存在,就写进去
- 5 分钟后自动过期
- 如果已经存在,就说明之前登记过了
它很像门卫说:
这张票我已经见过了,今天这轮你不能再进。
而且因为 Redis 是自动过期的,你不用自己再去做一堆清理逻辑。
十、那是不是做了这些就万无一失了?
也不能这么说。
安全这件事很少有“一个方案打天下”,更多是你补哪一层短板。
这套方案解决得比较好的,是:
- 身份识别
- 防篡改
- 短时间窗口防重放
但在真实业务里,很多系统还需要额外考虑:
- token 泄漏怎么失效
- secret 怎么轮换
- 请求日志里是否泄漏敏感信息
- 业务本身是否需要幂等
这里尤其要说一下:
防重放,不等于业务幂等。
这是两个经常被混在一起的概念。
防重放关注的是:
同一份请求是不是被恶意重复发送了
业务幂等关注的是:
即使重复提交,业务结果也只能生效一次
比如支付接口里,就算你做了签名、防重放,往往还是要有:
- 订单号
- 幂等号
- 状态机控制
因为安全层拦住的是“可疑重复请求”,业务层兜住的是“重复执行后果”。
两层都重要,谁也替代不了谁。
十一、接口鉴权还有哪些常见选择?
讲到这里,顺便把常见方案也放到一张脑图里。
1. Session / Cookie
适合传统 Web 网站。
特点是简单,浏览器天然支持,但不太适合开放平台,也不负责请求防篡改。
2. Bearer Token / Access Token
适合前后端分离、App、小程序等用户登录态接口。
优点是好用、生态成熟。
但它重点仍然是“身份凭证”,不是“请求级别安全”。
3. JWT
本质上也是 token 的一种,只不过是“自带信息”的 token。
适合分布式场景,减少查库压力。
但别误会,JWT 也不能天然防重放、防参数篡改。
4. API Key / AppKey
开放平台常见。
它能帮助服务端知道“你是哪家接入方”,但通常也要搭配签名一起使用,不然只能识别身份,防护不完整。
5. HMAC 签名
非常适合开放平台、支付接口、回调验签。
它在“请求防篡改”这件事上很强,但通常需要和时间戳、nonce、Redis 配套,才能把重放问题也一起收掉。
6. mTLS / 双向证书
安全性高,但接入和运维复杂度也高。
更适合银行、政企、强管控内网环境。
对大部分普通业务团队来说,它不是第一层默认解法。
十二、如果只能记住一句话,我希望是这句
很多文章喜欢把安全机制讲得像一堆缩写词比赛:
- Token
- JWT
- HMAC
- Nonce
- TTL
但你真正该记住的,不是名词,而是它们各自负责的那一小块问题。
你可以把整件事压缩成一句话:
Token 负责“你是谁”,HMAC 负责“你有没有改内容”,时间戳负责“你是不是过期了”,Nonce + Redis TTL 负责“你是不是拿同一份请求又来了一次”。
一旦这样理解,很多看似复杂的接口鉴权设计,都会变得很清楚。
十三、最后给一个更接地气的判断标准
如果你的接口只是普通的用户查询接口,可能 token 就已经够用了。
但如果你的接口带着下面这些关键词:
- 金额
- 扣减
- 发券
- 核心状态变更
- 第三方开放调用
- 回调通知
那你最好别只停留在“带个 token”这一步。
更稳妥的思路通常是:
Token + HMAC + 时间戳 + Nonce + Redis TTL
它不一定是最豪华的方案,但常常是性价比很高、工程落地也比较顺手的一套组合拳。
因为它不是在赌“别人不会攻击我”,而是在默认:
请求可能被截获、可能被篡改、可能被重放。
而好的安全设计,往往就是从这种不侥幸开始的。
尾声
刚接触接口安全时,大家很容易把这些机制看成“后端为了折磨前端和客户端发明出来的附加题”。
但理解得更深一点你会发现,它们并不是为了复杂而复杂。
它们只是分别在回答几个很朴素的问题:
- 你是谁?
- 你带来的东西有没有被动过?
- 这是不是过期票?
- 这张票是不是已经用过了?
安全方案之所以看起来层层叠叠,不是因为工程师爱堆概念,而是因为现实里的风险,本来就不是一个问题。
当你把这些问题拆开,一个个去对应,接口鉴权这件事就会从“神秘学”,慢慢变成“常识”。
如果你愿意,下一篇我们还可以继续聊两个非常实战的话题:
- Spring Boot 里怎么落一套 HMAC + 时间戳 + Nonce 验签中间件
- 防重放和业务幂等,到底应该怎么分层设计
