安全最佳实践

学习目标

  • 了解OWASP Top 10安全风险及其防范措施
  • 掌握NestJS应用的认证和授权安全最佳实践
  • 学习输入验证和数据清理的方法
  • 理解密码存储和加密的安全实践
  • 掌握安全配置和环境变量管理
  • 了解漏洞扫描和安全测试工具的使用
  • 学习如何构建安全的API和防止常见攻击

核心知识点

OWASP Top 10安全风险

OWASP Top 10是Web应用最常见的安全风险列表:

  • 失效的访问控制:未正确实施访问控制,导致未授权用户访问敏感数据
  • 加密机制失效:敏感数据未加密或使用弱加密算法
  • 注入攻击:如SQL注入、NoSQL注入等
  • 不安全的设计:系统设计阶段的安全缺陷
  • 安全配置错误:默认配置、过度权限、错误的权限设置等
  • 易受攻击的第三方组件:使用有已知漏洞的依赖库
  • 身份认证与授权失效:认证机制薄弱,导致身份被冒用
  • 软件与数据完整性失效:无法确保代码和数据的完整性
  • 安全日志与监控不足:无法检测和响应安全事件
  • 服务端请求伪造:服务器被诱导向意外目标发送请求

认证与授权安全

认证和授权是应用安全的基础:

  • **认证(Authentication)**:验证用户身份
  • **授权(Authorization)**:验证用户是否有权限执行特定操作
  • 会话管理:安全管理用户会话
  • 多因素认证:增加认证的安全性
  • 单点登录:简化认证流程,提高安全性

输入验证与数据清理

输入验证是防止注入攻击的关键:

  • 客户端验证:提升用户体验,但不能替代服务端验证
  • 服务端验证:确保所有输入数据符合预期格式
  • 参数绑定验证:使用管道和装饰器验证请求参数
  • 数据清理:移除或转义潜在的恶意数据
  • 内容安全策略:限制可执行的脚本来源

密码存储与加密

安全存储用户密码和敏感数据:

  • 密码哈希:使用bcrypt等算法哈希存储密码
  • 加盐:为每个密码添加唯一盐值
  • 密钥管理:安全管理加密密钥
  • 传输加密:使用HTTPS保护数据传输
  • 敏感数据加密:加密存储敏感数据

安全配置

正确配置应用以提高安全性:

  • 环境变量管理:使用.env文件和配置模块管理敏感配置
  • 最小权限原则:授予应用和用户最小必要权限
  • 安全HTTP头:配置安全相关的HTTP头
  • CORS配置:正确配置跨域资源共享
  • 防止信息泄露:避免在响应中包含敏感信息

漏洞扫描与安全测试

定期扫描和测试应用安全性:

  • 静态代码分析:分析代码中的安全漏洞
  • 动态应用安全测试:模拟攻击测试应用
  • 依赖项扫描:检查依赖库的已知漏洞
  • 安全审计:定期进行安全审计
  • 渗透测试:模拟攻击者尝试入侵系统

实践案例

实现安全的认证系统

步骤1:使用bcrypt哈希密码

// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../users/user.entity';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
    private jwtService: JwtService,
  ) {}

  async register(registerDto: RegisterDto): Promise<{ accessToken: string }> {
    const { username, email, password } = registerDto;

    // 检查用户是否已存在
    const existingUser = await this.userRepository.findOne({
      where: [{ username }, { email }],
    });

    if (existingUser) {
      throw new UnauthorizedException('Username or email already exists');
    }

    // 哈希密码
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);

    // 创建用户
    const user = this.userRepository.create({
      username,
      email,
      password: hashedPassword,
    });

    await this.userRepository.save(user);

    // 生成JWT令牌
    const payload = { sub: user.id, username: user.username };
    const accessToken = this.jwtService.sign(payload);

    return { accessToken };
  }

  async login(loginDto: LoginDto): Promise<{ accessToken: string }> {
    const { email, password } = loginDto;

    // 查找用户
    const user = await this.userRepository.findOne({ where: { email } });

    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    // 验证密码
    const isPasswordValid = await bcrypt.compare(password, user.password);

    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    // 生成JWT令牌
    const payload = { sub: user.id, username: user.username };
    const accessToken = this.jwtService.sign(payload);

    return { accessToken };
  }
}

