Skip to content
On this page

NestJS 安全最佳实践 (Security Best Practices)

NestJS 应用程序的安全性是构建生产就绪应用程序的关键要素。本指南涵盖了在 NestJS 应用程序中实施的各种安全措施和最佳实践,以保护应用程序免受常见攻击和漏洞的影响。

基础安全概念

NestJS 安全实践涉及:

  1. 认证 - 验证用户身份
  2. 授权 - 控制访问权限
  3. 输入验证 - 防止恶意输入
  4. 数据保护 - 保护敏感数据
  5. 通信安全 - 加密数据传输

输入验证和净化

使用管道进行输入验证

typescript
// create-user.dto.ts
import { IsString, IsEmail, IsNumberString, MinLength, MaxLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsNumberString()
  @MinLength(10)
  @MaxLength(15)
  phone: string;
}

// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      const messages = errors.map(error => Object.values(error.constraints)).flat();
      throw new BadRequestException(messages);
    }
    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

防止 NoSQL 注入

typescript
// 使用白名单验证查询参数
import { Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class QueryValidator {
  validateFindQuery(query: any) {
    // 只允许特定的查询字段
    const allowedFields = ['name', 'email', 'createdAt'];
    const invalidFields = Object.keys(query).filter(field => !allowedFields.includes(field));
    
    if (invalidFields.length > 0) {
      throw new BadRequestException(`Invalid query fields: ${invalidFields.join(', ')}`);
    }
    
    // 防止 NoSQL 操作符注入
    const noSqlOperators = ['$ne', '$gt', '$lt', '$regex', '$where', '$expr'];
    for (const [key, value] of Object.entries(query)) {
      if (typeof value === 'object') {
        const operators = Object.keys(value).filter(op => noSqlOperators.includes(op));
        if (operators.length > 0) {
          throw new BadRequestException(`NoSQL operator not allowed: ${operators.join(', ')}`);
        }
      }
    }
    
    return query;
  }
}

认证系统

JWT 认证

typescript
// 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';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    // 验证用户是否存在且未被禁用
    if (!payload.isActive) {
      throw new UnauthorizedException('User account is disabled');
    }
    return { userId: payload.sub, username: payload.username };
  }
}
typescript
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '60m' },
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

会话认证

typescript
// session.module.ts
import * as session from 'express-session';
import * as passport from 'passport';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';

@Module({
  // 模块配置
})
export class SessionModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(
        session({
          secret: process.env.SESSION_SECRET,
          resave: false,
          saveUninitialized: false,
          cookie: {
            secure: process.env.NODE_ENV === 'production', // HTTPS only
            httpOnly: true, // 防止 XSS
            maxAge: 24 * 60 * 60 * 1000, // 24小时
          },
        }),
        passport.initialize(),
        passport.session(),
      )
      .forRoutes('*');
  }
}

授权和权限控制

基于角色的访问控制 (RBAC)

typescript
// 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', [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (!requiredRoles) {
      return true;
    }
    
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// 在控制器中使用
@Controller('admin')
@UseGuards(RolesGuard)
export class AdminController {
  @Get('users')
  @Roles('admin', 'moderator')
  getUsers() {
    return 'Admin users data';
  }

  @Get('settings')
  @Roles('admin')
  getSettings() {
    return 'Admin settings';
  }
}

基于权限的访问控制

typescript
// permissions.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>('permissions', [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (!requiredPermissions) {
      return true;
    }
    
    const { user } = context.switchToHttp().getRequest();
    
    // 检查用户是否有必需的权限
    return requiredPermissions.every(permission => 
      user.permissions?.includes(permission)
    );
  }
}

// permissions.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Permissions = (...permissions: string[]) => 
  SetMetadata('permissions', permissions);

// 使用示例
@Put('user/:id')
@UseGuards(PermissionsGuard)
@Permissions('user:update', 'user:write')
updateUser(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
  return this.usersService.update(id, updateUserDto);
}

中间件安全

CORS 配置

typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.enableCors({
    origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
    credentials: true,
    optionsSuccessStatus: 204,
  });
  
  await app.listen(3000);
}
bootstrap();

Helmet 安全头

