签名版本号必须作为独立参数传入,且只能是url query或表单字段(json请求需加顶层字段),名称严格为sign_version;验签前须显式获取并校验其合法性,再动态选密钥,签名原文必须包含sign_version但排除sign字段。

签名版本号必须作为独立参数传入,不能藏在 body 或 header 里
ThinkPHP 验签失败一大半是因为 sign_version 没被正确提取——它得是请求 URL 的 query 参数或表单字段,且名字必须和验签逻辑里硬编码/配置的键名完全一致。藏在 Authorization header 里、拼在 JSON body 里、或者用 ver v 等别名,都会导致验签时根本拿不到版本号,直接 fallback 到默认密钥(往往不是你想要的那版)。
- 推荐统一用
sign_version作为参数名,前后端对齐 - GET 请求放 query:
/api/user?sign_version=2&sign=xxx - POST 表单提交也走 form field;JSON 请求则必须额外加一个顶层字段:
{"sign_version": "2", "data": {...}, "sign": "..."} - 别依赖中间件自动注入——版本号是业务参数,不是基础设施,该由调用方明确提供
验签前必须先读取 sign_version,再动态选密钥
ThinkPHP 默认验签逻辑(比如用 think\facade\Request 拿参数)是“先取全部参数 → 生成签名 → 对比”,但这个流程里 sign_version 如果没被提前识别,就无法决定用哪套密钥。所以不能把验签写成一个黑盒函数,必须拆开:先抽参数、再判版本、再取对应密钥、最后算签。
- 在控制器或中间件里,用
$request->param('sign_version', '1')显式获取版本,默认值设为历史兼容版(如'1') - 密钥不能写死,要按版本查配置:
config('sign.keys.' . $version),配置示例:'keys' => ['1' => 'abc', '2' => 'def_v2'] - 注意:如果用
$request->only()过滤参数,务必把sign_version加进去,否则后续验签时它就消失了
sign 字段本身不参与签名计算,但 sign_version 必须参与
常见错误是把 sign 和 sign_version 都从待签名参数中排除,结果新版签名算法用了新密钥,却拿旧版参数序列化结果去比,必然失败。签名原文必须包含 sign_version,但必须剔除 sign —— 否则就是拿自己验证自己,失去意义。
- 标准做法:用
$request->except(['sign'])拿原始参数,确保sign_version在里面 - 排序、拼接、加盐、哈希前,确认数组里有
sign_version键,且值是字符串(不是 int,避免 PHP 自动类型转换导致拼接差异) - 别用
$request->param()直接拿全部,它可能过滤掉空值或特殊键名;用$request->get()+$request->post()合并更可控
多版本共存时,sign_version 的合法性校验不能省
不校验版本号就直接去查配置,遇到非法值(如 sign_version=999)会返回 null 密钥,导致签名恒为 false,而且错误日志里只显示“验签失败”,看不出是版本不存在。线上一旦被恶意刷,排查成本很高。
- 在取密钥前加判断:
if (!in_array($version, ['1', '2', '3'])) { throw new ValidateException('Invalid sign_version'); } - 版本号建议用字符串定义(配置和代码里都写
'1'而非1),避免弱类型比较出错 - 测试时故意传个不存在的版本,确认返回的是明确错误,而不是静默 fallback 到 v1 密钥(后者会让问题更隐蔽)
最麻烦的点其实是参数来源混杂:GET、POST、JSON、form-data 可能同时存在,而 sign_version 如果在某个分支里被忽略或覆盖,验签就会用错密钥——这种问题在线上压测时才暴露,但日志里看不出参数是否被正确采集。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










