安全最佳实践
学习目标
- 了解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版本控制
互动问答
什么是OWASP Top 10?为什么它很重要?
OWASP Top 10是由Open Web Application Security Project (OWASP)维护的Web应用最常见安全风险列表。它很重要因为:
- 帮助开发者了解最常见的安全漏洞
- 提供了防范这些漏洞的最佳实践
- 是安全测试和审计的重要参考
- 帮助组织建立安全开发流程
- 是许多安全合规要求的基础
什么是JWT?它的安全优势和劣势是什么?
JWT(JSON Web Token)是一种基于JSON的开放标准,用于在各方之间安全地传输信息。
优势:
- 无状态,便于水平扩展
- 包含过期时间,自动失效
- 可以包含自定义声明
- 支持多种签名算法
劣势:
- 一旦签发,无法在服务端撤销
- 令牌可能被盗用
- 令牌大小可能较大
- 需要安全存储密钥
如何实现安全的密码存储?
安全存储密码的最佳实践:
- 使用bcrypt、Argon2等现代哈希算法
- 为每个密码添加唯一的盐值
- 使用适当的工作因子(哈希计算时间)
- 定期提醒用户更新密码
- 实现密码强度检查
- 限制登录尝试次数,防止暴力破解
什么是CORS?如何安全配置CORS?
CORS(Cross-Origin Resource Sharing)是一种机制,允许服务器指示浏览器允许从不同源(域、协议或端口)的请求。
安全配置CORS的方法:
- 明确指定允许的源,而不是使用通配符(*)
- 只允许必要的HTTP方法
- 只允许必要的HTTP头
- 谨慎使用credentials选项
- 考虑使用预检请求(OPTIONS)验证
如何进行安全的API速率限制?
实现API速率限制的方法:
- 基于IP地址限制请求频率
- 基于用户ID限制认证用户的请求频率
- 使用Redis等分布式缓存存储计数器
- 实现滑动窗口算法或令牌桶算法
- 返回适当的HTTP状态码(429 Too Many Requests)
- 提供速率限制相关的响应头
总结
安全是NestJS应用开发中不可忽视的重要方面。通过本教程的学习,你应该能够:
- 了解OWASP Top 10安全风险及其防范措施
- 实现安全的认证和授权系统
- 对输入数据进行有效的验证和清理
- 安全存储密码和敏感数据
- 配置应用的安全设置
- 扫描和测试应用的安全性
- 设计和实现安全的API
安全是一个持续的过程,需要开发者保持警惕并不断学习最新的安全实践。通过遵循本教程中的最佳实践,你可以显著提高NestJS应用的安全性,保护用户数据和系统资源。