Appearance
NestJS 安全最佳实践 (Security Best Practices)
NestJS 应用程序的安全性是构建生产就绪应用程序的关键要素。本指南涵盖了在 NestJS 应用程序中实施的各种安全措施和最佳实践,以保护应用程序免受常见攻击和漏洞的影响。
基础安全概念
NestJS 安全实践涉及:
- 认证 - 验证用户身份
- 授权 - 控制访问权限
- 输入验证 - 防止恶意输入
- 数据保护 - 保护敏感数据
- 通信安全 - 加密数据传输
输入验证和净化
使用管道进行输入验证
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 安全最佳实践是构建安全应用程序的基础。通过实施这些安全措施,可以保护应用程序免受常见的安全威胁和漏洞的影响。安全应该是开发过程的一部分,而不是事后的考虑。
安全的主要特点:
- 多层防御策略
- 输入验证和净化
- 认证和授权机制
- 数据保护措施
- 安全头配置
- 速率限制和防刷
- 错误处理安全
- 日志和监控
- 环境安全管理