步骤2:配置JWT模块

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../users/user.entity';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { AuthController } from './auth.controller';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: {
          expiresIn: configService.get<string>('JWT_EXPIRES_IN'),
        },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

步骤3:实现JWT策略

// src/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../../users/user.entity';

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

  async validate(payload: any) {
    const user = await this.userRepository.findOne({ where: { id: payload.sub } });

    if (!user) {
      throw new UnauthorizedException('Invalid token');
    }

    return user;
  }
}

输入验证与数据清理

步骤1:使用DTO和验证管道

// src/users/dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  @IsString()
  username: string;

  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  @IsString()
  @MinLength(8)
  password: string;
}
// src/users/users.controller.ts
import { Controller, Post, Body, Get, Param, UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @UseGuards(JwtAuthGuard)
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }
}

步骤2:配置全局验证管道

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 配置全局验证管道
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 自动移除未在DTO中定义的属性
      forbidNonWhitelisted: true, // 当发现未在DTO中定义的属性时抛出错误
      transform: true, // 自动将请求参数转换为DTO类型
    }),
  );
  
  await app.listen(3000);
}
bootstrap();

安全配置与HTTP头

步骤1:配置安全HTTP头

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import * as helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 使用Helmet配置安全HTTP头
  app.use(helmet());
  
  // 配置内容安全策略
  app.use(
    helmet.contentSecurityPolicy({
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "trusted-scripts.com"],
        styleSrc: ["'self'", "trusted-styles.com"],
        imgSrc: ["'self'", "data:", "trusted-images.com"],
      },
    }),
  );
  
  // 配置全局验证管道
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
  
  await app.listen(3000);
}
bootstrap();

步骤2:配置CORS

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 配置CORS
  app.enableCors({
    origin: ["https://trusted-domain.com"], // 允许的域名
    methods: ["GET", "POST", "PUT", "DELETE"], // 允许的HTTP方法
    allowedHeaders: ["Content-Type", "Authorization"], // 允许的HTTP头
    credentials: true, // 允许携带凭证
  });
  
  await app.listen(3000);
}
bootstrap();

漏洞扫描与安全测试

步骤1:使用npm audit扫描依赖项

# 扫描依赖项漏洞
npm audit

# 自动修复漏洞
npm audit fix

# 检查过时的依赖项
npm outdated

步骤2:配置ESLint安全规则

// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: 'tsconfig.json',
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint/eslint-plugin', 'security'],
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:security/recommended',
  ],
  rules: {
    'security/detect-non-literal-fs-filename': 'error',
    'security/detect-unsafe-regex': 'warn',
    'security/detect-buffer-noassert': 'error',
    'security/detect-child-process': 'error',
    'security/detect-disable-mustache-escape': 'error',
    'security/detect-eval-with-expression': 'error',
    'security/detect-no-csrf-before-method-override': 'error',
    'security/detect-non-literal-regexp': 'error',
    'security/detect-non-literal-require': 'error',
    'security/detect-object-injection': 'warn',
    'security/detect-possible-timing-attacks': 'error',
    'security/detect-pseudoRandomBytes': 'error',
  },
};

代码示例

实现API速率限制

// src/common/guards/rate-limit.guard.ts
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/common';
import { Inject } from '@nestjs/common';

@Injectable()
export class RateLimitGuard implements CanActivate {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const ip = request.ip;
    const path = request.path;
    const key = `rate_limit:${ip}:${path}`;

    const current = await this.cacheManager.get<number>(key);

    if (current) {
      if (current >= 10) { // 限制每IP每分钟10个请求
        throw new HttpException('Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);
      }
      await this.cacheManager.set(key, current + 1, 60); // 60秒过期
    } else {
      await this.cacheManager.set(key, 1, 60); // 60秒过期
    }

    return true;
  }
}

实现安全的密码重置功能