typescript
// main.ts
import * as helmet from 'helmet';
import { NestFactory } from '@nestjs/core';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 添加安全头
  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", 'data:', 'https:'],
      },
    },
    hsts: {
      maxAge: 31536000, // 一年
      includeSubDomains: true,
    },
    frameguard: {
      action: 'deny', // 防止点击劫持
    },
  }));
  
  await app.listen(3000);
}
bootstrap();

数据保护

密码哈希

typescript
// auth.service.ts
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  private readonly saltRounds = 12;

  async hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, this.saltRounds);
  }

  async validatePassword(
    plainTextPassword: string,
    hashedPassword: string,
  ): Promise<boolean> {
    return bcrypt.compare(plainTextPassword, hashedPassword);
  }
}

敏感数据加密

typescript
// encryption.service.ts
import * as crypto from 'crypto';

@Injectable()
export class EncryptionService {
  private readonly algorithm = 'aes-256-cbc';
  private readonly key = process.env.ENCRYPTION_KEY;

  encrypt(text: string): string {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher(this.algorithm, this.key);
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    return iv.toString('hex') + ':' + encrypted;
  }

  decrypt(encrypted: string): string {
    const parts = encrypted.split(':');
    const iv = Buffer.from(parts.shift(), 'hex');
    const encryptedText = parts.join(':');
    
    const decipher = crypto.createDecipher(this.algorithm, this.key);
    let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }
}

错误处理安全

安全的错误响应

typescript
// error.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Response } from 'express';

@Catch()
export class SecurityExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest();

    // 记录完整错误信息(仅记录,不发送给客户端)
    console.error('Security error:', exception);

    // 发送通用错误响应
    const status = exception instanceof HttpException 
      ? exception.getStatus() 
      : 500;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: status === 500 
        ? 'Internal server error' 
        : exception.message, // 只在非500错误时发送具体消息
    });
  }
}

速率限制

全局速率限制

typescript
// main.ts
import * as rateLimit from 'express-rate-limit';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 全局速率限制
  app.use(
    rateLimit({
      windowMs: 15 * 60 * 1000, // 15分钟
      max: 100, // 限制每个IP 15分钟内最多100个请求
      message: 'Too many requests from this IP, please try again later.',
      standardHeaders: true, // 返回速率限制相关的标准头
      legacyHeaders: false, // 不使用旧的 X-RateLimit-* 头
    }),
  );
  
  await app.listen(3000);
}
bootstrap();

路由特定速率限制

typescript
// auth.rate-limiter.ts
import * as rateLimit from 'express-rate-limit';

export const authRateLimit = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 5, // 限制每个IP在15分钟内最多5次登录尝试
  message: 'Too many login attempts, please try again later.',
  skipSuccessfulRequests: true, // 成功请求不计入限制
});

// 在中间件中使用
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    authRateLimit(req, res, next);
  }
}

SQL 注入防护

使用参数化查询

typescript
// 使用 TypeORM(默认使用参数化查询)
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async findByEmail(email: string): Promise<User> {
    // TypeORM 自动使用参数化查询
    return this.usersRepository.findOne({
      where: { email: email }, // 安全,不会导致SQL注入
    });
  }

  // 使用原生查询时确保参数化
  async findUsersWithNativeQuery(searchTerm: string): Promise<User[]> {
    return this.usersRepository.query(
      'SELECT * FROM user WHERE name LIKE ?', 
      [`%${searchTerm}%`] // 参数化查询
    );
  }
}

XSS 防护

输出编码

typescript
// utils/xss-sanitizer.ts
import * as xss from 'xss';

export function sanitizeHtml(input: string): string {
  return xss(input, {
    whiteList: {
      p: [],
      br: [],
      strong: [],
      em: [],
      u: [],
      h1: [], h2: [], h3: [], h4: [], h5: [], h6: [],
      ul: [], ol: [], li: [],
    },
  });
}

// 在服务中使用
@Injectable()
export class ContentService {
  createContent(content: string) {
    const sanitizedContent = sanitizeHtml(content);
    return this.contentRepository.create({ content: sanitizedContent });
  }
}

CSRF 防护

CSRF 保护中间件

