Skip to content

自定义装饰器:如何创建 @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() 和守卫,我们可以实现:

  1. 简化代码:通过装饰器直接注入常用数据
  2. 提高可读性:使代码意图更加明确
  3. 增强类型安全:通过 TypeScript 提供完整的类型支持
  4. 实现复杂逻辑:结合守卫和元数据实现复杂的注入逻辑

创建自定义装饰器的关键步骤:

  1. 使用 createParamDecorator 创建参数装饰器
  2. ExecutionContext 中提取所需数据
  3. 结合守卫进行数据预处理和验证
  4. 使用 @SetMetadata() 添加元数据支持
  5. 实现相应的守卫来处理元数据

通过合理使用自定义装饰器,我们可以:

  1. 减少重复代码
  2. 提高开发效率
  3. 增强应用的安全性
  4. 创建更加优雅的 API 接口

在下一篇文章中,我们将探讨微服务通信:ClientProxy 如何封装 TCP/RMQ/Kafka,了解 emit() vs send() 的区别以及事件驱动与请求响应模式的区别。