// src/auth/auth.service.ts
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../users/user.entity';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { ResetPasswordDto } from './dto/reset-password.dto';
import { ForgotPasswordDto } from './dto/forgot-password.dto';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import { MailService } from '../mail/mail.service';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
    private jwtService: JwtService,
    private mailService: MailService,
  ) {}

  // 其他方法...

  async forgotPassword(forgotPasswordDto: ForgotPasswordDto): Promise<void> {
    const { email } = forgotPasswordDto;

    // 查找用户
    const user = await this.userRepository.findOne({ where: { email } });

    if (!user) {
      throw new BadRequestException('User not found');
    }

    // 生成密码重置令牌
    const resetToken = crypto.randomBytes(32).toString('hex');
    const resetTokenHash = await bcrypt.hash(resetToken, 10);

    // 保存重置令牌和过期时间
    user.resetToken = resetTokenHash;
    user.resetTokenExpiry = new Date(Date.now() + 3600000); // 1小时过期
    await this.userRepository.save(user);

    // 发送密码重置邮件
    const resetUrl = `https://example.com/reset-password?token=${resetToken}&email=${email}`;
    await this.mailService.sendResetPasswordEmail(email, resetUrl);
  }

  async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<void> {
    const { email, token, newPassword } = resetPasswordDto;

    // 查找用户
    const user = await this.userRepository.findOne({ where: { email } });

    if (!user) {
      throw new BadRequestException('User not found');
    }

    // 检查重置令牌是否存在且未过期
    if (!user.resetToken || !user.resetTokenExpiry) {
      throw new BadRequestException('Reset token not found');
    }

    if (user.resetTokenExpiry < new Date()) {
      throw new BadRequestException('Reset token has expired');
    }

    // 验证重置令牌
    const isTokenValid = await bcrypt.compare(token, user.resetToken);

    if (!isTokenValid) {
      throw new BadRequestException('Invalid reset token');
    }

    // 哈希新密码
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(newPassword, salt);

    // 更新密码并清除重置令牌
    user.password = hashedPassword;
    user.resetToken = null;
    user.resetTokenExpiry = null;
    await this.userRepository.save(user);
  }
}

实现防止SQL注入的查询

// src/users/user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
  ) {}

  // 安全的查询方法,使用参数化查询
  async searchUsers(query: string): Promise<User[]> {
    // 使用TypeORM的Like操作符,自动防止SQL注入
    return this.userRepository.find({
      where: [
        { username: Like(`%${query}%`) },
        { email: Like(`%${query}%`) },
      ],
    });
  }

  // 安全的原生SQL查询
  async getUserStatistics(): Promise<any> {
    // 使用参数化查询防止SQL注入
    const result = await this.userRepository.query(
      'SELECT COUNT(*) as userCount, MAX(created_at) as lastUserCreated FROM users WHERE active = ?',
      [true],
    );

    return result[0];
  }
}

实现安全的文件上传

// src/files/file-upload.utils.ts
import { extname } from 'path';
import { BadRequestException } from '@nestjs/common';

// 允许的文件类型
const allowedFileTypes = ['.jpg', '.jpeg', '.png', '.gif', '.pdf'];

// 文件大小限制(5MB)
const maxFileSize = 5 * 1024 * 1024;

export const imageFileFilter = (req, file, callback) => {
  // 检查文件扩展名
  const fileExt = extname(file.originalname).toLowerCase();
  if (!allowedFileTypes.includes(fileExt)) {
    return callback(new BadRequestException('Only image files are allowed!'), false);
  }
  callback(null, true);
};

export const fileSizeValidator = (req, file, callback) => {
  // 检查文件大小
  if (file.size > maxFileSize) {
    return callback(new BadRequestException('File size must be less than 5MB!'), false);
  }
  callback(null, true);
};

export const editFileName = (req, file, callback) => {
  // 生成安全的文件名
  const randomName = Array(32)
    .fill(null)
    .map(() => Math.round(Math.random() * 16).toString(16))
    .join('');
  const fileExt = extname(file.originalname).toLowerCase();
  callback(null, `${randomName}${fileExt}`);
};
// src/files/files.controller.ts
import { Controller, Post, UploadedFile, UseInterceptors, MaxFileSizeValidator, FileTypeValidator, ParseFilePipe } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { editFileName, imageFileFilter } from './file-upload.utils';

@Controller('files')
export class FilesController {
  @Post('upload')
  @UseInterceptors(
    FileInterceptor('file', {
      storage: diskStorage({
        destination: './uploads',
        filename: editFileName,
      }),
      fileFilter: imageFileFilter,
    }),
  )
  uploadFile(
    @UploadedFile(
      new ParseFilePipe({
        validators: [
          new MaxFileSizeValidator({ maxSize: 5 * 1024 * 1024 }),
          new FileTypeValidator({ fileType: /(jpg|jpeg|png|gif|pdf)$/ }),
        ],
      }),
    )
    file: Express.Multer.File,
  ) {
    return {
      filename: file.filename,
      path: `uploads/${file.filename}`,
    };
  }
}

