签名必须包含时间戳和随机串,否则易受重放攻击;应统一参数归一化、使用hmac-sha256、redis原子去重、配置化开关及完整日志。

签名生成必须包含时间戳和随机串
不加时间戳或随机串的签名等于没签——攻击者重放一次合法请求就能无限使用。ThinkPHP 里常见错误是只对 $_POST 或 $request->param() 做 MD5,漏掉关键上下文。
实操建议:
- 强制要求客户端传
timestamp(秒级或毫秒级)和nonce(如 UUID 短格式),服务端校验时间偏差不超过 300 秒 - 签名原文拼接顺序固定:按参数名 ASCII 升序排序后,用
key1=value1&key2=value2格式拼接,再拼上timestamp=xxx&nonce=yyy - 密钥不硬编码在代码里,从配置或环境变量读取,例如
config('api.sign_key') - 别用
md5($str),改用hash_hmac('sha256', $str, $sign_key),防长度扩展攻击
ThinkPHP 中间件里统一验签最稳妥
写在控制器方法里容易漏、难复用,也绕过中间件的前置逻辑(比如跨域、日志)。很多项目把验签塞进 app\common\middleware\Auth,但没拦截非 API 路由,导致 /admin/ 下的接口也被误放行。
实操建议:
- 新建中间件
app\middleware\SignVerify,在app/middleware.php中只对api/前缀路由启用 - 在
handle()开头就调用$this->verifySign($request),验证失败直接return json(['code'=>401, 'msg'=>'sign invalid'], 401) - 注意:GET 请求的 query 参数、POST 的 JSON body、FORM 表单要统一提取成关联数组再排序拼串,别漏
$request->get()或json_decode($request->getContent(), true) - 验签通过后,把
timestamp和nonce从参数中 unset 掉,避免污染后续业务逻辑
nonce 需要服务端去重存储,不能只靠内存缓存
用 Cache::store('redis')->set($nonce, 1, 300) 看似可行,但并发高时可能重复写入成功,导致“一次 nonce 多次通过”。更糟的是用 session() 或本地 file 缓存,根本跨不了机器。
实操建议:
- 必须用支持原子操作的存储,推荐 Redis 的
SETNX(ThinkPHP 8.0+ 可用cache()->handler()->setNx($key, $value, $expire)) - key 设计为
sign:nonce:{md5($nonce)},防止 key 过长或含特殊字符 - 如果用数据库,建唯一索引字段
nonce,插入前不查直接INSERT IGNORE,失败即拒绝请求 - 注意清理时机:不是等过期,而是验签成功后立刻写入,并设 TTL 略大于时间窗口(比如 310 秒)
调试阶段必须关掉签名,上线前打开开关配置
开发时硬编码 if (env('APP_DEBUG')) return true; 很危险——有人忘了删,或者测试环境开着 debug 但没配好签名,结果线上切流量就全挂。
实操建议:
- 所有验签逻辑包裹在配置开关下:
if (!config('api.check_sign', false)) return; - 配置项放在
config/api.php,不同环境用.env控制:API_CHECK_SIGN=true - 日志里记录每次验签的原始参数、生成的 sign 字符串、客户端传的
sign,方便对比排查差异(比如大小写、空格、编码) - 前端调试时,用 Postman 的 Pre-request Script 自动生成签名,别手动拼——URL 编码不一致是高频错误点
签名机制真正难的不是算法,是参数归一化过程里各种隐式转换:JSON 的键序、空值处理、嵌套数组扁平方式、URL 解码时机。哪怕只差一个空格或一个斜杠,hash_hmac 结果就完全不同。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











