主题
JWT
使用
@nestjs/jwt与 Passport 实现 Bearer Token 鉴权
适用范围
本页记录 NestJS 自行签发 JWT 的集成方式:登录后签发 access token,受保护接口通过 Authorization: Bearer <token> 验证身份。
如果项目已使用 Better Auth 管理会话,应由 Better Auth 作为唯一认证边界,不要同时为同一套登录接口维护另一套 JWT 签发和验证逻辑。本页适合无状态 API、服务间调用,或需要自行管理用户凭据与令牌生命周期的场景。
安装
bash
pnpm add @nestjs/jwt @nestjs/passport passport passport-jwt
pnpm add -D @types/passport-jwt配置 JWT 模块
密钥只从环境变量读取,不写入源码或提交到仓库。JwtModule 负责签发和验证令牌;将注册逻辑放在 AuthModule,而非在业务模块中重复配置。
ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { JwtModule } from '@nestjs/jwt'
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.getOrThrow<string>('JWT_SECRET'),
signOptions: { expiresIn: '15m' },
}),
}),
],
})
export class AuthModule {}md
JWT_SECRET=替换为高熵随机密钥短期 access token 降低泄露后的有效窗口;需要长期登录时,另行设计可撤销、可轮换的 refresh token,不要只延长 access token 的过期时间。
签发令牌
密码校验、用户状态检查等登录逻辑完成后,只将最小身份信息写入 payload。标准的 sub 字段用于用户 ID;不要写入密码、完整用户资料或权限快照。
ts
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { UsersService } from '../users/users.service'
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async signIn(email: string, password: string) {
const user = await this.usersService.validateCredentials(email, password)
if (!user) throw new UnauthorizedException()
return {
accessToken: await this.jwtService.signAsync({
sub: user.id,
email: user.email,
}),
}
}
}验证请求
JwtStrategy 统一从 Bearer Header 提取令牌、验证签名和过期时间,并把 validate() 的返回值赋给 request.user。业务代码不应自行解析 JWT。
ts
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.getOrThrow<string>('JWT_SECRET'),
})
}
validate(payload: { sub: string; email: string }) {
return { id: payload.sub, email: payload.email }
}
}通过 Guard 应用该策略;需要身份的路由显式使用 JwtAuthGuard。若大多数接口都受保护,可将它注册为 APP_GUARD,再用元数据装饰器明确标记登录、注册、健康检查等公开路由。
ts
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}ts
@UseGuards(JwtAuthGuard)
@Get('profile')
profile(@Request() request: { user: { id: string; email: string } }) {
return request.user
}校验边界
- JWT 验证只证明身份;角色、资源归属和业务状态仍由 Guard 或 Service 校验。
- 修改密钥会使全部现有令牌失效;正常登出或主动吊销需要服务端维护令牌版本、会话或拒绝列表。
- 对登录接口实施限流;密码仅保存为安全哈希,不记录原始密码、token 或
Authorization请求头。