typescript
// csrf.middleware.ts
import * as csurf from 'csurf';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class CsrfMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // 只对非 GET、HEAD、OPTIONS 请求启用 CSRF 保护
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
      return next();
    }
    
    csurf({
      cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
      },
    })(req, res, next);
  }
}

// 获取 CSRF 令牌的端点
@Controller('csrf')
export class CsrfController {
  @Get('token')
  getCsrfToken(@Req() req: any) {
    return { csrfToken: req.csrfToken() };
  }
}

安全头部配置

自定义安全中间件

typescript
// security.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class SecurityMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // 设置安全头
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('X-XSS-Protection', '1; mode=block');
    res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    
    // 移除敏感头
    res.removeHeader('X-Powered-By');
    
    next();
  }
}

日志和监控

安全事件日志

typescript
// security-logger.service.ts
import { Injectable } from '@nestjs/common';
import { Logger } from 'winston';

@Injectable()
export class SecurityLoggerService {
  constructor(private readonly logger: Logger) {}

  logAuthenticationFailure(username: string, ip: string, userAgent: string) {
    this.logger.warn('Authentication failure', {
      type: 'auth_failure',
      username,
      ip,
      userAgent,
      timestamp: new Date().toISOString(),
    });
  }

  logSuspiciousActivity(userId: string, action: string, ip: string) {
    this.logger.alert('Suspicious activity detected', {
      type: 'suspicious_activity',
      userId,
      action,
      ip,
      timestamp: new Date().toISOString(),
    });
  }

  logPermissionDenied(userId: string, resource: string, ip: string) {
    this.logger.warn('Permission denied', {
      type: 'permission_denied',
      userId,
      resource,
      ip,
      timestamp: new Date().toISOString(),
    });
  }
}

环境安全

环境变量管理

typescript
// config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
    username: process.env.DATABASE_USERNAME,
    password: process.env.DATABASE_PASSWORD,
    name: process.env.DATABASE_NAME,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '1d',
  },
  security: {
    saltRounds: parseInt(process.env.SALT_ROUNDS, 10) || 12,
    rateLimit: {
      windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10) || 15 * 60 * 1000,
      max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100,
    },
  },
});

// 使用配置
@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
      isGlobal: true,
    }),
  ],
})
export class AppModule {}

安全扫描和测试

安全测试示例

typescript
// security.test.ts
describe('Security Tests', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('should reject invalid input', async () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({
        name: '<script>alert("xss")</script>', // 恶意输入
        email: 'invalid-email', // 无效邮箱
        password: '123', // 弱密码
      })
      .expect(400); // 应该返回 400 错误
  });

  it('should require authentication', async () => {
    return request(app.getHttpServer())
      .get('/admin/users')
      .expect(401); // 未认证应该返回 401
  });

  it('should prevent SQL injection', async () => {
    return request(app.getHttpServer())
      .get('/users/search?q=user\' OR \'1\'=\'1')
      .expect(400); // 应该拒绝恶意查询
  });

  afterAll(async () => {
    await app.close();
  });
});

安全最佳实践清单

1. 认证和会话管理

  • 使用强密码策略
  • 实施多因素认证 (MFA)
  • 设置适当的会话超时
  • 使用安全的密码重置机制

2. 输入验证

  • 始终验证和净化用户输入
  • 使用白名单而非黑名单验证
  • 实施适当的输出编码

3. 访问控制

  • 实施最小权限原则
  • 使用 RBAC 或 ABAC 模型
  • 定期审查权限分配

4. 数据保护

  • 加密敏感数据存储
  • 使用 HTTPS/TLS 传输
  • 实施适当的数据备份和恢复

5. 配置管理

  • 安全的默认配置
  • 定期更新依赖
  • 禁用不必要的服务和端口

6. 错误处理

  • 不泄露敏感信息
  • 记录安全相关事件
  • 实施适当的监控

总结

NestJS 安全最佳实践是构建安全应用程序的基础。通过实施这些安全措施,可以保护应用程序免受常见的安全威胁和漏洞的影响。安全应该是开发过程的一部分,而不是事后的考虑。

安全的主要特点:

  • 多层防御策略
  • 输入验证和净化
  • 认证和授权机制
  • 数据保护措施
  • 安全头配置
  • 速率限制和防刷
  • 错误处理安全
  • 日志和监控
  • 环境安全管理