第4章:JWT认证与授权
🎯 本章目标
在这一章中,我们将:
- 实现 JWT 认证系统
- 创建登录和注册功能
- 实现认证守卫和权限控制
- 创建刷新令牌机制
- 实现密码重置功能
📦 安装认证相关依赖
如果在第1章中没有安装,请安装以下依赖:
bash
# JWT 和 Passport 相关
npm install @nestjs/jwt @nestjs/passport passport passport-jwt passport-local
npm install -D @types/passport-jwt @types/passport-local
# 密码加密(如果没有安装)
npm install bcrypt
npm install -D @types/bcrypt
🔧 创建认证模块
1. 生成认证模块
bash
# 生成认证模块
nest generate module modules/auth
nest generate service modules/auth
nest generate controller modules/auth
# 或使用简写
nest g mo modules/auth
nest g s modules/auth
nest g co modules/auth
2. 创建目录结构
src/modules/auth/
├── dto/ # 数据传输对象
│ ├── login.dto.ts
│ ├── register.dto.ts
│ ├── refresh-token.dto.ts
│ └── reset-password.dto.ts
├── guards/ # 守卫
│ ├── jwt-auth.guard.ts
│ ├── local-auth.guard.ts
│ └── roles.guard.ts
├── strategies/ # Passport 策略
│ ├── jwt.strategy.ts
│ └── local.strategy.ts
├── decorators/ # 自定义装饰器
│ ├── current-user.decorator.ts
│ ├── public.decorator.ts
│ └── roles.decorator.ts
├── auth.controller.ts # 控制器
├── auth.service.ts # 服务
└── auth.module.ts # 模块
📝 创建认证 DTO
1. 登录和注册 DTO
typescript
// src/modules/auth/dto/login.dto.ts
import { IsEmail, IsString, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class LoginDto {
@ApiProperty({
description: '邮箱地址',
example: 'user@example.com'
})
@IsEmail({}, { message: '请输入有效的邮箱地址' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
@ApiProperty({
description: '密码',
example: 'Password123!'
})
@IsString({ message: '密码必须是字符串' })
@IsNotEmpty({ message: '密码不能为空' })
password: string;
}
typescript
// src/modules/auth/dto/register.dto.ts
import { CreateUserDto } from '../../users/dto/create-user.dto';
import { OmitType } from '@nestjs/swagger';
export class RegisterDto extends OmitType(CreateUserDto, ['role'] as const) {}
typescript
// src/modules/auth/dto/refresh-token.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RefreshTokenDto {
@ApiProperty({
description: '刷新令牌',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
})
@IsString({ message: '刷新令牌必须是字符串' })
@IsNotEmpty({ message: '刷新令牌不能为空' })
refreshToken: string;
}
typescript
// src/modules/auth/dto/reset-password.dto.ts
import { IsEmail, IsString, IsNotEmpty, MinLength, Matches } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ForgotPasswordDto {
@ApiProperty({
description: '邮箱地址',
example: 'user@example.com'
})
@IsEmail({}, { message: '请输入有效的邮箱地址' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
}
export class ResetPasswordDto {
@ApiProperty({
description: '重置令牌',
example: 'reset-token-here'
})
@IsString({ message: '重置令牌必须是字符串' })
@IsNotEmpty({ message: '重置令牌不能为空' })
token: string;
@ApiProperty({
description: '新密码',
example: 'NewPassword123!',
minLength: 8
})
@IsString({ message: '新密码必须是字符串' })
@IsNotEmpty({ message: '新密码不能为空' })
@MinLength(8, { message: '新密码至少8个字符' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
message: '新密码必须包含大小写字母、数字和特殊字符'
})
newPassword: string;
}
🔐 创建自定义装饰器
1. 公共路由装饰器
typescript
// src/modules/auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
2. 角色装饰器
typescript
// src/modules/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '@prisma/client';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
3. 当前用户装饰器
typescript
// src/modules/auth/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '@prisma/client';
export const CurrentUser = createParamDecorator(
(data: keyof User | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
🛡️ 创建 Passport 策略
1. 本地策略(用户名密码登录)
typescript
// src/modules/auth/strategies/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';
import { User } from '@prisma/client';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
usernameField: 'email', // 使用邮箱作为用户名
});
}
async validate(email: string, password: string): Promise<User> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('邮箱或密码错误');
}
return user;
}
}
2. JWT 策略
typescript
// src/modules/auth/strategies/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/users.service';
import { User } from '@prisma/client';
export interface JwtPayload {
sub: string;
email: string;
role: string;
iat?: number;
exp?: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService,
private usersService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('jwt.secret'),
});
}
async validate(payload: JwtPayload): Promise<User> {
const user = await this.usersService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException('用户不存在');
}
// 检查用户状态
if (user.status !== 'ACTIVE') {
throw new UnauthorizedException('用户账户已被禁用');
}
return user as User;
}
}
🔒 创建认证守卫
1. JWT 认证守卫
typescript
// src/modules/auth/guards/jwt-auth.guard.ts
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
throw err || new UnauthorizedException('访问令牌无效或已过期');
}
return user;
}
}
2. 本地认证守卫
typescript
// src/modules/auth/guards/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
3. 角色权限守卫
typescript
// src/modules/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '@prisma/client';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.role?.includes(role));
}
}
🔧 实现认证服务
typescript
// src/modules/auth/auth.service.ts
import {
Injectable,
UnauthorizedException,
BadRequestException,
ConflictException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../users/users.service';
import { PrismaService } from '../../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { ForgotPasswordDto, ResetPasswordDto } from './dto/reset-password.dto';
import { User } from '@prisma/client';
import { UserEntity } from '../users/entities/user.entity';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
export interface AuthTokens {
accessToken: string;
refreshToken: string;
expiresIn: number;
}
export interface AuthResponse {
user: UserEntity;
tokens: AuthTokens;
}
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private configService: ConfigService,
private prisma: PrismaService,
) {}
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.usersService.findByEmail(email);
if (!user) {
return null;
}
// 检查用户状态
if (user.status !== 'ACTIVE') {
throw new UnauthorizedException('用户账户已被禁用');
}
const isPasswordValid = await this.usersService.validatePassword(
user,
password,
);
if (!isPasswordValid) {
return null;
}
return user;
}
async login(loginDto: LoginDto): Promise<AuthResponse> {
const { email, password } = loginDto;
const user = await this.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('邮箱或密码错误');
}
const tokens = await this.generateTokens(user);
return {
user: new UserEntity(user),
tokens,
};
}
async register(registerDto: RegisterDto): Promise<AuthResponse> {
try {
const user = await this.usersService.create(registerDto);
const tokens = await this.generateTokens(user as User);
return {
user,
tokens,
};
} catch (error) {
if (error instanceof ConflictException) {
throw error;
}
throw new BadRequestException('注册失败');
}
}
async refreshTokens(refreshTokenDto: RefreshTokenDto): Promise<AuthTokens> {
const { refreshToken } = refreshTokenDto;
try {
const payload = this.jwtService.verify(refreshToken, {
secret: this.configService.get<string>('jwt.refreshSecret'),
});
const user = await this.usersService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException('用户不存在');
}
if (user.status !== 'ACTIVE') {
throw new UnauthorizedException('用户账户已被禁用');
}
return this.generateTokens(user as User);
} catch (error) {
throw new UnauthorizedException('刷新令牌无效或已过期');
}
}
async forgotPassword(forgotPasswordDto: ForgotPasswordDto): Promise<void> {
const { email } = forgotPasswordDto;
const user = await this.usersService.findByEmail(email);
if (!user) {
// 为了安全,不暴露用户是否存在
return;
}
// 生成重置令牌
const resetToken = crypto.randomBytes(32).toString('hex');
const resetExpires = new Date(Date.now() + 15 * 60 * 1000); // 15分钟后过期
await this.prisma.user.update({
where: { id: user.id },
data: {
passwordResetToken: resetToken,
passwordResetExpires: resetExpires,
},
});
// TODO: 发送重置密码邮件
// await this.emailService.sendPasswordResetEmail(user.email, resetToken);
console.log(`密码重置令牌: ${resetToken}`); // 开发环境下打印令牌
}
async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<void> {
const { token, newPassword } = resetPasswordDto;
const user = await this.prisma.user.findFirst({
where: {
passwordResetToken: token,
passwordResetExpires: {
gt: new Date(),
},
},
});
if (!user) {
throw new BadRequestException('重置令牌无效或已过期');
}
// 加密新密码
const hashedPassword = await bcrypt.hash(newPassword, 12);
await this.prisma.user.update({
where: { id: user.id },
data: {
password: hashedPassword,
passwordResetToken: null,
passwordResetExpires: null,
},
});
}
async logout(userId: string): Promise<void> {
// 在实际应用中,你可能需要将令牌加入黑名单
// 这里只是一个占位符实现
console.log(`用户 ${userId} 已登出`);
}
private async generateTokens(user: User): Promise<AuthTokens> {
const payload = {
sub: user.id,
email: user.email,
role: user.role,
};
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('jwt.secret'),
expiresIn: this.configService.get<string>('jwt.expiresIn'),
}),
this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('jwt.refreshSecret'),
expiresIn: this.configService.get<string>('jwt.refreshExpiresIn'),
}),
]);
return {
accessToken,
refreshToken,
expiresIn: this.getTokenExpirationTime(),
};
}
private getTokenExpirationTime(): number {
const expiresIn = this.configService.get<string>('jwt.expiresIn');
// 简单解析过期时间(如 '1d' -> 86400 秒)
if (expiresIn.endsWith('d')) {
return parseInt(expiresIn) * 24 * 60 * 60;
} else if (expiresIn.endsWith('h')) {
return parseInt(expiresIn) * 60 * 60;
} else if (expiresIn.endsWith('m')) {
return parseInt(expiresIn) * 60;
}
return parseInt(expiresIn);
}
}
🎮 实现认证控制器
typescript
// src/modules/auth/auth.controller.ts
import {
Controller,
Post,
Body,
UseGuards,
HttpCode,
HttpStatus,
Get,
Request,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { ForgotPasswordDto, ResetPasswordDto } from './dto/reset-password.dto';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { Public } from './decorators/public.decorator';
import { CurrentUser } from './decorators/current-user.decorator';
import { User } from '@prisma/client';
@ApiTags('认证')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('register')
@ApiOperation({ summary: '用户注册' })
@ApiResponse({
status: 201,
description: '注册成功',
schema: {
type: 'object',
properties: {
user: { $ref: '#/components/schemas/UserEntity' },
tokens: {
type: 'object',
properties: {
accessToken: { type: 'string' },
refreshToken: { type: 'string' },
expiresIn: { type: 'number' },
}
}
}
}
})
@ApiResponse({ status: 400, description: '请求参数错误' })
@ApiResponse({ status: 409, description: '用户已存在' })
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
@Public()
@UseGuards(LocalAuthGuard)
@Post('login')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '用户登录' })
@ApiResponse({
status: 200,
description: '登录成功',
schema: {
type: 'object',
properties: {
user: { $ref: '#/components/schemas/UserEntity' },
tokens: {
type: 'object',
properties: {
accessToken: { type: 'string' },
refreshToken: { type: 'string' },
expiresIn: { type: 'number' },
}
}
}
}
})
@ApiResponse({ status: 401, description: '邮箱或密码错误' })
async login(@Body() loginDto: LoginDto, @Request() req) {
return this.authService.login(loginDto);
}
@Public()
@Post('refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '刷新访问令牌' })
@ApiResponse({
status: 200,
description: '刷新成功',
schema: {
type: 'object',
properties: {
accessToken: { type: 'string' },
refreshToken: { type: 'string' },
expiresIn: { type: 'number' },
}
}
})
@ApiResponse({ status: 401, description: '刷新令牌无效或已过期' })
async refreshTokens(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshTokens(refreshTokenDto);
}
@Public()
@Post('forgot-password')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '忘记密码' })
@ApiResponse({ status: 204, description: '密码重置邮件已发送' })
async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
return this.authService.forgotPassword(forgotPasswordDto);
}
@Public()
@Post('reset-password')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '重置密码' })
@ApiResponse({ status: 204, description: '密码重置成功' })
@ApiResponse({ status: 400, description: '重置令牌无效或已过期' })
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
return this.authService.resetPassword(resetPasswordDto);
}
@ApiBearerAuth()
@Post('logout')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '用户登出' })
@ApiResponse({ status: 204, description: '登出成功' })
async logout(@CurrentUser('id') userId: string) {
return this.authService.logout(userId);
}
@ApiBearerAuth()
@Get('profile')
@ApiOperation({ summary: '获取当前用户信息' })
@ApiResponse({
status: 200,
description: '获取成功',
type: 'UserEntity'
})
@ApiResponse({ status: 401, description: '未授权' })
async getProfile(@CurrentUser() user: User) {
return user;
}
}
🔧 配置认证模块
typescript
// src/modules/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RolesGuard } from './guards/roles.guard';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('jwt.secret'),
signOptions: {
expiresIn: configService.get<string>('jwt.expiresIn'),
},
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
JwtAuthGuard,
RolesGuard,
],
exports: [AuthService, JwtAuthGuard, RolesGuard],
})
export class AuthModule {}
🌐 配置全局认证
1. 更新主模块
typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { UsersModule } from './modules/users/users.module';
import { AuthModule } from './modules/auth/auth.module';
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
import configuration from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
isGlobal: true,
envFilePath: '.env',
}),
PrismaModule,
UsersModule,
AuthModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard, // 全局启用 JWT 认证
},
],
})
export class AppModule {}
2. 更新应用控制器
typescript
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from './prisma/prisma.service';
import { AppService } from './app.service';
import { Public } from './modules/auth/decorators/public.decorator';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly configService: ConfigService,
private readonly prismaService: PrismaService,
) {}
@Public()
@Get()
getHello(): string {
return this.appService.getHello();
}
@Public()
@Get('health')
async getHealth() {
const dbHealthy = await this.prismaService.isHealthy();
return {
status: 'ok',
timestamp: new Date().toISOString(),
database: dbHealthy ? 'connected' : 'disconnected',
environment: this.configService.get('app.environment'),
};
}
}
🔐 添加权限控制示例
1. 更新用户控制器添加权限控制
typescript
// src/modules/users/users.controller.ts (更新部分方法)
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
HttpCode,
HttpStatus,
UseGuards,
ParseUUIDPipe,
ForbiddenException,
} from '@nestjs/common';
import { UserRole } from '@prisma/client';
import { Roles } from '../auth/decorators/roles.decorator';
import { RolesGuard } from '../auth/guards/roles.guard';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Public } from '../auth/decorators/public.decorator';
@ApiTags('用户管理')
@Controller('users')
@UseGuards(RolesGuard) // 启用角色守卫
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Roles(UserRole.ADMIN) // 只有管理员可以创建用户
@Post()
@ApiOperation({ summary: '创建用户(管理员)' })
async create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> {
return this.usersService.create(createUserDto);
}
@Roles(UserRole.ADMIN, UserRole.MODERATOR) // 管理员和版主可以查看用户列表
@Get()
@ApiOperation({ summary: '获取用户列表(管理员/版主)' })
async findAll(@Query() queryDto: QueryUserDto) {
return this.usersService.findAll(queryDto);
}
@Get('me') // 获取当前用户信息,无需特殊权限
@ApiOperation({ summary: '获取当前用户信息' })
async getCurrentUser(@CurrentUser() user: User): Promise<UserEntity> {
return new UserEntity(user);
}
@Get(':id')
@ApiOperation({ summary: '根据ID获取用户信息' })
async findOne(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() currentUser: User,
): Promise<UserEntity> {
// 用户只能查看自己的信息,管理员和版主可以查看所有用户
if (
currentUser.id !== id &&
!['ADMIN', 'MODERATOR'].includes(currentUser.role)
) {
throw new ForbiddenException('无权访问其他用户信息');
}
const user = await this.usersService.findById(id);
if (!user) {
throw new NotFoundException('用户不存在');
}
return user;
}
@Patch('me') // 更新当前用户信息
@ApiOperation({ summary: '更新当前用户信息' })
async updateCurrentUser(
@CurrentUser('id') userId: string,
@Body() updateUserDto: UpdateUserDto,
): Promise<UserEntity> {
return this.usersService.update(userId, updateUserDto);
}
@Roles(UserRole.ADMIN) // 只有管理员可以更新其他用户
@Patch(':id')
@ApiOperation({ summary: '更新用户信息(管理员)' })
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() updateUserDto: UpdateUserDto,
): Promise<UserEntity> {
return this.usersService.update(id, updateUserDto);
}
@Patch('me/password') // 修改当前用户密码
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '修改当前用户密码' })
async changeCurrentUserPassword(
@CurrentUser('id') userId: string,
@Body() changePasswordDto: ChangePasswordDto,
): Promise<void> {
return this.usersService.changePassword(userId, changePasswordDto);
}
@Roles(UserRole.ADMIN) // 只有管理员可以更新用户状态
@Patch(':id/status')
@ApiOperation({ summary: '更新用户状态(管理员)' })
async updateStatus(
@Param('id', ParseUUIDPipe) id: string,
@Body('status') status: UserStatus,
): Promise<UserEntity> {
return this.usersService.updateStatus(id, status);
}
@Roles(UserRole.ADMIN) // 只有管理员可以删除用户
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '删除用户(管理员)' })
async remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
return this.usersService.remove(id);
}
}
✅ 测试认证系统
1. 启动应用
bash
npm run start:dev
2. 测试认证 API
用户注册
bash
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"username": "testuser",
"password": "Password123!",
"firstName": "Test",
"lastName": "User"
}'
用户登录
bash
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "Password123!"
}'
使用访问令牌访问受保护的端点
bash
# 替换 YOUR_ACCESS_TOKEN 为实际的访问令牌
curl -X GET http://localhost:3000/auth/profile \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
刷新访问令牌
bash
curl -X POST http://localhost:3000/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "YOUR_REFRESH_TOKEN"
}'
忘记密码
bash
curl -X POST http://localhost:3000/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com"
}'
重置密码
bash
curl -X POST http://localhost:3000/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "RESET_TOKEN_FROM_EMAIL",
"newPassword": "NewPassword123!"
}'
3. 测试权限控制
访问需要管理员权限的端点(应该返回 403)
bash
curl -X GET http://localhost:3000/users \
-H "Authorization: Bearer USER_ACCESS_TOKEN"
使用管理员账户访问
bash
# 首先创建管理员账户(通过数据库或种子数据)
curl -X GET http://localhost:3000/users \
-H "Authorization: Bearer ADMIN_ACCESS_TOKEN"
🔧 常见问题和解决方案
1. JWT 密钥配置
确保在 .env
文件中配置了强密钥:
bash
JWT_SECRET=your-very-long-and-secure-secret-key-at-least-32-characters
JWT_REFRESH_SECRET=your-very-long-and-secure-refresh-secret-key-at-least-32-characters
2. CORS 配置
如果前端需要访问 API,在 main.ts
中配置 CORS:
typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
// 启用 CORS
app.enableCors({
origin: configService.get('cors.origin'),
credentials: configService.get('cors.credentials'),
});
// 全局验证管道
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
const port = configService.get('app.port');
await app.listen(port);
console.log(`应用运行在: http://localhost:${port}`);
}
bootstrap();
🎉 小结
在本章中,我们完成了:
- ✅ 实现了完整的 JWT 认证系统
- ✅ 创建了登录、注册、刷新令牌功能
- ✅ 实现了密码重置机制
- ✅ 创建了认证守卫和权限控制
- ✅ 配置了全局认证保护
- ✅ 添加了基于角色的访问控制
在下一章中,我们将开发文章管理模块,实现博客系统的核心功能。