常见问题与解决方案

1. 如何防止XSS攻击?

解决方案

  • 对用户输入进行验证和清理
  • 使用内容安全策略(CSP)限制脚本执行
  • 在前端使用模板引擎的自动转义功能
  • 对输出到HTML的内容进行HTML实体编码
  • 使用Helmet库配置安全HTTP头

2. 如何安全存储敏感配置?

解决方案

  • 使用环境变量存储敏感配置
  • 使用.env文件和配置模块管理环境变量
  • 不要将.env文件提交到版本控制系统
  • 使用密钥管理服务存储生产环境的敏感配置
  • 对配置进行加密存储

3. 如何防止CSRF攻击?

解决方案

  • 使用CSRF令牌验证
  • 验证Origin和Referer头
  • 对敏感操作使用POST请求而不是GET
  • 实现SameSite cookie属性
  • 使用双重提交Cookie模式

4. 如何安全使用第三方依赖?

解决方案

  • 定期更新依赖项到最新版本
  • 使用npm audit扫描依赖项漏洞
  • 限制依赖项的权限和访问范围
  • 审查第三方依赖的代码和安全性
  • 使用依赖锁定文件(lockfile)确保依赖版本一致性

5. 如何实现安全的API设计?

解决方案

  • 使用HTTPS保护API通信
  • 实现适当的认证和授权机制
  • 对所有输入进行验证
  • 限制API请求频率(速率限制)
  • 提供详细的API文档和安全指南
  • 实现API版本控制

互动问答

  1. 什么是OWASP Top 10?为什么它很重要?

    OWASP Top 10是由Open Web Application Security Project (OWASP)维护的Web应用最常见安全风险列表。它很重要因为:

    • 帮助开发者了解最常见的安全漏洞
    • 提供了防范这些漏洞的最佳实践
    • 是安全测试和审计的重要参考
    • 帮助组织建立安全开发流程
    • 是许多安全合规要求的基础
  2. 什么是JWT?它的安全优势和劣势是什么?

    JWT(JSON Web Token)是一种基于JSON的开放标准,用于在各方之间安全地传输信息。

    优势

    • 无状态,便于水平扩展
    • 包含过期时间,自动失效
    • 可以包含自定义声明
    • 支持多种签名算法

    劣势

    • 一旦签发,无法在服务端撤销
    • 令牌可能被盗用
    • 令牌大小可能较大
    • 需要安全存储密钥
  3. 如何实现安全的密码存储?

    安全存储密码的最佳实践:

    • 使用bcrypt、Argon2等现代哈希算法
    • 为每个密码添加唯一的盐值
    • 使用适当的工作因子(哈希计算时间)
    • 定期提醒用户更新密码
    • 实现密码强度检查
    • 限制登录尝试次数,防止暴力破解
  4. 什么是CORS?如何安全配置CORS?

    CORS(Cross-Origin Resource Sharing)是一种机制,允许服务器指示浏览器允许从不同源(域、协议或端口)的请求。

    安全配置CORS的方法:

    • 明确指定允许的源,而不是使用通配符(*)
    • 只允许必要的HTTP方法
    • 只允许必要的HTTP头
    • 谨慎使用credentials选项
    • 考虑使用预检请求(OPTIONS)验证
  5. 如何进行安全的API速率限制?

    实现API速率限制的方法:

    • 基于IP地址限制请求频率
    • 基于用户ID限制认证用户的请求频率
    • 使用Redis等分布式缓存存储计数器
    • 实现滑动窗口算法或令牌桶算法
    • 返回适当的HTTP状态码(429 Too Many Requests)
    • 提供速率限制相关的响应头

总结

安全是NestJS应用开发中不可忽视的重要方面。通过本教程的学习,你应该能够:

  1. 了解OWASP Top 10安全风险及其防范措施
  2. 实现安全的认证和授权系统
  3. 对输入数据进行有效的验证和清理
  4. 安全存储密码和敏感数据
  5. 配置应用的安全设置
  6. 扫描和测试应用的安全性
  7. 设计和实现安全的API

安全是一个持续的过程,需要开发者保持警惕并不断学习最新的安全实践。通过遵循本教程中的最佳实践,你可以显著提高NestJS应用的安全性,保护用户数据和系统资源。

« 上一篇 性能优化 下一篇 » 自定义模块开发