NestJS守卫 (Guards)
学习目标
- 理解守卫在NestJS中的作用和地位
- 掌握守卫的创建和基本使用方法
- 学会实现认证守卫和授权守卫
- 理解角色基础访问控制的实现原理
- 能够应用守卫保护API端点
核心知识点
1. 守卫概念
守卫是NestJS中用于权限控制的组件,它们在请求处理管道中的位置如下:
- 客户端发送请求
- 中间件处理请求
- 守卫验证权限
- 管道处理数据
- 控制器处理请求
- 服务执行业务逻辑
- 拦截器处理响应
- 异常过滤器处理异常
- 响应返回给客户端
守卫的主要作用:
- 验证用户身份
- 检查用户权限
- 保护API端点
- 基于角色或策略进行访问控制
2. 守卫的创建
创建守卫需要实现CanActivate接口:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
// 实现权限验证逻辑
return true;
}
}3. 守卫的应用
控制器级别
使用@UseGuards()装饰器在控制器级别应用守卫:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
@Controller('cats')
@UseGuards(AuthGuard)
export class CatsController {
@Get()
findAll() {
return 'This action returns all cats';
}
}方法级别
使用@UseGuards()装饰器在方法级别应用守卫:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
return 'This action returns all cats';
}
@Get(':id')
@UseGuards(AuthGuard)
findOne() {
return 'This action returns a cat';
}
}全局级别
在应用程序级别注册全局守卫:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './auth.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new AuthGuard());
await app.listen(3000);
}
bootstrap();4. 认证守卫实现
创建一个基于JWT的认证守卫:
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
// 将用户信息添加到请求对象中
request.user = payload;
} catch {
throw new UnauthorizedException('Invalid token');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}5. 角色守卫实现
创建一个基于角色的授权守卫:
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 从元数据中获取所需角色
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true; // 如果没有指定角色,则允许访问
}
const request = context.switchToHttp().getRequest();
const user = request.user;
// 检查用户是否拥有所需角色
const hasRole = requiredRoles.some((role) => user?.roles?.includes(role));
if (!hasRole) {
throw new ForbiddenException('Insufficient permissions');
}
return true;
}
}6. 角色装饰器
创建一个自定义装饰器,用于指定路由所需的角色:
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);7. 组合使用
组合使用认证守卫和角色守卫:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
@Controller('admin')
@UseGuards(AuthGuard, RolesGuard)
export class AdminController {
@Get()
@Roles('admin')
getDashboard() {
return 'Admin dashboard';
}
@Get('users')
@Roles('admin', 'moderator')
getUsers() {
return 'User list';
}
}8. 守卫的依赖注入
守卫可以通过依赖注入系统注入其他服务:
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization;
return this.authService.validateToken(token);
}
}9. 守卫链
可以同时应用多个守卫,它们会按照注册顺序执行:
@Controller('protected')
@UseGuards(AuthGuard, RolesGuard, ThrottlerGuard)
export class ProtectedController {
// 控制器方法
}10. 上下文类型
守卫可以处理不同类型的上下文,不仅仅是HTTP请求:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class WsAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// WebSocket上下文
if (context.getType() === 'ws') {
const client = context.switchToWs().getClient();
// 实现WebSocket认证逻辑
return true;
}
// HTTP上下文
if (context.getType() === 'http') {
const request = context.switchToHttp().getRequest();
// 实现HTTP认证逻辑
return true;
}
return false;
}
}实践案例分析
案例:基于角色的访问控制系统
需求分析
我们需要创建一个基于角色的访问控制系统,包括:
- 用户认证(基于JWT)
- 角色授权(管理员、普通用户)
- 保护不同级别的API端点
- 提供清晰的错误信息
实现步骤
- 创建认证服务
- 创建JWT模块
- 创建认证守卫
- 创建角色守卫和装饰器
- 创建受保护的控制器
- 测试访问控制
代码实现
1. 创建认证服务
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
// 模拟用户数据
private users = [
{ id: 1, username: 'admin', password: 'admin123', roles: ['admin'] },
{ id: 2, username: 'user', password: 'user123', roles: ['user'] },
];
constructor(private jwtService: JwtService) {}
async validateUser(username: string, password: string) {
const user = this.users.find(u => u.username === username && u.password === password);
if (!user) {
return null;
}
const { password: pass, ...result } = user;
return result;
}
async login(user: any) {
const payload = { username: user.username, sub: user.id, roles: user.roles };
return {
access_token: this.jwtService.sign(payload),
};
}
async validateToken(token: string) {
try {
const payload = this.jwtService.verify(token, {
secret: process.env.JWT_SECRET || 'secret_key',
});
return payload;
} catch {
return null;
}
}
}2. 创建JWT模块
// jwt.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET || 'secret_key',
signOptions: { expiresIn: '1h' },
}),
],
exports: [JwtModule],
})
export class JwtAuthModule {}3. 创建认证守卫
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
const user = await this.authService.validateToken(token);
if (!user) {
throw new UnauthorizedException('Invalid token');
}
// 将用户信息添加到请求对象中
request.user = user;
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}4. 创建角色守卫和装饰器
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new ForbiddenException('User not authenticated');
}
const hasRole = requiredRoles.some((role) => user.roles.includes(role));
if (!hasRole) {
throw new ForbiddenException('Insufficient permissions');
}
return true;
}
}5. 创建认证控制器
// auth.controller.ts
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
async login(@Body() loginDto: { username: string; password: string }) {
const user = await this.authService.validateUser(loginDto.username, loginDto.password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return this.authService.login(user);
}
}6. 创建受保护的控制器
// users.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
@Controller('users')
@UseGuards(AuthGuard, RolesGuard)
export class UsersController {
// 所有认证用户都可以访问
@Get('profile')
getProfile(@Body() req: any) {
return req.user;
}
// 只有管理员可以访问
@Get('all')
@Roles('admin')
getAllUsers() {
return [
{ id: 1, username: 'admin' },
{ id: 2, username: 'user' },
];
}
// 只有管理员可以创建用户
@Post()
@Roles('admin')
createUser(@Body() user: { username: string; password: string }) {
return { id: 3, ...user };
}
}7. 创建模块
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtAuthModule } from './jwt.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { AuthGuard } from './auth.guard';
import { RolesGuard } from './roles.guard';
@Module({
imports: [JwtAuthModule],
providers: [AuthService, AuthGuard, RolesGuard],
controllers: [AuthController],
exports: [AuthService, AuthGuard, RolesGuard],
})
export class AuthModule {}// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { AuthModule } from './auth.module';
@Module({
imports: [AuthModule],
controllers: [UsersController],
})
export class UsersModule {}// app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [AuthModule, UsersModule],
})
export class AppModule {}测试结果
1. 用户登录
请求:
POST /auth/login
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}响应:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}2. 访问个人资料(所有认证用户)
请求:
GET /users/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...响应:
{
"username": "admin",
"sub": 1,
"roles": ["admin"]
}3. 访问用户列表(仅管理员)
请求:
GET /users/all
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...响应:
[
{ "id": 1, "username": "admin" },
{ "id": 2, "username": "user" }
]4. 普通用户尝试访问管理员资源
请求:
POST /auth/login
Content-Type: application/json
{
"username": "user",
"password": "user123"
}响应:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}请求:
GET /users/all
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...响应:
{
"statusCode": 403,
"message": "Insufficient permissions",
"error": "Forbidden"
}5. 未认证用户尝试访问受保护资源
请求:
GET /users/profile响应:
{
"statusCode": 401,
"message": "No token provided",
"error": "Unauthorized"
}代码解析
认证服务:
- 实现了用户验证和JWT令牌生成功能
- 使用
@nestjs/jwt模块处理JWT相关操作
守卫实现:
AuthGuard:验证JWT令牌并提取用户信息RolesGuard:检查用户是否拥有所需角色
装饰器:
@Roles()装饰器:用于指定路由所需的角色
控制器:
AuthController:处理用户登录UsersController:提供受保护的用户相关API
模块结构:
- 清晰的模块划分,便于维护和扩展
- 正确的依赖注入配置
互动思考问题
思考:守卫和中间件的区别是什么?它们各自的使用场景是什么?
讨论:在实际应用中,如何设计一个灵活的角色权限系统?除了基于角色的访问控制,还有哪些访问控制模型?
实践:尝试创建一个基于策略的守卫,根据不同的业务规则进行访问控制。
挑战:如何实现一个基于权限的细粒度访问控制系统,允许对单个资源进行权限管理?
扩展:了解NestJS的
@nestjs/passport模块,思考它如何与守卫配合使用来实现更复杂的认证策略。
小结
本集我们学习了NestJS守卫的核心概念和使用方法,包括:
- 守卫的基本概念和作用
- 守卫的创建和注册方法
- 认证守卫的实现
- 角色守卫和装饰器的创建
- 守卫链的使用
- 不同上下文类型的处理
通过实践案例,我们创建了一个完整的基于角色的访问控制系统,展示了如何使用守卫保护API端点并实现权限控制。守卫是NestJS实现安全访问控制的重要组件,它使得权限验证更加集中和可管理。
在下一集中,我们将学习NestJS的拦截器(Interceptors),了解如何使用拦截器处理请求和响应,实现响应格式化、日志记录等功能。