name: api-security-best-practices description: “实现安全的API设计模式,包括身份认证、授权、输入验证、速率限制和防护常见API漏洞” license: MIT metadata: version: “1.0.0” domain: architecture triggers: API安全, 身份认证, 授权, 输入验证, 速率限制, OWASP, JWT, OAuth role: 专家 scope: 实施 output-format: 代码 related-skills: api设计原则, api模式
API安全最佳实践
概述
指导开发者通过实现身份认证、授权、输入验证、速率限制和防护常见漏洞来构建安全API。本技能涵盖REST、GraphQL和WebSocket API的安全模式。
何时使用此技能
- 在设计新API端点时使用
- 在保护现有API时使用
- 在实现身份认证和授权时使用
- 在防护API攻击(如注入、DDoS等)时使用
- 在进行API安全审查时使用
- 在准备安全审计时使用
- 在实现速率限制和节流时使用
- 在处理API中的敏感数据时使用
工作原理
步骤1:身份认证与授权
我将帮助您实现安全身份认证:
- 选择身份认证方法(JWT、OAuth 2.0、API密钥)
- 实现基于令牌的身份认证
- 设置基于角色的访问控制(RBAC)
- 保护会话管理
- 实现多因素身份认证(MFA)
步骤2:输入验证与净化
防护注入攻击:
- 验证所有输入数据
- 净化用户输入
- 使用参数化查询
- 实现请求模式验证
- 防止SQL注入、XSS和命令注入
步骤3:速率限制与节流
防止滥用和DDoS攻击:
- 实现按用户/IP的速率限制
- 设置API节流
- 配置请求配额
- 优雅处理速率限制错误
- 监控可疑活动
步骤4:数据保护
保护敏感数据:
- 加密传输中的数据(HTTPS/TLS)
- 加密静态敏感数据
- 实现适当的错误处理(无数据泄露)
- 净化错误消息
- 使用安全头
步骤5: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: '邮箱和密码是必需的'
});
}
// 查找用户
const user = await db.user.findUnique({
where: { email }
});
if (!user) {
// 不透露用户是否存在
return res.status(401).json({
error: '无效凭据'
});
}
// 验证密码
const validPassword = await bcrypt.compare(
password,
user.passwordHash
);
if (!validPassword) {
return res.status(401).json({
error: '无效凭据'
});
}
// 生成JWT令牌
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'
}
);
// 生成刷新令牌
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
// 在数据库中存储刷新令牌
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('登录错误:', error);
res.status(500).json({
error: '登录过程中发生错误'
});
}
});
2. 验证JWT令牌(中间件)
// middleware/auth.js
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
// 从头获取令牌
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
error: '需要访问令牌'
});
}
// 验证令牌
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: '令牌已过期'
});
}
return res.status(403).json({
error: '无效令牌'
});
}
// 将用户附加到请求
req.user = user;
next();
}
);
}
module.exports = { authenticateToken };
3. 保护路由
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: '服务器错误' });
}
});
4. 实现令牌刷新
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
error: '需要刷新令牌'
});
}
try {
// 验证刷新令牌
const decoded = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET
);
// 检查刷新令牌是否存在于数据库中
const storedToken = await db.refreshToken.findFirst({
where: {
token: refreshToken,
userId: decoded.userId,
expiresAt: { gt: new Date() }
}
});
if (!storedToken) {
return res.status(403).json({
error: '无效刷新令牌'
});
}
// 生成新访问令牌
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: '无效刷新令牌'
});
}
});
安全最佳实践
- ✅ 使用强JWT密钥(至少256位)
- ✅ 设置短过期时间(访问令牌1小时)
- ✅ 实现刷新令牌以支持长会话
- ✅ 在数据库中存储刷新令牌(可撤销)
- ✅ 仅使用HTTPS
- ✅ 不在JWT负载中存储敏感数据
- ✅ 验证令牌发行者和受众
- ✅ 实现注销时的令牌黑名单
### 示例2:输入验证和SQL注入防护
```markdown
## 防止SQL注入和输入验证
### 问题
**❌ 易受攻击的代码:**
```javascript
// 永远不要这样做 - SQL注入漏洞
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
// 危险:用户输入直接用于查询
const query = `SELECT * FROM users WHERE id = '${userId}'`;
const user = await db.query(query);
res.json(user);
});
// 攻击示例:
// GET /api/users/1' OR '1'='1
// 返回所有用户!
解决方案
1. 使用参数化查询
// ✅ 安全:参数化查询
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
// 首先验证输入
if (!userId || !/^\d+$/.test(userId)) {
return res.status(400).json({
error: '无效用户ID'
});
}
// 使用参数化查询
const user = await db.query(
'SELECT id, email, name FROM users WHERE id = $1',
[userId]
);
if (!user) {
return res.status(404).json({
error: '用户未找到'
});
}
res.json(user);
});
2. 使用带有适当转义的ORM
// ✅ 安全:使用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: '无效用户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: '用户未找到'
});
}
res.json(user);
});
3. 使用Zod实现请求验证
const { z } = require('zod');
// 定义验证模式
const createUserSchema = z.object({
email: z.string().email('无效邮箱格式'),
password: z.string()
.min(8, '密码必须至少8个字符')
.regex(/[A-Z]/, '密码必须包含大写字母')
.regex(/[a-z]/, '密码必须包含小写字母')
.regex(/[0-9]/, '密码必须包含数字'),
name: z.string()
.min(2, '名称必须至少2个字符')
.max(100, '名称过长'),
age: z.number()
.int('年龄必须是整数')
.min(18, '必须18岁或以上')
.max(120, '无效年龄')
.optional()
});
// 验证中间件
function validateRequest(schema) {
return (req, res, next) => {
try {
schema.parse(req.body);
next();
} catch (error) {
res.status(400).json({
error: '验证失败',
details: error.errors
});
}
};
}
// 使用验证
app.post('/api/users',
validateRequest(createUserSchema),
async (req, res) => {
// 输入在此点已验证
const { 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
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防护
```markdown
## 实现速率限制
### 为什么需要速率限制?
- 防止暴力攻击
- 防护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) => {
// 昂贵操作
}
);
高级:按用户速率限制
// 基于用户层级的限制
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 = `rl:user:${user.userId}`;
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: '速率限制已超',
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());
DDoS防护与Helmet
const helmet = require('helmet');
app.use(helmet({
// 内容安全策略
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,盐轮数>=10
- **使用短生命周期令牌** - JWT访问令牌应快速过期
- **正确实现CORS** - 仅允许受信任源
- **记录安全事件** - 监控可疑活动
- **保持依赖更新** - 定期更新包
- **使用安全头** - 实现Helmet.js
- **净化错误消息** - 不泄露敏感信息
### ❌ 不应做事项
- **不要以明文存储密码** - 始终哈希密码
- **不要使用弱密钥** - 使用强随机JWT密钥
- **不要信任用户输入** - 始终验证和净化
- **不要暴露堆栈跟踪** - 在生产中隐藏错误详情
- **不要使用字符串拼接进行SQL查询** - 使用参数化查询
- **不要在JWT中存储敏感数据** - JWT未加密
- **不要忽略安全更新** - 定期更新依赖
- **不要使用默认凭据** - 更改所有默认密码
- **不要完全禁用CORS** - 正确配置它
- **不要记录敏感数据** - 净化日志
## 常见陷阱
### 问题:JWT密钥在代码中暴露
**症状:** JWT密钥硬编码或提交到Git
**解决方案:**
```javascript
// ❌ 错误
const JWT_SECRET = 'my-secret-key';
// ✅ 正确
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
throw new Error('JWT_SECRET环境变量是必需的');
}
// 生成强密钥
// node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
问题:弱密码要求
症状: 用户可以设置弱密码如“password123” 解决方案:
const passwordSchema = z.string()
.min(12, '密码必须至少12个字符')
.regex(/[A-Z]/, '必须包含大写字母')
.regex(/[a-z]/, '必须包含小写字母')
.regex(/[0-9]/, '必须包含数字')
.regex(/[^A-Za-z0-9]/, '必须包含特殊字符');
// 或使用密码强度库
const zxcvbn = require('zxcvbn');
const result = zxcvbn(password);
if (result.score < 3) {
return res.status(400).json({
error: '密码太弱',
suggestions: result.feedback.suggestions
});
}
问题:缺少授权检查
症状: 用户可以访问不应访问的资源 解决方案:
// ❌ 错误:仅检查身份认证
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
await prisma.post.delete({ where: { id: req.params.id } });
res.json({ success: true });
});
// ✅ 正确:检查身份认证和授权
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: '帖子未找到' });
}
// 检查用户是否拥有帖子或是管理员
if (post.userId !== req.user.userId && req.user.role !== 'admin') {
return res.status(403).json({
error: '无权删除此帖子'
});
}
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 });
// 错误:“唯一约束失败于字段:(`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('用户创建错误:', error); // 记录完整错误
if (error.code === 'P2002') {
return res.status(400).json({
error: '邮箱已存在'
});
}
res.status(500).json({
error: '创建用户时发生错误'
});
}
});
安全清单
身份认证与授权
- [ ] 实现强身份认证(JWT、OAuth 2.0)
- [ ] 对所有端点使用HTTPS
- [ ] 使用bcrypt哈希密码(盐轮数>=10)
- [ ] 实现令牌过期
- [ ] 添加刷新令牌机制
- [ ] 验证每个请求的用户授权
- [ ] 实现基于角色的访问控制(RBAC)
输入验证
- [ ] 验证所有用户输入
- [ ] 使用参数化查询或ORM
- [ ] 净化HTML内容
- [ ] 验证文件上传
- [ ] 实现请求模式验证
- [ ] 使用允许列表,而非阻止列表
速率限制与DDoS防护
- [ ] 实现按用户/IP的速率限制
- [ ] 为身份认证端点添加更严格限制
- [ ] 使用Redis进行分布式速率限制
- [ ] 返回适当的速率限制头
- [ ] 实现请求节流
数据保护
- [ ] 对所有流量使用HTTPS/TLS
- [ ] 加密静态敏感数据
- [ ] 不在JWT中存储敏感数据
- [ ] 净化错误消息
- [ ] 实现正确的CORS配置
- [ ] 使用安全头(Helmet.js)
监控与记录
- [ ] 记录安全事件
- [ ] 监控可疑活动
- [ ] 设置失败身份认证尝试的警报
- [ ] 跟踪API使用模式
- [ ] 不记录敏感数据
OWASP API安全Top 10
- 损坏的对象级授权 - 始终验证用户可以访问资源
- 损坏的身份认证 - 实现强身份认证机制
- 损坏的对象属性级授权 - 验证用户可以访问哪些属性
- 无限制的资源消耗 - 实现速率限制和配额
- 损坏的功能级授权 - 验证每个功能的用户角色
- 无限制访问敏感业务流程 - 保护关键工作流
- 服务器端请求伪造(SSRF) - 验证和净化URL
- 安全配置错误 - 使用安全最佳实践和头
- 不当的清单管理 - 记录和保护所有API端点
- 不安全地使用API - 验证来自第三方API的数据
相关技能
@ethical-hacking-methodology- 安全测试视角@sql-injection-testing- 测试SQL注入@xss-html-injection- 测试XSS漏洞@broken-authentication- 身份认证漏洞@backend-dev-guidelines- 后端开发标准@systematic-debugging- 调试安全问题
额外资源
专业提示: 安全不是一次性任务 - 定期审计您的API,保持依赖更新,并了解新漏洞!