API安全防护与鉴权机制:构建安全可靠的RESTful API
引言 API安全是现代应用开发中的核心议题。随着微服务架构的普及,API作为系统间的通信桥梁,其安全性直接影响整个系统的安全。本文将全面讲解API安全的各个方面,从认证授权到防护策略。 一、身份认证机制 1.1 JWT(JSON Web Token) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 // JWT服务实现 import jwt from 'jsonwebtoken' import bcrypt from 'bcrypt' interface JWTPayload { userId: string email: string role: string } class AuthService { private readonly JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key' private readonly JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'your-refresh-secret' private readonly JWT_EXPIRES_IN = '15m' private readonly JWT_REFRESH_EXPIRES_IN = '7d' // 生成访问令牌 generateAccessToken(payload: JWTPayload): string { return jwt.sign(payload, this.JWT_SECRET, { expiresIn: this.JWT_EXPIRES_IN, issuer: 'myapp.com', audience: 'myapp-api' }) } // 生成刷新令牌 generateRefreshToken(payload: JWTPayload): string { return jwt.sign( { userId: payload.userId }, this.JWT_REFRESH_SECRET, { expiresIn: this.JWT_REFRESH_EXPIRES_IN } ) } // 验证访问令牌 verifyAccessToken(token: string): JWTPayload | null { try { return jwt.verify(token, this.JWT_SECRET) as JWTPayload } catch (error) { return null } } // 验证刷新令牌 verifyRefreshToken(token: string): { userId: string } | null { try { return jwt.verify(token, this.JWT_REFRESH_SECRET) as { userId: string } } catch (error) { return null } } // 密码哈希 async hashPassword(password: string): Promise<string> { return bcrypt.hash(password, 12) } // 密码验证 async comparePassword(password: string, hash: string): Promise<boolean> { return bcrypt.compare(password, hash) } // 登录流程 async login(email: string, password: string) { const user = await this.findUserByEmail(email) if (!user) { throw new Error('User not found') } const isValid = await this.comparePassword(password, user.passwordHash) if (!isValid) { throw new Error('Invalid password') } const payload: JWTPayload = { userId: user.id, email: user.email, role: user.role } return { accessToken: this.generateAccessToken(payload), refreshToken: this.generateRefreshToken(payload), expiresIn: 15 * 60 // 15分钟 } } // 刷新令牌 async refreshTokens(refreshToken: string) { const payload = this.verifyRefreshToken(refreshToken) if (!payload) { throw new Error('Invalid refresh token') } const user = await this.findUserById(payload.userId) if (!user) { throw new Error('User not found') } const jwtPayload: JWTPayload = { userId: user.id, email: user.email, role: user.role } return { accessToken: this.generateAccessToken(jwtPayload), refreshToken: this.generateRefreshToken(jwtPayload) } } private async findUserByEmail(email: string) { // 数据库查询实现 return null } private async findUserById(userId: string) { // 数据库查询实现 return null } } export const authService = new AuthService() 1.2 Express中间件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 // 认证中间件 import { Request, Response, NextFunction } from 'express' declare global { namespace Express { interface Request { user?: { userId: string email: string role: string } } } } // JWT认证中间件 export function authenticate(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Unauthorized', message: 'Missing or invalid authorization header' }) } const token = authHeader.substring(7) const payload = authService.verifyAccessToken(token) if (!payload) { return res.status(401).json({ error: 'Unauthorized', message: 'Invalid or expired token' }) } req.user = payload next() } // 角色授权中间件 export function authorize(...roles: string[]) { return (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return res.status(401).json({ error: 'Unauthorized', message: 'Authentication required' }) } if (!roles.includes(req.user.role)) { return res.status(403).json({ error: 'Forbidden', message: 'Insufficient permissions' }) } next() } } // 可选认证中间件(允许未登录用户访问) export function optionalAuthenticate(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization if (authHeader && authHeader.startsWith('Bearer ')) { const token = authHeader.substring(7) const payload = authService.verifyAccessToken(token) if (payload) { req.user = payload } } next() } // 使用示例 import express from 'express' const router = express.Router() // 公开路由 router.get('/public', (req, res) => { res.json({ message: 'Public endpoint' }) }) // 需要认证 router.get('/profile', authenticate, (req, res) => { res.json({ user: req.user }) }) // 需要特定角色 router.delete('/users/:id', authenticate, authorize('admin'), (req, res) => { res.json({ message: 'User deleted' }) }) // 可选认证(登录和未登录都可以访问) router.get('/content', optionalAuthenticate, (req, res) => { if (req.user) { res.json({ content: 'premium content', user: req.user }) } else { res.json({ content: 'free content' }) } }) 1.3 OAuth 2.0实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 // OAuth 2.0授权码模式 import crypto from 'crypto' import { OAuth2Client } from 'google-auth-library' class OAuthService { private clients = new Map() private authorizationCodes = new Map() private accessTokens = new Map() // 注册OAuth客户端 registerClient(clientId: string, redirectUris: string[], scopes: string[]) { const clientSecret = crypto.randomBytes(32).toString('hex') this.clients.set(clientId, { clientId, clientSecret, redirectUris, scopes }) return { clientId, clientSecret } } // 生成授权码 generateAuthorizationCode(userId: string, clientId: string, scopes: string[]): string { const code = crypto.randomBytes(32).toString('hex') this.authorizationCodes.set(code, { userId, clientId, scopes, expiresAt: Date.now() + 10 * 60 * 1000 // 10分钟过期 }) return code } // 验证授权码并生成访问令牌 async exchangeCodeForToken(code: string, clientId: string, clientSecret: string, redirectUri: string) { const authCode = this.authorizationCodes.get(code) if (!authCode) { throw new Error('Invalid authorization code') } if (authCode.clientId !== clientId) { throw new Error('Client ID mismatch') } if (Date.now() > authCode.expiresAt) { this.authorizationCodes.delete(code) throw new Error('Authorization code expired') } const client = this.clients.get(clientId) if (client.clientSecret !== clientSecret) { throw new Error('Invalid client secret') } if (!client.redirectUris.includes(redirectUri)) { throw new Error('Invalid redirect URI') } // 生成访问令牌 const accessToken = crypto.randomBytes(32).toString('hex') this.accessTokens.set(accessToken, { userId: authCode.userId, clientId, scopes: authCode.scopes, expiresAt: Date.now() + 60 * 60 * 1000 // 1小时过期 }) // 删除已使用的授权码 this.authorizationCodes.delete(code) return { access_token: accessToken, token_type: 'Bearer', expires_in: 3600, scope: authCode.scopes.join(' ') } } // 验证访问令牌 verifyAccessToken(accessToken: string) { const token = this.accessTokens.get(accessToken) if (!token) { return null } if (Date.now() > token.expiresAt) { this.accessTokens.delete(accessToken) return null } return token } // Google OAuth集成 async verifyGoogleToken(idToken: string) { const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID) const ticket = await client.verifyIdToken({ idToken, audience: process.env.GOOGLE_CLIENT_ID }) const payload = ticket.getPayload() return { userId: payload.sub, email: payload.email, name: payload.name, picture: payload.picture } } } export const oauthService = new OAuthService() 二、数据安全 2.1 敏感数据加密 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 // 加密服务 import crypto from 'crypto' class EncryptionService { private readonly ALGORITHM = 'aes-256-gcm' private readonly KEY_LENGTH = 32 private readonly IV_LENGTH = 16 private readonly AUTH_TAG_LENGTH = 16 private key: Buffer constructor() { // 从环境变量获取加密密钥 const keyString = process.env.ENCRYPTION_KEY || 'default-key-change-in-production' this.key = crypto.scryptSync(keyString, 'salt', this.KEY_LENGTH) } // 加密数据 encrypt(plaintext: string): string { const iv = crypto.randomBytes(this.IV_LENGTH) const cipher = crypto.createCipheriv(this.ALGORITHM, this.key, iv) let encrypted = cipher.update(plaintext, 'utf8', 'hex') encrypted += cipher.final('hex') const authTag = cipher.getAuthTag() // 组合:iv + authTag + encrypted return iv.toString('hex') + authTag.toString('hex') + encrypted } // 解密数据 decrypt(ciphertext: string): string { const iv = Buffer.from(ciphertext.slice(0, this.IV_LENGTH * 2), 'hex') const authTag = Buffer.from( ciphertext.slice(this.IV_LENGTH * 2, (this.IV_LENGTH + this.AUTH_TAG_LENGTH) * 2), 'hex' ) const encrypted = ciphertext.slice((this.IV_LENGTH + this.AUTH_TAG_LENGTH) * 2) const decipher = crypto.createDecipheriv(this.ALGORITHM, this.key, iv) decipher.setAuthTag(authTag) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') return decrypted } // 哈希数据(单向) hash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex') } // 生成HMAC generateHMAC(data: string, secret: string): string { return crypto.createHmac('sha256', secret).update(data).digest('hex') } // 验证HMAC verifyHMAC(data: string, hmac: string, secret: string): boolean { const computedHMAC = this.generateHMAC(data, secret) return crypto.timingSafeEqual( Buffer.from(computedHMAC, 'hex'), Buffer.from(hmac, 'hex') ) } } export const encryptionService = new EncryptionService() // 使用示例 const sensitiveData = 'user-ssn-123-45-6789' const encrypted = encryptionService.encrypt(sensitiveData) const decrypted = encryptionService.decrypt(encrypted) // 数据库中存储加密数据 async function saveUserWithEncryptedData(userData: any) { const encrypted = encryptionService.encrypt(userData.ssn) await db.users.create({ ...userData, ssn: encrypted, ssnHash: encryptionService.hash(userData.ssn) // 用于查询 }) } 2.2 签名验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // Webhook签名验证 import crypto from 'crypto' import { Request, Response } from 'express' function verifyWebhookSignature(req: Request, res: Response, next: NextFunction) { const signature = req.headers['x-webhook-signature'] as string const timestamp = req.headers['x-webhook-timestamp'] as string if (!signature || !timestamp) { return res.status(401).json({ error: 'Missing signature headers' }) } // 检查时间戳(防重放攻击) const now = Date.now() const webhookTime = parseInt(timestamp) if (Math.abs(now - webhookTime) > 5 * 60 * 1000) { // 5分钟窗口 return res.status(401).json({ error: 'Request too old' }) } // 生成预期签名 const payload = `${timestamp}.${req.body}` const expectedSignature = crypto .createHmac('sha256', process.env.WEBHOOK_SECRET!) .update(payload) .digest('hex') // 安全比较签名 const isValid = crypto.timingSafeEqual( Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex') ) if (!isValid) { return res.status(401).json({ error: 'Invalid signature' }) } next() } 三、防护策略 3.1 SQL注入防护 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 // 使用参数化查询 import { Pool } from 'pg' class UserRepository { private pool: Pool constructor(pool: Pool) { this.pool = pool } // 安全的查询 async findById(id: string) { const query = 'SELECT * FROM users WHERE id = $1' const result = await this.pool.query(query, [id]) return result.rows[0] } async findByEmail(email: string) { const query = 'SELECT * FROM users WHERE email = $1' const result = await this.pool.query(query, [email]) return result.rows[0] } // 使用查询构建器(如Knex.js) async findWithFilters(filters: any) { const query = this.pool .select('*') .from('users') if (filters.email) { query = query.where('email', filters.email) } if (filters.role) { query = query.where('role', filters.role) } if (filters.minAge) { query = query.where('age', '>=', filters.minAge) } return await query } // 使用ORM(如Prisma、TypeORM) async create(data: any) { // ORM自动处理参数化查询 return await this.pool.query( 'INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3) RETURNING *', [data.name, data.email, data.passwordHash] ) } } 3.2 XSS防护 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 // 输入验证和输出编码 import validator from 'validator' import xss from 'xss' class SecurityMiddleware { // 输入验证 static validateInput(req: Request, res: Response, next: NextFunction) { const { name, email, bio } = req.body // 验证和清理输入 if (name && !validator.isLength(name, { min: 1, max: 100 })) { return res.status(400).json({ error: 'Invalid name length' }) } if (email && !validator.isEmail(email)) { return res.status(400).json({ error: 'Invalid email format' }) } if (bio && !validator.isLength(bio, { max: 500 })) { return res.status(400).json({ error: 'Bio too long' }) } // 清理XSS if (name) req.body.name = xss(name) if (bio) req.body.bio = xss(bio) next() } // 设置CSP头 static setSecurityHeaders(req: Request, res: Response, next: NextFunction) { // 内容安全策略 res.setHeader('Content-Security-Policy', "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self'; " + "connect-src 'self' https://api.example.com; " + "frame-ancestors 'none';" ) // 其他安全头 res.setHeader('X-Content-Type-Options', 'nosniff') res.setHeader('X-Frame-Options', 'DENY') res.setHeader('X-XSS-Protection', '1; mode=block') res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin') res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()') next() } } 3.3 CORS配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // CORS配置 import express from 'express' import cors from 'cors' const app = express() // 生产环境CORS配置 const corsOptions = { origin: function (origin: string | undefined, callback: Function) { const allowedOrigins = [ 'https://example.com', 'https://www.example.com', 'https://app.example.com' ] // 允许无origin的请求(如移动应用、Postman) if (!origin) return callback(null, true) if (allowedOrigins.indexOf(origin) !== -1) { callback(null, true) } else { callback(new Error('Not allowed by CORS')) } }, credentials: true, // 允许携带cookie methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], exposedHeaders: ['X-Total-Count'], maxAge: 86400 // 预检请求缓存24小时 } app.use(cors(corsOptions)) // 或者针对特定路由 app.options('/api/*', cors(corsOptions)) app.get('/api/data', cors(corsOptions), (req, res) => { res.json({ data: 'sensitive data' }) }) 3.4 速率限制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 // API速率限制 import rateLimit from 'express-rate-limit' import RedisStore from 'rate-limit-redis' import Redis from 'ioredis' // 不同场景的速率限制 // 1. 通用API限制 const generalLimiter = rateLimit({ store: new RedisStore({ client: new Redis(process.env.REDIS_URL) }), windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 限制100次请求 standardHeaders: true, legacyHeaders: false, message: 'Too many requests from this IP, please try again later.', handler: (req, res) => { res.status(429).json({ error: 'Too many requests', message: 'Rate limit exceeded, please try again later.', retryAfter: 900 // 秒 }) } }) // 2. 登录限流(更严格) const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // 15分钟内最多5次登录尝试 skipSuccessfulRequests: true, // 成功的请求不计入限制 message: 'Too many login attempts, please try again later.' }) // 3. API密钥限流 const apiKeyLimiter = rateLimit({ store: new RedisStore({ client: new Redis(process.env.REDIS_URL), prefix: 'limiter:apikey:' }), windowMs: 60 * 1000, // 1分钟 max: 60, // 每分钟60次 keyGenerator: (req) => { return req.headers['x-api-key'] as string } }) // 应用限流 app.use('/api/', generalLimiter) app.post('/api/auth/login', loginLimiter) app.use('/api/v2/', apiKeyLimiter) // 基于用户的限流 async function getUserRateLimit(userId: string) { const user = await db.users.findById(userId) // 不同用户等级有不同限制 const limits = { free: { windowMs: 60 * 1000, max: 10 }, pro: { windowMs: 60 * 1000, max: 100 }, enterprise: { windowMs: 60 * 1000, max: 1000 } } return limits[user.plan] || limits.free } 四、安全最佳实践 4.1 安全配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // helmet安全头配置 import helmet from 'helmet' app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], } }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, noSniff: true, xssFilter: true, frameguard: { action: 'deny' } })) // 禁用不必要的头 app.disable('x-powered-by') 4.2 安全日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 // 安全审计日志 class SecurityAuditLogger { private auditLog: any constructor(auditLog: any) { this.auditLog = auditLog } logAuthenticationAttempt(userId: string, success: boolean, ip: string) { this.auditLog.create({ eventType: 'AUTH_ATTEMPT', userId, success, ip, timestamp: new Date(), userAgent: undefined }) } logAuthorizationAttempt(userId: string, resource: string, action: string, success: boolean) { this.auditLog.create({ eventType: 'AUTHZ_ATTEMPT', userId, resource, action, success, timestamp: new Date() }) } logDataAccess(userId: string, resourceType: string, resourceId: string) { this.auditLog.create({ eventType: 'DATA_ACCESS', userId, resourceType, resourceId, timestamp: new Date() }) } logSecurityEvent(eventType: string, details: any) { this.auditLog.create({ eventType, details, timestamp: new Date() }) } } // 使用示例 const securityLogger = new SecurityAuditLogger(auditLog) app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body const ip = req.ip try { const result = await authService.login(email, password) securityLogger.logAuthenticationAttempt(result.user.id, true, ip) res.json(result) } catch (error) { securityLogger.logAuthenticationAttempt(email, false, ip) res.status(401).json({ error: 'Invalid credentials' }) } }) 4.3 输入验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 // 使用Joi进行输入验证 import Joi from 'joi' // 验证schemas const schemas = { register: Joi.object({ name: Joi.string().min(2).max(50).required(), email: Joi.string().email().required(), password: Joi.string() .min(8) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) .required() .messages({ 'string.pattern.base': 'Password must contain uppercase, lowercase, number, and special character' }), confirmPassword: Joi.string().valid(Joi.ref('password')).required(), age: Joi.number().integer().min(13).max(120) }), createPost: Joi.object({ title: Joi.string().min(5).max(200).required(), content: Joi.string().min(10).max(10000).required(), tags: Joi.array().items(Joi.string().max(30)).max(10), published: Joi.boolean().default(false) }), updateProfile: Joi.object({ name: Joi.string().min(2).max(50), bio: Joi.string().max(500), website: Joi.string().uri(), avatar: Joi.string().uri() }) } // 验证中间件 function validate(schemaName: keyof typeof schemas) { return (req: Request, res: Response, next: NextFunction) => { const { error, value } = schemas[schemaName].validate(req.body, { abortEarly: false, stripUnknown: true }) if (error) { const errors = error.details.map(detail => ({ field: detail.path.join('.'), message: detail.message })) return res.status(400).json({ errors }) } req.body = value next() } } // 使用 app.post('/api/auth/register', validate('register'), authController.register) app.post('/api/posts', authenticate, validate('createPost'), postController.create) 总结 API安全是一个多层次的主题: ...