API 安全最佳实践
API 安全最佳实践
概述
指导开发人员通过实现身份验证、授权、输入验证、速率限制和常见漏洞防护来构建安全的 API。本技能涵盖 REST、GraphQL 和 WebSocket API 的安全模式。
何时使用此技能
- 设计新的 API 端点时
- 为现有 API 增强安全性时
- 实现身份验证和授权时
- 防御 API 攻击(注入、DDoS 等)时
- 进行 API 安全审查时
- 准备安全审计时
- 实现速率限制(Rate Limiting)和流量控制(Throttling)时
- 在 API 中处理敏感数据时
工作原理
第一步:身份验证与授权
我将帮助您实现安全的身份验证:
- 选择身份验证方法(JWT, OAuth 2.0, API keys)
- 实现基于令牌(Token)的身份验证
- 设置基于角色的访问控制(RBAC)
- 实现安全的会话管理
- 实现多因素身份验证(MFA)
第二步:输入验证与清洗
防御注入攻击:
- 验证所有输入数据
- 清洗用户输入
- 使用参数化查询
- 实现请求模式(Schema)验证
- 防止 SQL 注入、XSS 和命令注入
第三步:速率限制与流量控制
防止滥用和 DDoS 攻击:
- 实现基于用户/IP 的速率限制
- 设置 API 流量控制
- 配置请求配额
- 优雅地处理速率限制错误
- 监控可疑活动
第四步:数据保护
保护敏感数据:
- 对传输中的数据加密(HTTPS/TLS)
- 对存储的敏感数据加密
- 实现正确的错误处理(防止数据泄露)
- 清洗错误消息
- 使用安全响应头
第五步:API 安全测试
验证安全实现情况:
- 测试身份验证和授权
- 进行渗透测试
- 检查常见漏洞(OWASP API Top 10)
- 验证输入处理
- 测试速率限制
示例
示例 1:实现 JWT 身份验证
## 安全 JWT 身份验证实现\身份验证流程
1. 用户使用凭据登录
2. 服务器验证凭据
3. 服务器生成 JWT 令牌
4. 客户端安全存储令牌
5. 客户端在每次请求中发送令牌
6. 服务器验证令牌实现
#### 1. 生成安全 JWT 令牌
\
\\javascript
// auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');// 登录端点
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// 验证输入
if (!email || !password) {
return res.status(400).json({
error: 'Email and password are required'
});
}
// 查找用户
const user = await db.user.findUnique({
where: { email }
});
if (!user) {
// 不要透露用户是否存在
return res.status(401).json({
error: 'Invalid credentials'
});
}
// 验证密码
const validPassword = await bcrypt.compare(
password,
user.passwordHash
);
if (!validPassword) {
return res.status(401).json({
error: 'Invalid credentials'
});
}
\\
// 生成 JWT token
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{
expiresIn: '1h',
issuer: 'your-app',
audience: 'your-app-users'
}
);
// 生成 refresh token
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
// 将 refresh token 存储在数据库中
await db.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
});
res.json({
token,
refreshToken,
expiresIn: 3600
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
error: 'An error occurred during login'
});
}
});
\
\\
#### 2. 验证 JWT Token (中间件)
\
\\javascript// middleware/auth.js
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
// 从请求头获取 token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
error: 'Access token required'
});
}
// 验证 token
jwt.verify(
token,
process.env.JWT_SECRET,
{
issuer: 'your-app',
audience: 'your-app-users'
},
(err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired'
});
}
return res.status(403).json({
error: 'Invalid token'
});
}
// 将用户信息绑定到请求对象
req.user = user;
next();
}
);
}
module.exports = { authenticateToken };
\\\
#### 3. 保护路由
\\\javascript
const { authenticateToken } = require('./middleware/auth');
// 受保护的路由
app.get('/api/user/profile', authenticateToken, async (req, res) => {
try {
const user = await db.user.findUnique({
where: { id: req.user.userId },
select: {
id: true,
email: true,
name: true,
// 不返回 passwordHash
}
});
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
\\\
#### 4. 实现 Token 刷新
\\\javascript
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
error: 'Refresh token required'
});
}
try {
// 验证 refresh token
const decoded = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET
);
// 检查 refresh token 是否存在于数据库中
const storedToken = await db.refreshToken.findFirst({
where: {
token: refreshToken,
userId: decoded.userId,
expiresAt: { gt: new Date() }
}
});
if (!storedToken) {
return res.status(403).json({
error: 'Invalid refresh token'
});
}
// 生成新的 access token
const user = await db.user.findUnique({
where: { id: decoded.userId }
});
const newToken = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({
token: newToken,
expiresIn: 3600
});
} catch (error) {
res.status(403).json({ error: 'Invalid refresh token' });
}
});
\\\\
: '1h' }
);
res.json({
token: newToken,
expiresIn: 3600
});
} catch (error) {
res.status(403).json({
error: 'Invalid refresh token'
});
}
});
\\
安全最佳实践
- ✅ 使用强 JWT 密钥(至少 256 位)
- ✅ 设置较短的过期时间(访问令牌 1 小时)
- ✅ 实现刷新令牌(Refresh Token)以维持长会话
- ✅ 将刷新令牌存储在数据库中(以便可撤销)
- ✅ 仅使用 HTTPS
- ✅ 不要在 JWT payload 中存储敏感数据
- ✅ 验证令牌的发行者(issuer)和受众(audience)
- ✅ 实现令牌黑名单机制以支持登出
### 示例 2:输入验证与防止 SQL 注入防止 SQL 注入与输入验证
问题所在
❌ 脆弱的代码:
\\\javascriptSELECT * FROM users WHERE id = '\${userId}'\
// 绝对不要这样做 - 存在 SQL 注入漏洞
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
// 危险:用户输入直接拼接到查询语句中
const query = \;
const user = await db.query(query);
res.json(user);
});
// 攻击示例:
// GET /api/users/1' OR '1'='1
// 将返回所有用户!
\\\
解决方案
#### 1. 使用参数化查询
\\\javascript\
// ✅ 安全:参数化查询
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
// 首先验证输入
if (!userId || !/^\d+$/.test(userId)) {
return res.status(400).json({
error: 'Invalid user ID'
});
}
// 使用参数化查询
const user = await db.query(
'SELECT id, email, name FROM users WHERE id = $1',
[userId]
);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json(user);
});
\\
#### 2. 使用带有正确转义的 ORM
\\\javascript\
// ✅ 安全:使用 Prisma ORM
app.get('/api/users/:id', async (req, res) => {
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
return res.status(400).json({
error: 'Invalid user ID'
});
}
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
// 不要选择敏感字段
}
});
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json(user);
});
\\
#### 3. 使用 Zod 实现请求验证
\\\javascript
const { z } = require('zod');
// 定义验证 Schema
const createUserSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain uppercase letter')
.regex(/[a-z]/, 'Password must contain lowercase letter')
.regex(/[0-9]/, 'Password must contain number'),
name: z.string()
.min(2, 'Name must be at least 2 characters')
.max(100, 'Name too long'),
age: z.number()
.int('Age must be an integer')
.min(18, 'Must be 18 or older')
.max(120, 'Invalid age')
.optional()
});
// 验证中间件
function validateRequest(schema) {
return (req, res, next) => {
try {
schema.parse(req.body);
next();
} catch (error) {
res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
};
}
// 使用验证
app.post('/api/users',
validateRequest(createUserSchema),
async (req, res) => {
// 此时输入已通过验证
cons
t { email, password, name, age } = req.body;\
// 对密码进行哈希处理
const passwordHash = await bcrypt.hash(password, 10);
// 创建用户
const user = await prisma.user.create({
data: {
email,
passwordHash,
name,
age
}
});
// 不要返回密码哈希
const { passwordHash: _, ...userWithoutPassword } = user;
res.status(201).json(userWithoutPassword);
}
);
\\#### 4. 对输出进行清洗以防止 XSS
\
\\javascript
const DOMPurify = require('isomorphic-dompurify');app.post('/api/comments', authenticateToken, async (req, res) => {
const { content } = req.body;
// 验证
if (!content || content.length > 1000) {
return res.status(400).json({
error: '评论内容无效'
});
}
// 清洗 HTML 以防止 XSS
const sanitizedContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
const comment = await prisma.comment.create({
data: {
content: sanitizedContent,
userId: req.user.userId
}
});
res.status(201).json(comment);
});
\\\验证清单
- [ ] 验证所有用户输入
- [ ] 使用参数化查询或 ORM
- [ ] 验证数据类型(字符串、数字、邮箱等)
- [ ] 验证数据范围(最小/最大长度、数值范围)
- [ ] 清洗 HTML 内容
- [ ] 对特殊字符进行转义
- [ ] 验证文件上传(类型、大小、内容)
- [ ] 使用白名单而非黑名单
示例 3:速率限制与 DDoS 防护
## 实现速率限制 (Rate Limiting)
为什么需要速率限制?
- 防止暴力破解攻击
- 防御 DDoS 攻击
- 防止 API 被滥用
- 确保公平使用
- 降低服务器成本
使用 Express Rate Limit 实现
\\\javascript
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
// 创建 Redis 客户端
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
// 通用 API 速率限制
const apiLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:api:'
}),
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个时间窗口最多 100 次请求
message: {
error: '请求过多,请稍后再试',
retryAfter: 900 // 秒
},
standardHeaders: true, // 在响应头中返回速率限制信息
legacyHeaders: false,
// 自定义键生成器(根据用户 ID 或 IP)
keyGenerator: (req) => {
return req.user?.userId || req.ip;
}
});
// 针对身份验证端点的严格速率限制
const authLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:auth:'
}),
windowMs: 15 * 60 * 1000, // 15 分钟
max: 5, // 每 15 分钟仅允许 5 次登录尝试
skipSuccessfulRequests: true, // 不计算成功的登录请求
message: {
error: '登录尝试过多,请稍后再试',
retryAfter: 900
}
});
// 应用速率限制器
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
// 针对高开销操作的自定义速率限制器
const expensiveLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 小时
max: 10, // 每小时 10 次请求
message: {
error: '该操作已超过速率限制'
}
});
app.post('/api/reports/generate',
authenticateToken,
expensiveLimiter,
async (req, res) => {
// 高开销操作...
sive operation
}
);
\\
\
进阶:针对用户的速率限制
\\\javascriptrl:user:\${user.userId}\
// 根据用户等级设置不同的限制
function createTieredRateLimiter() {
const limits = {
free: { windowMs: 60 * 60 * 1000, max: 100 },
pro: { windowMs: 60 * 60 * 1000, max: 1000 },
enterprise: { windowMs: 60 * 60 * 1000, max: 10000 }
};
return async (req, res, next) => {
const user = req.user;
const tier = user?.tier || 'free';
const limit = limits[tier];
const key = \;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, limit.windowMs / 1000);
}
if (current > limit.max) {
return res.status(429).json({
error: 'Rate limit exceeded',
limit: limit.max,
remaining: 0,
reset: await redis.ttl(key)
});
}
// 设置速率限制响应头
res.set({
'X-RateLimit-Limit': limit.max,
'X-RateLimit-Remaining': limit.max - current,
'X-RateLimit-Reset': await redis.ttl(key)
});
next();
};
}
app.use('/api/', authenticateToken, createTieredRateLimiter());
\\\
使用 Helmet 进行 DDoS 防护
\\\javascript
const helmet = require('helmet');
app.use(helmet({
// 内容安全策略 (CSP)
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
// 防止点击劫持
frameguard: { action: 'deny' },
// 隐藏 X-Powered-By 响应头
hidePoweredBy: true,
// 防止 MIME 类型嗅探
noSniff: true,
// 启用 HSTS
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
\\\
速率限制响应头
\\\\
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1640000000
Retry-After: 900
\\
## 最佳实践
✅ 推荐做法
- 全站使用 HTTPS - 绝不要通过 HTTP 发送敏感数据
- 实现身份验证 - 对受保护的端点要求身份验证
- 验证所有输入 - 不要信任任何用户输入
- 使用参数化查询 - 防止 SQL 注入
- 实现速率限制 - 防御暴力破解和 DDoS 攻击
- 对密码进行哈希处理 - 使用 bcrypt 且 salt rounds $\ge$ 10
- 使用短有效期令牌 - JWT 访问令牌应快速过期
- 正确配置 CORS - 仅允许可信的源
- 记录安全事件 - 监控可疑活动
- 保持依赖更新 - 定期更新软件包
- 使用安全响应头 - 部署 Helmet.js
- 清理错误消息 - 避免泄露敏感信息
❌ 避免做法
- 不要以明文存储密码 - 务必对密码进行哈希处理
- 不要使用弱密钥 - 使用强随机的 JWT 密钥
- 不要盲目信任用户输入 - 始终进行验证和清理
- 不要暴露堆栈跟踪 - 在生产环境中隐藏错误详情
- 不要使用字符串拼接构建 SQL - 使用参数化查询
- 不要在 JWT 中存储敏感数据 - JWT 并非加密存储
- 不要忽略安全更新 - 定期更新依赖项
- 不要使用默认凭据 - 修改所有默认密码
- 不要完全禁用 CORS - 应当进行正确配置
- 不要记录敏感数据 - 对日志进行脱敏处理
常见陷阱
问题:JWT 密钥在代码中泄露
症状: JWT 密钥被硬编码或提交到了 Git 仓库
解决方案:
\\// ✅ 正确做法
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is required');
}
// 生成强密钥
// node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
### 问题:密码强度要求过低
症状: 用户可以使用像 "password123" 这样简单的弱密码
解决方案:const passwordSchema = z.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[a-z]/, 'Must contain lowercase letter')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
// 或使用密码强度评估库
const zxcvbn = require('zxcvbn');
const result = zxcvbn(password);
if (result.score < 3) {
return res.status(400).json({
error: 'Password too weak',
suggestions: result.feedback.suggestions
});
}
### 问题:缺失权限检查
症状: 用户可以访问其不应访问的资源
解决方案:// ❌ 错误做法:仅检查身份验证(Authentication)
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
await prisma.post.delete({ where: { id: req.params.id } });
res.json({ success: true });
});
// ✅ 正确做法:同时检查身份验证和权限授权(Authorization)
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
const post = await prisma.post.findUnique({
where: { id: req.params.id }
});
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// 检查用户是否为文章所有者或管理员
if (post.userId !== req.user.userId && req.user.role !== 'admin') {
return res.status(403).json({
error: 'Not authorized to delete this post'
});
}
await prisma.post.delete({ where: { id: req.params.id } });
res.json({ success: true });
});
### 问题:错误信息过于详细
症状: 错误消息泄露了系统内部细节
解决方案:// ❌ 错误做法:暴露数据库细节
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({ data: req.body });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
// 错误示例: "Unique constraint failed on the fields: (
email)"}
});
// ✅ 正确做法:使用通用错误消息
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({ data: req.body });
res.json(user);
} catch (error) {
console.error('User creation error:', error); // 记录完整错误日志
if (error.code === 'P2002') {
return res.status(400).json({
error: 'Email already exists'
});
}
res.status(500).json({
error: 'An error occurred while creating user'
});
}
});
``
安全检查清单
身份验证与授权
- [ ] 实现强身份验证(JWT, OAuth 2.0)
- [ ] 所有端点均使用 HTTPS
- [ ] 使用 bcrypt 对密码进行哈希处理(salt rounds >= 10)
- [ ] 实现 Token 过期机制
- [ ] 添加 Refresh Token 机制
- [ ] 对每个请求验证用户授权
- [ ] 实现基于角色的访问控制 (RBAC)
输入验证
- [ ] 验证所有用户输入
- [ ] 使用参数化查询或 ORM
- [ ] 对 HTML 内容进行清洗(Sanitize)
- [ ] 验证文件上传
- [ ] 实现请求 Schema 验证
- [ ] 使用白名单而非黑名单
速率限制 (Rate Limiting)
& DDoS 防护- [ ] 实现基于用户/IP 的速率限制 (Rate Limiting)
- [ ] 为认证接口设置更严格的限制
- [ ] 使用 Redis 实现分布式速率限制
- [ ] 返回正确的速率限制响应头
- [ ] 实现请求节流 (Request Throttling)
数据保护
- [ ] 所有流量均使用 HTTPS/TLS
- [ ] 对静态存储的敏感数据进行加密
- [ ] 不在 JWT 中存储敏感数据
- [ ] 对错误消息进行脱敏处理
- [ ] 实现正确的 CORS 配置
- [ ] 使用安全响应头 (Helmet.js)
监控与日志
- [ ] 记录安全事件日志
- [ ] 监控可疑活动
- [ ] 为认证失败尝试设置告警
- [ ] 追踪 API 使用模式
- [ ] 不要记录敏感数据
OWASP API 安全 Top 10
1. 失效的对象级授权 (BOLA) - 始终验证用户是否有权访问该资源
2. 失效的身份认证 - 实现强身份认证机制
3. 失效的对象属性级授权 - 验证用户可访问的具体属性
4. 无限制的资源消耗 - 实现速率限制和配额管理
5. 失效的功能级授权 - 为每个功能验证用户角色
6. 无限制的敏感业务流访问 - 保护关键业务工作流
7. 服务端请求伪造 (SSRF) - 验证并过滤 URL
8. 安全配置错误 - 遵循安全最佳实践并配置安全响应头
9. 不恰当的资产管理 - 对所有 API 接口进行文档化和安全加固
10. 不安全的 API 消费 - 验证来自第三方 API 的数据
相关技能
- @ethical-hacking-methodology
- 安全测试视角
- @sql-injection-testing
- SQL 注入测试
- @xss-html-injection
- XSS 漏洞测试
- @broken-authentication
- 身份认证漏洞
- @backend-dev-guidelines
- 后端开发标准
- @systematic-debugging` - 安全问题系统化调试
附加资源
---
专业提示: 安全并非一次性任务 —— 请定期审计 API,保持依赖库更新,并及时关注新出现的漏洞!
局限性
- 仅在任务明确符合上述范围时使用此技能。
- 不要将输出结果视为针对特定环境的验证、测试或专家评审的替代方案。
- 如果缺少必要的输入、权限、安全边界或验收标准,请停止并请求澄清。