REST API 认证与授权
API安全是后端开发中最关键的环节之一。认证(Authentication)和授权(Authorization)是API安全的两大支柱。本章将深入探讨REST API中的认证与授权机制,帮助你设计出安全可靠的API。
认证与授权的区别
在深入讨论具体机制之前,我们需要明确这两个概念的区别:
认证(Authentication):验证"你是谁"。确认用户的身份是否合法。
授权(Authorization):验证"你能做什么"。确认已认证用户是否有权限执行特定操作。
用户请求流程:
请求 → 认证中间件 → 授权检查 → 业务逻辑 → 响应
↓ ↓
你是谁? 你能做这个吗?
↓ ↓
验证身份 检查权限
举个例子:用户张三登录系统后,系统确认他是张三(认证通过),但当他尝试删除一篇文章时,系统发现这篇文章不是他写的,拒绝了他的请求(授权失败)。
常见认证方式
API Key 认证
API Key是最简单的认证方式,适用于服务器之间的通信或公开API。
工作原理:客户端在请求中携带一个预分配的密钥,服务器验证这个密钥是否有效。
请求方式:
通过请求头传递:
GET /api/articles HTTP/1.1
Host: api.example.com
X-API-Key: your-api-key-here
通过查询参数传递:
GET /api/articles?api_key=your-api-key-here HTTP/1.1
Host: api.example.com
服务端验证示例(Node.js Express):
// API Key 认证中间件
function apiKeyAuth(req, res, next) {
const apiKey = req.headers['x-api-key'] || req.query.api_key;
if (!apiKey) {
return res.status(401).json({
error: 'API Key缺失',
message: '请在请求头中提供X-API-Key或在查询参数中提供api_key'
});
}
// 验证API Key是否有效
const validKey = await getApiKeyFromDatabase(apiKey);
if (!validKey) {
return res.status(401).json({
error: '无效的API Key',
message: '提供的API Key不存在或已失效'
});
}
// 检查API Key是否过期
if (validKey.expiresAt && new Date() > validKey.expiresAt) {
return res.status(401).json({
error: 'API Key已过期',
message: '请重新获取新的API Key'
});
}
// 将API Key信息附加到请求对象
req.apiKey = validKey;
next();
}
app.use(apiKeyAuth);
API Key的设计要点:
- 使用足够长度的随机字符串(至少32字符)
- 可以设置过期时间和使用限制
- 支持撤销和重新生成
- 记录使用日志便于审计
API Key的优缺点:
优点:
- 实现简单,易于理解
- 无需维护会话状态
- 适合服务间通信
缺点:
- 安全性较低,密钥泄露风险大
- 无法进行细粒度的权限控制
- 无法实现单点登出
Basic 认证
Basic认证是HTTP协议内置的认证方式,使用Base64编码传输用户名和密码。
请求方式:
GET /api/articles HTTP/1.1
Host: api.example.com
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
其中 dXNlcm5hbWU6cGFzc3dvcmQ= 是 username:password 的Base64编码。
服务端验证示例:
function basicAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="API"');
return res.status(401).json({
error: '需要认证',
message: '请提供有效的用户名和密码'
});
}
// 解码Base64
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf8');
const [username, password] = credentials.split(':');
// 验证用户名密码
const user = await authenticateUser(username, password);
if (!user) {
res.setHeader('WWW-Authenticate', 'Basic realm="API"');
return res.status(401).json({
error: '认证失败',
message: '用户名或密码错误'
});
}
req.user = user;
next();
}
注意事项:
- Base64是编码,不是加密,可以轻松解码
- 必须配合HTTPS使用,否则凭证会被明文传输
- 浏览器会缓存凭证,存在安全风险
- 无法实现细粒度的权限控制
适用场景:内部工具、开发测试环境、简单的一次性脚本。
Bearer Token 认证
Bearer Token是目前最流行的API认证方式,广泛应用于OAuth 2.0和JWT场景。
请求方式:
GET /api/articles HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Bearer的含义:持有Token的人就可以访问资源,服务器不关心Token是如何获得的。
服务端验证示例:
function bearerAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: '需要认证',
message: '请在请求头中提供Bearer Token'
});
}
const token = authHeader.split(' ')[1];
try {
// 验证Token(以JWT为例)
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token已过期',
message: '请重新登录获取新的Token'
});
}
return res.status(401).json({
error: '无效的Token',
message: 'Token验证失败'
});
}
}
JWT(JSON Web Token)
JWT是目前最常用的Token格式,它是一种自包含的Token,包含了用户身份信息和签名。
JWT结构:
JWT由三部分组成,用点号分隔:Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header(Base64编码)
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuW8oOS4iSIsImlhdCI6MTUxNjIzOTAyMn0 ← Payload(Base64编码)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature(签名)
Header:描述Token类型和签名算法
{
"alg": "HS256",
"typ": "JWT"
}
Payload:包含用户信息和自定义数据
{
"sub": "1234567890", // Subject:用户唯一标识
"name": "张三", // 用户名
"role": "admin", // 用户角色
"iat": 1516239022, // Issued At:签发时间
"exp": 1516242622, // Expiration:过期时间
"iss": "api.example.com" // Issuer:签发者
}
Signature:对Header和Payload的签名,防止篡改
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
JWT生成示例(Node.js):
const jwt = require('jsonwebtoken');
// 配置
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '1h';
const JWT_REFRESH_EXPIRES_IN = '7d';
// 生成访问Token
function generateAccessToken(user) {
return jwt.sign(
{
sub: user.id,
name: user.name,
role: user.role,
permissions: user.permissions
},
JWT_SECRET,
{
expiresIn: JWT_EXPIRES_IN,
issuer: 'api.example.com'
}
);
}
// 生成刷新Token
function generateRefreshToken(user) {
return jwt.sign(
{
sub: user.id,
type: 'refresh'
},
JWT_SECRET,
{
expiresIn: JWT_REFRESH_EXPIRES_IN,
issuer: 'api.example.com'
}
);
}
// 登录接口
app.post('/auth/login', async (req, res) => {
const { username, password } = req.body;
// 验证用户
const user = await authenticateUser(username, password);
if (!user) {
return res.status(401).json({
error: '认证失败',
message: '用户名或密码错误'
});
}
// 生成Token对
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// 保存刷新Token到数据库(用于撤销)
await saveRefreshToken(user.id, refreshToken);
res.json({
accessToken,
refreshToken,
expiresIn: 3600, // 秒
tokenType: 'Bearer'
});
});
JWT验证示例:
function verifyToken(token) {
return jwt.verify(token, JWT_SECRET, {
issuer: 'api.example.com'
});
}
// 认证中间件
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
error: '需要认证',
message: '请提供有效的Bearer Token'
});
}
const token = authHeader.substring(7);
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token已过期',
code: 'TOKEN_EXPIRED'
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
error: '无效的Token',
code: 'INVALID_TOKEN'
});
}
return res.status(401).json({
error: 'Token验证失败',
code: 'VERIFICATION_FAILED'
});
}
}
Token刷新机制:
// 刷新Token接口
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
error: '缺少刷新Token'
});
}
try {
// 验证刷新Token
const decoded = verifyToken(refreshToken);
// 检查Token类型
if (decoded.type !== 'refresh') {
return res.status(400).json({
error: '无效的刷新Token'
});
}
// 检查Token是否在数据库中(未被撤销)
const stored = await getRefreshToken(decoded.sub, refreshToken);
if (!stored) {
return res.status(401).json({
error: '刷新Token已失效',
message: '请重新登录'
});
}
// 获取用户信息
const user = await getUserById(decoded.sub);
// 生成新的Token对
const newAccessToken = generateAccessToken(user);
const newRefreshToken = generateRefreshToken(user);
// 撤销旧的刷新Token
await revokeRefreshToken(refreshToken);
await saveRefreshToken(user.id, newRefreshToken);
res.json({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiresIn: 3600,
tokenType: 'Bearer'
});
} catch (error) {
return res.status(401).json({
error: '刷新Token无效',
message: '请重新登录'
});
}
});
// 登出接口(撤销刷新Token)
app.post('/auth/logout', authenticate, async (req, res) => {
await revokeAllRefreshTokens(req.user.sub);
res.json({ message: '登出成功' });
});
JWT的安全考虑:
-
签名算法选择:
- HS256(对称加密):速度快,但密钥泄露风险大
- RS256(非对称加密):更安全,可以使用公钥验证
-
Payload注意事项:
- 不要存储敏感信息(密码、信用卡号等)
- 控制Payload大小,影响传输效率
- 使用标准的声明字段(sub, iat, exp等)
-
Token存储:
- Web应用:使用HttpOnly Cookie(防XSS)或内存存储
- 移动应用:使用安全的存储机制(如Keychain)
-
Token生命周期:
- 访问Token有效期短(15分钟-1小时)
- 刷新Token有效期长(7天-30天)
OAuth 2.0
OAuth 2.0是一个授权框架,允许第三方应用在用户授权下访问用户的资源,而无需共享用户的密码。
核心概念:
- Resource Owner:资源所有者(用户)
- Client:第三方应用
- Authorization Server:授权服务器
- Resource Server:资源服务器(API服务器)
- Access Token:访问令牌
- Refresh Token:刷新令牌
授权流程(授权码模式):
用户 → 第三方应用 → 授权服务器 → 用户登录授权
↓
第三方应用 ← 授权码 ←─────────────────┘
↓
第三方应用 → 授权服务器(用授权码换取Token)
↓
第三方应用 ← Access Token ←─────────┘
↓
第三方应用 → 资源服务器(使用Token访问资源)
↓
第三方应用 ← 用户数据 ←─────────────┘
授权码模式的实现:
// 第一步:重定向到授权服务器
app.get('/auth/authorize', (req, res) => {
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'read write',
state: generateRandomState() // 防止CSRF
});
res.redirect(`https://auth.example.com/authorize?${params}`);
});
// 第二步:接收授权码
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
// 验证state(防止CSRF攻击)
if (!verifyState(state)) {
return res.status(400).json({ error: '无效的state' });
}
// 用授权码换取Token
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI
})
});
const tokens = await tokenResponse.json();
// tokens包含:access_token, refresh_token, expires_in, token_type
res.json(tokens);
});
// 第三步:使用Token访问资源
async function fetchUserInfo(accessToken) {
const response = await fetch('https://api.example.com/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return response.json();
}
其他授权模式:
| 模式 | 适用场景 | 安全性 |
|---|---|---|
| 授权码模式 | 服务器端应用 | 最高 |
| 授权码+PKCE | 单页应用、移动应用 | 高 |
| 客户端凭证 | 服务间通信 | 中 |
| 设备码 | 无浏览器设备 | 中 |
| 密码模式 | 受信任的第一方应用 | 低 |
授权策略
基于角色的访问控制(RBAC)
RBAC是最常用的授权模型,通过角色来管理权限。
核心概念:
- 用户(User):系统使用者
- 角色(Role):权限的集合
- 权限(Permission):对资源的操作权限
数据库设计:
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(50),
password_hash VARCHAR(255)
);
-- 角色表
CREATE TABLE roles (
id INT PRIMARY KEY,
name VARCHAR(50),
description VARCHAR(255)
);
-- 用户角色关联表
CREATE TABLE user_roles (
user_id INT,
role_id INT,
PRIMARY KEY (user_id, role_id)
);
-- 权限表
CREATE TABLE permissions (
id INT PRIMARY KEY,
resource VARCHAR(50),
action VARCHAR(50),
description VARCHAR(255)
);
-- 角色权限关联表
CREATE TABLE role_permissions (
role_id INT,
permission_id INT,
PRIMARY KEY (role_id, permission_id)
);
RBAC中间件实现:
// 检查用户是否有指定角色
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '需要认证' });
}
const hasRole = roles.some(role => req.user.roles.includes(role));
if (!hasRole) {
return res.status(403).json({
error: '权限不足',
message: `需要以下角色之一: ${roles.join(', ')}`
});
}
next();
};
}
// 使用示例
app.delete('/articles/:id',
authenticate,
requireRole('admin', 'editor'),
deleteArticle
);
检查权限的实现:
// 检查用户是否有指定权限
async function hasPermission(userId, resource, action) {
const result = await db.query(`
SELECT COUNT(*) as count
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN role_permissions rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
WHERE u.id = ? AND p.resource = ? AND p.action = ?
`, [userId, resource, action]);
return result[0].count > 0;
}
// 权限检查中间件
function requirePermission(resource, action) {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '需要认证' });
}
const permitted = await hasPermission(req.user.sub, resource, action);
if (!permitted) {
return res.status(403).json({
error: '权限不足',
message: `需要权限: ${resource}:${action}`
});
}
next();
};
}
// 使用示例
app.post('/articles',
authenticate,
requirePermission('article', 'create'),
createArticle
);
app.put('/articles/:id',
authenticate,
requirePermission('article', 'update'),
updateArticle
);
基于资源的访问控制
有些场景下,权限不仅仅取决于角色,还取决于资源本身。例如:用户只能修改自己创建的文章。
// 检查资源所有权
async function checkArticleOwnership(userId, articleId) {
const article = await db.query(`
SELECT author_id FROM articles WHERE id = ?
`, [articleId]);
if (!article) {
return { exists: false };
}
return {
exists: true,
isOwner: article.author_id === userId
};
}
// 资源级别的授权中间件
function authorizeArticle(action) {
return async (req, res, next) => {
const articleId = req.params.id;
const userId = req.user.sub;
// 管理员拥有所有权限
if (req.user.roles.includes('admin')) {
return next();
}
const { exists, isOwner } = await checkArticleOwnership(userId, articleId);
if (!exists) {
return res.status(404).json({ error: '文章不存在' });
}
// 更新和删除需要是所有者
if ((action === 'update' || action === 'delete') && !isOwner) {
return res.status(403).json({
error: '权限不足',
message: '只能操作自己的文章'
});
}
next();
};
}
// 使用示例
app.put('/articles/:id',
authenticate,
authorizeArticle('update'),
updateArticle
);
app.delete('/articles/:id',
authenticate,
authorizeArticle('delete'),
deleteArticle
);
安全最佳实践
Token安全
-
使用HTTPS:所有API通信必须使用HTTPS,防止Token被截获。
-
Token存储安全:
// Web应用:使用HttpOnly Cookie
res.cookie('access_token', token, {
httpOnly: true, // 防止JavaScript访问
secure: true, // 只在HTTPS下传输
sameSite: 'strict', // 防止CSRF
maxAge: 3600000 // 1小时
}); -
Token刷新策略:
// 在Token即将过期时自动刷新
function shouldRefreshToken(decoded) {
const now = Math.floor(Date.now() / 1000);
const timeUntilExpiry = decoded.exp - now;
// 如果Token将在5分钟内过期,建议刷新
return timeUntilExpiry < 300;
}
密码安全
-
使用强哈希算法:
const bcrypt = require('bcrypt');
// 哈希密码
async function hashPassword(password) {
const salt = await bcrypt.genSalt(12); // cost factor = 12
return bcrypt.hash(password, salt);
}
// 验证密码
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
} -
密码复杂度要求:
function validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push('密码至少8个字符');
}
if (!/[A-Z]/.test(password)) {
errors.push('密码需要包含大写字母');
}
if (!/[a-z]/.test(password)) {
errors.push('密码需要包含小写字母');
}
if (!/[0-9]/.test(password)) {
errors.push('密码需要包含数字');
}
return errors;
}
防止常见攻击
-
防止暴力破解:
const rateLimit = require('express-rate-limit');
// 登录限流
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 最多5次尝试
message: {
error: '登录尝试过多',
message: '请15分钟后再试'
}
});
app.post('/auth/login', loginLimiter, loginHandler); -
防止CSRF攻击:
const csrf = require('csurf');
// 对于使用Cookie存储Token的API
app.use(csrf({ cookie: true }));
// 提供CSRF Token
app.get('/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
}); -
输入验证:
const { body, validationResult } = require('express-validator');
app.post('/articles',
authenticate,
body('title').trim().isLength({ min: 1, max: 200 }).escape(),
body('content').trim().isLength({ min: 1 }),
body('tags').optional().isArray({ max: 10 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: '输入验证失败',
details: errors.array()
});
}
// 处理请求
}
);
总结
REST API认证与授权的关键要点:
-
认证方式选择:
- API Key:简单场景、服务间通信
- JWT:现代应用首选,支持无状态认证
- OAuth 2.0:第三方授权、单点登录
-
授权策略:
- RBAC:基于角色的访问控制
- 资源级授权:检查资源所有权
-
安全措施:
- 强制HTTPS
- 安全存储Token
- 密码强哈希
- 限流防暴力破解
- 输入验证
良好的认证授权设计是API安全的基础,需要根据实际场景选择合适的方案。