一个无状态的令牌,服务器签完就不再过问,任何持有相同密钥的服务都能独立验证,连数据库都不必查询。听起来像分布式系统的理想认证方案,但它藏着一个天生无法回避的缺陷:一旦签发,在过期之前你没法让这个令牌立刻作废。
这是 NestJS 系列文章的第二篇,之前聊过小团队 Git 协作,代码仓库和分支模型就绪后,首先要上的就是认证——它会触及每个后续功能。本文覆盖的范围很明确:用 Passport 和 @nestjs/jwt 签发、验证访问令牌,构建 auth 模块,以及那些上线后总让团队头疼的坑。至于刷新令牌、轮换和撤销,它们值得单独一篇长文,塞在一起只会把真正引发事故的部分一笔带过。
JWT 的本质是一组经过签名、自包含的用户声明。服务端签发一次,此后任何持有签名密钥(或公钥,如果用了非对称签名)的服务都能离线验证,无需查数据库,也无需求助会话存储。这整套价值主张就落在“无状态验证”这五个字上。
这种特性在不止一个后端进程时才开始凸显。单实例、粘性会话的巨石应用几乎享受不到 JWT 的好处;一套用 Redis 托管的会话 cookie 就能以更低的复杂度搞定。可一旦负载均衡后面挂了多台 API 实例,或者某个独立服务要验证调用者身份而又不愿直连主库,无状态令牌就能省下一次网络往返和一套共享状态的依赖。
代价也同样真切:JWT 一旦发出,过期前就收不回来。服务端没有内置的“把这名用户立刻踢下线”的能力,除非你自己搭一套撤销机制。这几乎决定了所有后续设计的走向,也是为何需要把访问令牌的寿命压得很短,并与一个独立的刷新令牌机制搭配——这正是下一篇文章的主题。
NestJS 的 @nestjs/passport 和 @nestjs/jwt 包提供了构建积木。经过多个项目打磨后稳定下来的模块形状是:一个 AuthModule 掌管登录和令牌签发,一个 JwtStrategy 供 Passport 在校验进来令牌时调用,再用一个 JwtAuthGuard 来保护路由。
依赖安装十分直接:npm install @nestjs/passport @nestjs/jwt passport passport-jwt,外加对应的类型声明。接下来搭建模块时有一条铁律——令牌的密钥和过期时间必须来自配置,绝对不能在代码里写死。
模块的接线从 ConfigModule 和 JwtModule 开始,利用 ConfigService 将密钥和环境变量注入 JwtModule 的 secret 和 signOptions。这样一来,任何环境切换都只需改一处配置,而不是去翻业务代码。这个 auth.module.ts 只是起手式,紧跟着会建立 AuthService 处理登录校验与令牌签发,AuthController 暴露登录端点——但最核心的安全考量其实落在 JwtStrategy 如何从负载中还原用户,以及 JwtAuthGuard 如何拒绝不合规的请求。
团队在真实环境里踩的坑往往不在源码里,而在思维惯性上。把访问令牌的有效期设成几小时甚至几天,是早期最常犯的错;忘记刷新令牌意味着一旦令牌被泄露,攻击窗口就会被拉到失效时间那么长。另外,一旦产品要求管理员能“强制下线某个用户”,开发者才惊觉 JWT 根本不支持,这时要么引入黑名单,要么改造令牌版本号,而这些都不是一天能补好的。所以从一开始就规划短时效访问令牌和独立的刷新路径,远比事后打补丁便宜。
明白了 JWT 能给你什么,也清楚它给不了你什么之后,NestJS 内建的这些工具才能被用在刀刃上。下一篇文章会完整拆解刷新令牌、轮换和撤销的具体实现,把本文有意留出的那块短板补齐。
热门跟贴