跳到主要内容

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的安全考虑

  1. 签名算法选择

    • HS256(对称加密):速度快,但密钥泄露风险大
    • RS256(非对称加密):更安全,可以使用公钥验证
  2. Payload注意事项

    • 不要存储敏感信息(密码、信用卡号等)
    • 控制Payload大小,影响传输效率
    • 使用标准的声明字段(sub, iat, exp等)
  3. Token存储

    • Web应用:使用HttpOnly Cookie(防XSS)或内存存储
    • 移动应用:使用安全的存储机制(如Keychain)
  4. 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安全

  1. 使用HTTPS:所有API通信必须使用HTTPS,防止Token被截获。

  2. Token存储安全

    // Web应用:使用HttpOnly Cookie
    res.cookie('access_token', token, {
    httpOnly: true, // 防止JavaScript访问
    secure: true, // 只在HTTPS下传输
    sameSite: 'strict', // 防止CSRF
    maxAge: 3600000 // 1小时
    });
  3. Token刷新策略

    // 在Token即将过期时自动刷新
    function shouldRefreshToken(decoded) {
    const now = Math.floor(Date.now() / 1000);
    const timeUntilExpiry = decoded.exp - now;

    // 如果Token将在5分钟内过期,建议刷新
    return timeUntilExpiry < 300;
    }

密码安全

  1. 使用强哈希算法

    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);
    }
  2. 密码复杂度要求

    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;
    }

防止常见攻击

  1. 防止暴力破解

    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);
  2. 防止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() });
    });
  3. 输入验证

    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认证与授权的关键要点:

  1. 认证方式选择

    • API Key:简单场景、服务间通信
    • JWT:现代应用首选,支持无状态认证
    • OAuth 2.0:第三方授权、单点登录
  2. 授权策略

    • RBAC:基于角色的访问控制
    • 资源级授权:检查资源所有权
  3. 安全措施

    • 强制HTTPS
    • 安全存储Token
    • 密码强哈希
    • 限流防暴力破解
    • 输入验证

良好的认证授权设计是API安全的基础,需要根据实际场景选择合适的方案。