实用指南:【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.user
  return 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 要求这个签名(默认属性名是 usernamepassword,可通过 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/passportpassport-jwt 等。记得在 AuthModuleimports: [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 。

posted @ 2025-11-04 21:03  gccbuaa  阅读(24)  评论(0)    收藏  举报