开发文档

鲜花交易系统与鲜花交易平台(PHP / ThinkPHP 版)的研发记录:功能上线、架构演进与实施经验,按发布日期持续更新。

鉴权与授权的边界:一个配置项是怎么把令牌过期搞坏的

现象:禁用账号后旧令牌还能用

安全巡检时发现一个问题:把某个账号禁用、并把令牌版本号加一之后,按设计旧令牌应当立即失效。但实测中,旧的令牌在相当长的一段时间内仍能通过校验。

这不是 JWT 的"固有缺陷",而是我们的配置被写坏了。

根因:同一个配置键承担了两种语义

鉴权中间件里有一个"时钟容忍度"参数,用于容忍客户端与服务端之间的小幅时间漂移(正常取值是几十秒级别)。

授权模块上线时,为了处理"授权到期后给客户一个宽限期继续使用",也用了同一个配置键,写入了一个以"天"为单位的值。

于是:

  • 鉴权模块按秒解释这个值 → 容忍度被放大到极大的范围;
  • 结果:令牌的过期时间判断几乎失效;
  • 而所有功能测试都通过,因为正常登录、正常调用接口完全不受影响。

这类缺陷的可怕之处在于:它不影响功能,只削弱安全边界。

为什么排查花了时间

三个原因:

  1. 配置值本身合法。读取时没有范围断言,任何数字都接受;
  2. 两个模块耦合在配置键上,代码里看不出调用关系,只有运行时才串起来;
  3. 测试用例都在验证"能登录""能调通",没有一条验证"过期就必须拒绝"。

修法:三件事同时做

第一,配置命名空间隔离。 鉴权参数统一放在 auth.* 下(如 auth.jwt_leeway),授权参数放 license.* 下。同名键不允许跨命名空间复用。

第二,参数范围断言。 关键安全参数在读取时校验范围:容忍度必须是 0~300 秒,超出即拒绝启动或回落默认值并告警。宁可"配置不生效",也不要"配置生效但方向错了"。

第三,补上针对性用例。 加三条:

  • 令牌过期后调用接口必须 401;
  • 账号被禁用后,未过期的令牌也必须被拒;
  • 令牌版本号变更后,旧令牌立即失效。

这三条用例的价值远高于一堆"正常路径"测试,因为它们守护的是边界。

三条可复用的约束

这次事故之后,我们把它抽象成三条规则,写进开发规范:

  1. 安全相关参数必须带命名空间,禁止裸键名(如 leeway、timeout);
  2. 参数取值范围要在代码里断言,不能只写在文档里;
  3. 鉴权、授权、加密三条链路的关键行为,必须有负向用例(应该失败的必须失败)。

一句话总结

"能跑通"和"边界是对的"是两件事。功能测试证明前者,负向用例才证明后者。一套要交付给客户长期运行的系统,后者的价值往往更高——因为安全边界一旦破了,不会有人主动告诉你。

本文属于「开发文档」系列,记录鲜花交易系统(PHP + ThinkPHP 8 + MySQL 8)的实际研发进展。想了解功能清单与报价,可前往系统报价或联系我们。