自定义装饰器:如何创建 @CurrentUser()
在 NestJS 应用中,装饰器是实现各种功能的重要机制。通过自定义装饰器,我们可以简化代码、提高可读性,并实现复杂的注入逻辑。其中,@CurrentUser() 是一个非常常见的自定义装饰器,用于直接注入当前认证用户的信息。本文将深入探讨如何创建自定义装饰器,特别是如何结合 @SetMetadata() 与守卫来实现上下文注入。
1. 自定义装饰器基础
1.1 什么是参数装饰器?
参数装饰器是用于装饰函数参数的特殊函数,可以用来从请求上下文中提取特定信息:
typescript
// 内置参数装饰器示例
@Controller('users')
export class UserController {
@Get(':id')
findOne(
@Param('id') id: string, // 从路径参数中提取
@Query('page') page: number, // 从查询参数中提取
@Body() createUserDto: CreateUserDto, // 从请求体中提取
@Headers('authorization') auth: string, // 从请求头中提取
) {
// 处理逻辑
}
}1.2 createParamDecorator 工厂函数
NestJS 提供了 createParamDecorator 工厂函数来创建自定义参数装饰器:
typescript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// 使用自定义装饰器
@Controller('users')
export class UserController {
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}
}2. 创建 @CurrentUser() 装饰器
2.1 基础实现
typescript
// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);2.2 带属性访问的实现
typescript
// 支持访问用户特定属性
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// 如果指定了属性,返回该属性的值
if (data && user) {
return user[data];
}
// 否则返回整个用户对象
return user;
},
);
// 使用示例
@Controller('users')
export class UserController {
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}
@Get('name')
getUserName(@CurrentUser('name') userName: string) {
return { name: userName };
}
@Get('id')
getUserId(@CurrentUser('id') userId: number) {
return { id: userId };
}
}3. 认证守卫与用户注入
3.1 认证守卫实现
typescript
// guards/auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../user/user.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// 提取并验证 JWT 令牌
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Authentication token required');
}
try {
const payload = this.jwtService.verify(token);
const user = await this.userService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException('User not found');
}
// 将用户信息附加到请求对象
request.user = user;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractToken(request: any): string | undefined {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return undefined;
}
return authHeader.substring(7);
}
}3.2 使用认证守卫
typescript
// 在控制器中使用认证守卫
@UseGuards(AuthGuard)
@Controller('users')
export class UserController {
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}
}4. 高级自定义装饰器
4.1 带验证的用户装饰器
typescript
// decorators/auth-user.decorator.ts
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';
export interface AuthUserOptions {
required?: boolean;
roles?: string[];
}
export const AuthUser = createParamDecorator(
(data: AuthUserOptions | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// 检查是否需要用户认证
if (data?.required !== false && !user) {
throw new UnauthorizedException('User authentication required');
}
// 检查角色权限
if (data?.roles && user) {
const hasRole = data.roles.some(role => user.roles?.includes(role));
if (!hasRole) {
throw new UnauthorizedException('Insufficient permissions');
}
}
return user;
},
);
// 使用示例
@Controller('admin')
export class AdminController {
@Get('profile')
@UseGuards(AuthGuard)
getProfile(@AuthUser({ roles: ['admin'] }) user: User) {
return user;
}
}4.2 带默认值的装饰器
typescript
// decorators/user-or-guest.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const UserOrGuest = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// 如果用户未认证,返回访客对象
if (!user) {
return {
id: null,
name: 'Guest',
roles: ['guest'],
};
}
return user;
},
);
// 使用示例
@Controller('content')
export class ContentController {
@Get('articles')
getArticles(@UserOrGuest() user: User) {
// 无论用户是否认证都可以访问
return this.articleService.getArticles(user);
}
}5. 结合 SetMetadata 的高级用法
5.1 权限元数据装饰器
typescript
// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// 使用示例
@Controller('admin')
export class AdminController {
@Get('users')
@Roles('admin', 'moderator')
getAllUsers() {
return this.userService.findAll();
}
}5.2 权限守卫实现
typescript
// guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } 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.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
// 如果没有权限要求,允许访问
if (!requiredRoles) {
return true;
}
// 获取当前用户
const { user } = context.switchToHttp().getRequest();
if (!user) {
return false;
}
// 检查用户是否具有所需权限
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// 组合使用
@UseGuards(AuthGuard, RolesGuard)
@Controller('admin')
export class AdminController {
@Get('users')
@Roles('admin')
@UseInterceptors(LoggingInterceptor)
getAllUsers(@CurrentUser() user: User) {
return this.userService.findAll();
}
}6. 自定义装饰器工厂
6.1 通用装饰器工厂
typescript
// utils/decorator-factory.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
interface DecoratorFactoryOptions {
source: 'user' | 'headers' | 'query' | 'params';
property?: string;
defaultValue?: any;
transform?: (value: any) => any;
}
export const createCustomDecorator = (options: DecoratorFactoryOptions) => {
return createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
let value: any;
switch (options.source) {
case 'user':
value = request.user;
break;
case 'headers':
value = request.headers;
break;
case 'query':
value = request.query;
break;
case 'params':
value = request.params;
break;
}
// 访问特定属性
if (options.property && value) {
value = value[options.property];
}
// 应用转换函数
if (options.transform && value !== undefined) {
value = options.transform(value);
}
// 返回默认值(如果值未定义)
if (value === undefined && options.defaultValue !== undefined) {
return options.defaultValue;
}
return value;
});
};
// 使用工厂创建装饰器
export const CurrentUserId = createCustomDecorator({
source: 'user',
property: 'id',
transform: (value) => parseInt(value, 10),
defaultValue: 0,
});
export const UserAgent = createCustomDecorator({
source: 'headers',
property: 'user-agent',
});6.2 使用自定义装饰器工厂
typescript
// 使用工厂创建的装饰器
@Controller('analytics')
export class AnalyticsController {
@Get('user-stats')
getUserStats(
@CurrentUserId() userId: number,
@UserAgent() userAgent: string,
) {
return this.analyticsService.getUserStats(userId, userAgent);
}
}7. 错误处理和类型安全
7.1 类型安全的装饰器
typescript
// 带类型定义的装饰器
export const CurrentUser = createParamDecorator<
undefined,
ExecutionContext,
User
>((data: undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
});
// 带属性访问的类型安全装饰器
export const CurrentUserProperty = createParamDecorator<
keyof User,
ExecutionContext,
any
>((data: keyof User, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user as User;
if (data && user) {
return user[data];
}
return user;
});7.2 错误处理装饰器
typescript
// 带错误处理的装饰器
export const SafeCurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
try {
const request = ctx.switchToHttp().getRequest();
return request.user || null;
} catch (error) {
// 记录错误但不中断请求处理
console.warn('Error in CurrentUser decorator:', error);
return null;
}
},
);8. 总结
自定义装饰器是 NestJS 中非常强大的功能,通过结合 createParamDecorator、@SetMetadata() 和守卫,我们可以实现:
- 简化代码:通过装饰器直接注入常用数据
- 提高可读性:使代码意图更加明确
- 增强类型安全:通过 TypeScript 提供完整的类型支持
- 实现复杂逻辑:结合守卫和元数据实现复杂的注入逻辑
创建自定义装饰器的关键步骤:
- 使用
createParamDecorator创建参数装饰器 - 从
ExecutionContext中提取所需数据 - 结合守卫进行数据预处理和验证
- 使用
@SetMetadata()添加元数据支持 - 实现相应的守卫来处理元数据
通过合理使用自定义装饰器,我们可以:
- 减少重复代码
- 提高开发效率
- 增强应用的安全性
- 创建更加优雅的 API 接口
在下一篇文章中,我们将探讨微服务通信:ClientProxy 如何封装 TCP/RMQ/Kafka,了解 emit() vs send() 的区别以及事件驱动与请求响应模式的区别。