【Nest】登录鉴权
登录鉴权的整体流程:注册 -> 登录 -> 获取用户信息。
以下从流程的先后顺序入手。
注册
auth.controller.ts
@Post('register')@ApiOperation({summary: '用户注册'})@ApiBody({type: CreateUserDto})@ApiResponse({status: 201, description: '注册成功'})@ApiResponse({status: 400, description: '注册失败'})async register(@Body() createUserDto: CreateUserDto) {return this.authService.register(createUserDto);}
api 相关的装饰器是 swagger 文档装饰器。
然后 dto 模型,主要使用 class-validator 做验证,全局注册 pipe 管道,对传入的参数进行 class-validator 和 class-transformer 验证和转换。
create-user.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
import { Role } from '@prisma/client';export class CreateUserDto {@ApiProperty({ description: '用户名', example: 'john_doe' })@IsNotEmpty({ message: '用户名不能为空' })@IsString({ message: '用户名必须是字符串' })username: string;@ApiProperty({ description: '密码', example: 'password123' })@IsNotEmpty({ message: '密码不能为空' })@IsString({ message: '密码必须是字符串' })@MinLength(6, { message: '密码长度不能少于6个字符' })password: string;@ApiProperty({ description: '角色', enum: Role, default: Role.competitor })@IsOptional()@IsEnum(Role, { message: '角色值无效' })role?: Role;@ApiProperty({ description: '名称', example: '张三' })@IsOptional()@IsString({ message: '名称必须是字符串' })name?: string;@ApiProperty({ description: '邮箱', example: 'john@example.com' })@IsOptional()@IsEmail({}, { message: '邮箱格式不正确' })email?: string;@ApiProperty({ description: '手机号', example: '13800138000' })@IsOptional()@IsString({ message: '手机号必须是字符串' })phone?: string;
}
main.ts
import { ValidationPipe } from '@nestjs/common';// 启用全局验证管道app.useGlobalPipes(new ValidationPipe({whitelist: true,transform: true,forbidNonWhitelisted: true,}),);
然后注册的 service 通过加密,对敏感数据去除。
这里的响应数据去除也可以使用 响应拦截器,配合序列化 exclude-properties 。
auth.service.ts
async register(userData: any) {const hashedPassword = await bcrypt.hash(userData.password, 10);const newUser = await this.usersService.create({...userData,password: hashedPassword,});const { password, ...result } = newUser;return result;}
登录
auth.controller.ts
@UseGuards(LocalAuthGuard)@Post('login')@ApiOperation({summary: '用户登录'})@ApiBody({type: LoginDto})@ApiResponse({status: 200, description: '登录成功'})@ApiResponse({status: 401, description: '登录失败'})async login(@Request() req) {return this.authService.login(req.user);}
auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { UsersModule } from '../users/users.module';@Module({imports: [UsersModule,PassportModule,JwtModule.registerAsync({inject: [ConfigService],useFactory: (config: ConfigService) => {return {secret: config.get<string>('JWT_SECRET'),signOptions: {expiresIn: config.get<string>('JWT_EXPIRES_IN'),},};},}),],controllers: [AuthController],providers: [AuthService, JwtStrategy, LocalStrategy],exports: [AuthService],
})
export class AuthModule {}
auth.service.ts
async login(user: any) {const payload = { username: user.username, sub: user.id };return {access_token: this.jwtService.sign(payload),user: {id: user.id,username: user.username,role: user.role,name: user.name,},};}
这里使用到的守卫主要适用于做权限控制。
Local:(用户名/密码)相关的策略与 Guard ;
JWT(Token):相关的策略与 Guard 。
- Strategy(策略):Passport 的实现单元,负责验证凭证(比如 username/password 或 JWT token),验证通过后返回
user
(或抛出异常)。Nest 用@nestjs/passport
封装 Passport,并把validate()
作为 Passport 的 verify 回调。(docs.nestjs.com) - Guard(守卫):Nest 层面的拦截点,决定请求是否继续。
@UseGuards(AuthGuard('xxx'))
会把请求交给 Passport 的对应策略去做认证。Guard 在 Nest 请求生命周期中能访问ExecutionContext
(也可用于权限判定等)。(docs.nestjs.com)
一、LocalStrategy + LocalAuthGuard(用户登录验证 — username/password)
- 用于 登录(认证凭证):在登录接口上使用 LocalAuthGuard,它会触发
passport-local
的策略(LocalStrategy)去校验用户名和密码,成功后把user
放到req.user
上供 controller 使用(通常 controller 会再生成 JWT)。(docs.nestjs.com)
local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {constructor(private authService: AuthService) {// 如果表单字段不是 username/password,可在这里改: super({ usernameField: 'email' })super();}async validate(username: string, password: string): Promise<any> {const user = await this.authService.validateUser(username, password);if (!user) {throw new UnauthorizedException('用户名或密码错误');}// 返回的对象会被 passport 附加到 req.userreturn user;}
}
local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
要点与注意:
- LocalStrategy 的
validate(username, password)
是 Passport 的 verify 回调,Nest 要求这个签名(默认属性名是username
和password
,可通过super({ usernameField: 'email' })
改)。(docs.nestjs.com) - Local 验证本身可以配合 session(passport 的 session)使用,但在常见的 JWT 无状态登录流程里,Local 只是一次性验证并返回 user,真正的会话由 JWT 管理。(docs.nestjs.com)
二、JwtStrategy + JwtAuthGuard(Token 验证 — 保护接口)
- 在用户登录拿到 JWT 后,客户端在后续请求里带上 token(通常是
Authorization: Bearer <token>
)。受保护的路由用JwtAuthGuard
,它会触发passport-jwt
策略(JwtStrategy)来解析 token、校验签名并执行validate(payload)
,通过则把user
注入到req.user
,请求继续执行。(docs.nestjs.com)
jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './auth.service';
import { ConfigService } from '@nestjs/config';@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {constructor(private authService: AuthService,private configService: ConfigService,) {super({jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 从 Authorization: Bearer 提取ignoreExpiration: false,secretOrKey: configService.get('JWT_SECRET') || process.env.JWT_SECRET,});}async validate(payload: any) {// payload 是 jwt 的解码内容(例如 { sub: userId, username, iat, exp })// 建议在这里再去 DB 校验用户是否存在 / 是否被禁用 / 是否已登出等const user = await this.authService.validateUserByJwtPayload(payload);if (!user) {throw new UnauthorizedException();}return user; // 会被附到 req.user}
}
jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
要点与注意:
passport-jwt
提供多种 token 提取器(从 header, 从 cookie, 自定义 extractor),最常用的是ExtractJwt.fromAuthHeaderAsBearerToken()
。你可以根据客户端把 token 存哪里来自定义。(Passport.js)- 重要:如果 token 已过期或格式错误,Passport 不会调用
validate()
:策略会直接失败(因此 validate 一般假设拿到的是有效的未过期 token,并负责根据 payload 再查用户等)。这是常见调试点(如果 validate 没被调用,先检查 token 是否过期或提取方式是否正确)。(Stack Overflow)
常见坑 / 调试技巧:
- “Unknown authentication strategy ‘jwt’”:通常是忘记在模块里
providers: [JwtStrategy]
或没安装 / 注册@nestjs/passport
、passport-jwt
等。记得在AuthModule
中imports: [PassportModule, JwtModule.register(...)]
并把策略作为 provider 注入。(若遇到这类错误,先确认 providers/imports 注册是否正确) 。 - validate 不被调用:先确认 token 是否被正确提取(header 名、cookie 名),以及 token 是否过期(过期时
validate
不会被执行)。(GitHub) - 不要只信 payload:即使 JWT 签名校验通过,也建议在
validate(payload)
中去 DB 查用户状态(是否被删除/禁用、是否已登出/黑名单等),以便做 token 撤销等。 - session vs stateless:LocalStrategy 可以配合 session(passport session)做基于 session 的 auth;如果是 JWT 流程,通常不会启用 session(stateless)。(docs.nestjs.com)
获取用户信息
auth.controller.ts
@UseGuards(JwtAuthGuard)@Get('profile')@ApiOperation({summary: '获取用户信息'})@ApiResponse({status: 200, description: '获取成功'})@ApiResponse({status: 401, description: '未授权'})getProfile(@Request() req) {return req.user;}
Passport 的一个特性:Passport 根据 validate() 方法的返回值自动创建一个 user 对象,并将其赋值给 Request 对象,作为 req.user 。所以,这里如果通过了守卫,可以直接从 req 上面获取 user 。