阶段五:实战项目
安全不是功能,是底线。一个未加固的API可能在几小时内被攻破——注入攻击、身份伪造、数据泄露、DDoS。本课覆盖OWASP API安全Top 10,从输入验证到HTTPS、从CORS到日志审计,构建纵深防御体系。
| # | 风险 | 说明 | 防护 |
|---|---|---|---|
| 1 | 对象级授权失效(BOLA) | 用户A访问用户B的数据 | 资源级权限检查 |
| 2 | 认证失效 | 弱密码、令牌泄露 | 强密码策略+MFA |
| 3 | 对象属性级授权失效 | 更新不该更新的字段 | 白名单字段过滤 |
| 4 | 资源消耗无限制 | 无分页、无限制查询 | 限流+分页+超时 |
| 5 | 功能级授权失效(BFLA) | 普通用户调用管理API | 角色+权限检查 |
| 6 | 批量赋值 | 请求体包含不该设置的字段 | 严格输入验证 |
| 7 | 安全配置错误 | CORS过宽、调试信息泄露 | 硬化配置 |
| 8 | 注入攻击 | SQL/NoSQL/命令注入 | 参数化查询+转义 |
| 9 | 资产管理不当 | 暴露调试端点、旧版本 | 资产盘点+下线 |
| 10 | 日志监控不足 | 攻击无痕迹 | 完整日志+告警 |
// security-hardened-api.js - 安全加固的API
const express = require('express');
const crypto = require('crypto');
const helmet = require('helmet');
const app = express();
// ===== 1. HTTP安全头(Helmet)=====
app.use(helmet());
// 等效于手动设置:
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 0
// Strict-Transport-Security: max-age=31536000
// Content-Security-Policy: default-src 'self'
// ===== 2. CORS配置 =====
const cors = require('cors');
app.use(cors({
origin: (origin, callback) => {
const allowed = ['https://app.example.com', 'https://admin.example.com'];
if (!origin || allowed.includes(origin)) callback(null, true);
else callback(new Error('CORS不允许'));
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Request-ID', 'X-RateLimit-*'],
credentials: true,
maxAge: 86400,
}));
// ===== 3. 请求体大小限制 =====
app.use(express.json({ limit: '100kb' })); // 防止大负载攻击
app.use(express.urlencoded({ extended: false, limit: '100kb' }));
// ===== 4. 请求ID =====
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || crypto.randomUUID();
res.setHeader('X-Request-ID', req.requestId);
next();
});
// ===== 5. 输入验证 =====
class InputValidator {
static sanitizeString(str) {
if (typeof str !== 'string') return str;
return str
.replace(/[<>]/g, '') // 移除尖括号(防XSS)
.replace(/&/g, '&') // 转义HTML实体
.trim();
}
static validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
static validateObjectId(id) {
return /^\d+$/.test(String(id)); // 只允许数字ID
}
// 防止批量赋值:白名单过滤
static filterFields(data, allowedFields) {
const filtered = {};
for (const field of allowedFields) {
if (data[field] !== undefined) filtered[field] = data[field];
}
return filtered;
}
// 防止原型污染
static preventPrototypePollution(data) {
const dangerous = ['__proto__', 'constructor', 'prototype'];
for (const key of Object.keys(data)) {
if (dangerous.includes(key)) delete data[key];
if (typeof data[key] === 'object' && data[key] !== null) {
InputValidator.preventPrototypePollution(data[key]);
}
}
return data;
}
}
// ===== 6. 认证中间件(增强版)=====
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ success: false, error: { code: 'AUTH_MISSING' } });
}
const token = authHeader.substring(7);
// 检查令牌黑名单
if (tokenBlacklist.has(token)) {
return res.status(401).json({ success: false, error: { code: 'TOKEN_REVOKED' } });
}
try {
const payload = verifyJWT(token);
if (payload.exp < Date.now() / 1000) {
return res.status(401).json({ success: false, error: { code: 'TOKEN_EXPIRED' } });
}
req.user = payload;
next();
} catch {
return res.status(401).json({ success: false, error: { code: 'TOKEN_INVALID' } });
}
}
function verifyJWT(token) {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Invalid token');
const [headerB64, payloadB64, signatureB64] = parts;
const expectedSig = crypto.createHmac('sha256', JWT_SECRET)
.update(`${headerB64}.${payloadB64}`).digest('base64url');
if (signatureB64 !== expectedSig) throw new Error('Invalid signature');
return JSON.parse(Buffer.from(payloadB64, 'base64').toString());
}
const tokenBlacklist = new Set();
// ===== 7. 授权中间件 =====
function authorize(resource, action) {
return (req, res, next) => {
if (!req.user) return res.status(401).json({ success: false, error: { code: 'AUTH_REQUIRED' } });
const role = req.user.role;
const permissions = {
admin: { users: ['read','write','delete'], orders: ['read','write','delete'], products: ['read','write','delete'] },
editor: { users: ['read'], orders: ['read','write'], products: ['read','write'] },
viewer: { users: ['read'], orders: ['read'], products: ['read'] },
};
const userPerms = permissions[role]?.[resource] || [];
if (!userPerms.includes(action)) {
// 记录未授权访问
securityLog('UNAUTHORIZED_ACCESS', { userId: req.user.sub, resource, action, ip: req.ip });
return res.status(403).json({ success: false, error: { code: 'FORBIDDEN', message: `需要 ${resource}:${action} 权限` } });
}
next();
};
}
// ===== 8. 资源级授权(防BOLA)=====
function checkResourceOwnership(resourceType) {
return async (req, res, next) => {
const resourceId = req.params.id;
const userId = req.user.sub;
// 管理员可访问所有资源
if (req.user.role === 'admin') return next();
// 普通用户只能访问自己的资源
const resource = dataStore.get(resourceType, resourceId);
if (!resource) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
if (resource.userId !== userId && resource.ownerId !== userId) {
securityLog('BOLA_ATTEMPT', { userId, resourceType, resourceId, ip: req.ip });
return res.status(403).json({ success: false, error: { code: 'FORBIDDEN' } });
}
next();
};
}
// ===== 9. 安全日志 =====
const securityLogs = [];
function securityLog(event, details) {
const entry = {
event,
timestamp: new Date().toISOString(),
...details,
};
securityLogs.push(entry);
console.warn(`⚠️ 安全事件: ${event}`, details);
}
// 请求审计日志
app.use((req, res, next) => {
if (req.user) {
securityLog('API_ACCESS', {
userId: req.user.sub,
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.headers['user-agent'],
});
}
next();
});
// ===== 10. 限流(防暴力破解)=====
const loginAttempts = new Map();
function loginRateLimit(req, res, next) {
const ip = req.ip;
const attempts = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 };
if (attempts.count >= 5 && Date.now() - attempts.lastAttempt < 300000) {
securityLog('BRUTE_FORCE_BLOCKED', { ip, attempts: attempts.count });
return res.status(429).json({ success: false, error: { code: 'TOO_MANY_ATTEMPTS', message: '登录尝试过多,请5分钟后重试' } });
}
next();
}
// ===== 路由 =====
const dataStore = {
users: new Map([
['1', { id: '1', name: '张三', email: 'zhang@example.com', role: 'admin', password: crypto.createHash('sha256').update('admin123+salt').digest('hex') }],
['2', { id: '2', name: '李四', email: 'li@example.com', role: 'viewer', password: crypto.createHash('sha256').update('viewer123+salt').digest('hex') }],
]),
get(type, id) { return this[type]?.get(id); },
};
// 登录
app.post('/api/auth/login', loginRateLimit, (req, res) => {
const { email, password } = InputValidator.filterFields(req.body, ['email', 'password']);
const ip = req.ip;
if (!email || !password) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR' } });
}
const user = [...dataStore.users.values()].find(u => u.email === email);
const hashedPassword = crypto.createHash('sha256').update(password + '+salt').digest('hex');
if (!user || user.password !== hashedPassword) {
const attempts = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 };
attempts.count++;
attempts.lastAttempt = Date.now();
loginAttempts.set(ip, attempts);
securityLog('LOGIN_FAILED', { email, ip, attempts: attempts.count });
return res.status(401).json({ success: false, error: { code: 'INVALID_CREDENTIALS' } });
}
// 重置尝试计数
loginAttempts.delete(ip);
const token = createJWT({ sub: user.id, role: user.role, exp: Math.floor(Date.now() / 1000) + 900 });
securityLog('LOGIN_SUCCESS', { userId: user.id, ip });
res.json({ success: true, data: { token, expiresIn: 900 } });
});
function createJWT(payload) {
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
const sig = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
return `${header}.${body}.${sig}`;
}
// 用户列表(需要授权)
app.get('/api/users', authenticate, authorize('users', 'read'), (req, res) => {
const users = [...dataStore.users.values()].map(u => {
const { password, ...safe } = u; // 永远不返回密码
return safe;
});
res.json({ success: true, data: users });
});
// 用户详情(防BOLA)
app.get('/api/users/:id', authenticate, checkResourceOwnership('users'), (req, res) => {
const user = dataStore.users.get(req.params.id);
if (!user) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
const { password, ...safe } = user;
res.json({ success: true, data: safe });
});
// 更新用户(防批量赋值)
app.patch('/api/users/:id', authenticate, checkResourceOwnership('users'), (req, res) => {
const allowedFields = ['name', 'email']; // 白名单!不能改role/password
const safeData = InputValidator.filterFields(req.body, allowedFields);
InputValidator.preventPrototypePollution(safeData);
const user = dataStore.users.get(req.params.id);
if (!user) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
Object.assign(user, safeData);
const { password, ...safe } = user;
res.json({ success: true, data: safe });
});
// 安全审计日志
app.get('/api/admin/security-logs', authenticate, authorize('users', 'delete'), (req, res) => {
res.json({ success: true, data: securityLogs.slice(-100) });
});
app.listen(3000, () => console.log('🛡️ 安全加固API运行在 http://localhost:3000'));
# 安装依赖
npm install express helmet cors
# 启动
node security-hardened-api.js
# 1. 登录获取令牌
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"zhang@example.com","password":"admin123"}' | jq -r '.data.token')
# 2. 用令牌访问受保护资源
curl -s http://localhost:3000/api/users \
-H "Authorization: Bearer $TOKEN" | jq .
# 3. 尝试批量赋值(改role字段)
curl -s -X PATCH http://localhost:3000/api/users/2 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"李四","role":"admin"}' | jq .
# → role字段被过滤,不会变更
# 4. 暴力破解保护(5次失败后锁定)
for i in $(seq 1 6); do
curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"zhang@example.com","password":"wrong"}' | jq -c '.error.code'
done
"INVALID_CREDENTIALS"
"INVALID_CREDENTIALS"
"INVALID_CREDENTIALS"
"INVALID_CREDENTIALS"
"INVALID_CREDENTIALS"
"TOO_MANY_ATTEMPTS"
使用Let's Encrypt证书配置HTTPS,设置HSTS头。
编写脚本自动检测API的常见安全问题:无认证端点、CORS配置、敏感数据泄露等。