选择正确的 API 认证方式对安全性、可扩展性和开发体验至关重要。本指南全面比较 API Key、JWT Bearer Token、OAuth 2.0、基于 Session 的认证和 Basic Auth — 包含代码示例、安全考量和决策框架,帮助你为项目选择最佳方案。
为什么 API 认证如此重要
每个处理用户数据或执行特权操作的 API 端点都必须验证调用者的身份。没有适当的认证,你的 API 将面临数据被盗、未授权修改和滥用的风险。
区分认证(authn)和授权(authz)非常重要。认证回答"你是谁?"— 验证调用者的身份。授权回答"你能做什么?"— 确定已认证的调用者可以访问哪些资源和操作。大多数实际系统两者都需要:先认证用户,然后检查其权限。
现代 API 通常使用五种认证策略之一:API Key、JWT Bearer Token、OAuth 2.0、基于 Session 的 Cookie 或 HTTP Basic Auth。每种方式在安全性、复杂度和适用场景上都有不同的权衡。
API Key 认证
API Key 是分配给客户端应用的一串长随机字符串。客户端在每次请求中包含此密钥,以便服务器识别和授权调用者。API Key 是最简单的 API 认证形式。
API Key 工作原理
服务器为每个客户端生成唯一密钥。客户端在 HTTP 头(推荐)或查询参数中发送密钥。服务器在数据库中查找密钥,识别关联的客户端/项目,并应用速率限制和权限。
Header vs 查询参数
在头部(如 X-API-Key 或 Authorization)中发送密钥是首选方式,因为查询参数会出现在服务器日志、浏览器历史记录和 Referer 头中,增加密钥泄露风险。
// Express.js — API Key 中间件
const express = require('express');
const app = express();
// API Key 验证中间件
function apiKeyAuth(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
// 在数据库中查找密钥
const client = await db.apiKeys.findOne({ key: apiKey, active: true });
if (!client) {
return res.status(403).json({ error: 'Invalid API key' });
}
// 附加客户端信息供后续使用
req.client = client;
next();
}
app.get('/api/data', apiKeyAuth, (req, res) => {
res.json({ data: 'protected resource', client: req.client.name });
});Bearer Token / JWT 认证
JSON Web Token (JWT) 通过将用户声明直接编码到令牌中来提供无状态认证。客户端使用 Bearer 方案在 Authorization 头中发送令牌。服务器无需任何数据库查询即可验证令牌签名。
JWT 由三个 Base64URL 编码的部分组成,用点分隔:Header(算法和令牌类型)、Payload(用户声明如 sub、exp、role)和 Signature(完整性的加密证明)。因为负载只是编码而非加密,所以永远不要在其中存储敏感数据。
无状态认证
与 Session 不同,服务器不需要存储任何状态。令牌本身包含所有必要的用户信息。这使得 JWT 非常适合微服务和水平扩展架构,在这些场景中跨实例共享 Session 存储是不切实际的。
// Express.js — JWT Bearer Token 认证
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
// 登录端点 — 颁发令牌
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findOne({ email });
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
SECRET,
{ expiresIn: '15m', issuer: 'https://api.example.com' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
SECRET,
{ expiresIn: '7d' }
);
res.json({ accessToken, refreshToken });
});
// 认证中间件 — 验证 Bearer 令牌
function bearerAuth(req, res, next) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Bearer token required' });
}
try {
const token = header.split(' ')[1];
req.user = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
app.get('/api/profile', bearerAuth, (req, res) => {
res.json({ userId: req.user.sub, role: req.user.role });
});OAuth 2.0 授权流程
OAuth 2.0 是一个委托框架,允许用户授予第三方应用对其资源的有限访问权限,而无需共享凭证。它是"使用 Google/GitHub 登录"按钮和 API 集成背后的标准。
授权码流程(Authorization Code)
最安全的服务端 Web 应用流程。用户被重定向到授权服务器,登录后带着短期授权码被重定向回来。服务器通过后端通道请求将授权码换取令牌。访问令牌永远不会接触浏览器。
客户端凭证流程(Client Credentials)
用于无用户参与的机器间(M2M)通信。客户端使用 client_id 和 client_secret 直接向授权服务器认证以获取访问令牌。常用于后端服务、定时任务和微服务通信。
授权码 + PKCE 流程
专为无法安全存储 client_secret 的公共客户端(如单页应用 SPA 和移动应用)设计。PKCE(Proof Key for Code Exchange)添加了 code_verifier/code_challenge 对来防止授权码拦截攻击。这是目前推荐的所有浏览器端应用流程。
// Express.js — OAuth 2.0 授权码流程
const axios = require('axios');
// 第1步:将用户重定向到授权服务器
app.get('/auth/github', (req, res) => {
const params = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID,
redirect_uri: 'https://myapp.com/auth/callback',
scope: 'read:user user:email',
state: generateRandomState(), // CSRF 防护
});
res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});
// 第2步:处理回调 — 用授权码换取令牌
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
if (!verifyState(state)) {
return res.status(403).json({ error: 'Invalid state' });
}
// 用授权码换取访问令牌
const tokenRes = await axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code,
redirect_uri: 'https://myapp.com/auth/callback',
},
{ headers: { Accept: 'application/json' } }
);
const { access_token } = tokenRes.data;
// 第3步:使用令牌获取用户数据
const userRes = await axios.get('https://api.github.com/user', {
headers: { Authorization: `Bearer ${access_token}` },
});
// 创建 Session 或颁发自己的 JWT
const jwt = issueJwt(userRes.data);
res.cookie('token', jwt, { httpOnly: true, secure: true, sameSite: 'lax' });
res.redirect('/dashboard');
});基于 Session 的认证
基于 Session 的认证是 Web 应用的传统方式。登录后,服务器创建一个存储在数据库或内存存储(如 Redis)中的 Session 对象,并通过 Cookie 向客户端发送 Session ID。在后续每次请求中,浏览器自动携带 Cookie,服务器查找对应的 Session。
由于 Cookie 会自动发送,基于 Session 的认证容易受到跨站请求伪造(CSRF)攻击。为缓解此问题,使用 CSRF 令牌 — 包含在表单中并在服务器端验证的随机值 — 并设置 SameSite Cookie 属性。
// Express.js — 使用 express-session 的 Session 认证
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');
const redisClient = redis.createClient({ url: process.env.REDIS_URL });
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // 仅 HTTPS
httpOnly: true, // JS 无法访问
sameSite: 'lax', // CSRF 防护
maxAge: 24 * 60 * 60 * 1000, // 24 小时
},
}));
// 登录
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findOne({ email });
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
req.session.userId = user.id;
req.session.role = user.role;
res.json({ message: 'Logged in' });
});
// 认证中间件
function sessionAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
next();
}
// 登出 — 销毁 Session
app.post('/auth/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});HTTP Basic 认证
Basic Auth 在 Authorization 头中发送 Base64 编码的用户名和密码。格式为 Authorization: Basic base64(username:password)。它是最简单的 HTTP 认证方案,定义在 RFC 7617 中。
Basic Auth 仍然常用于内部 API、开发/测试环境、CI/CD 流水线和简单的 Webhook。由于 Base64 是编码而非加密,凭证可以被轻易解码,因此必须始终通过 HTTPS 使用。
// Express.js — Basic Auth 中间件
function basicAuth(req, res, next) {
const header = req.headers.authorization;
if (!header?.startsWith('Basic ')) {
res.set('WWW-Authenticate', 'Basic realm="API"');
return res.status(401).json({ error: 'Basic auth required' });
}
const base64 = header.split(' ')[1];
const decoded = Buffer.from(base64, 'base64').toString('utf-8');
const [username, password] = decoded.split(':');
// 验证凭证(使用时序安全比较!)
const valid = username === process.env.API_USER
&& crypto.timingSafeEqual(
Buffer.from(password),
Buffer.from(process.env.API_PASS)
);
if (!valid) {
return res.status(403).json({ error: 'Invalid credentials' });
}
req.user = { username };
next();
}
// 使用
app.get('/api/internal', basicAuth, (req, res) => {
res.json({ message: 'Authenticated', user: req.user.username });
});
// 客户端使用 Basic Auth 请求
// curl -u username:password https://api.example.com/internal
// fetch('https://api.example.com/internal', {
// headers: { Authorization: 'Basic ' + btoa('user:pass') }
// });认证方式对比表
使用此表快速比较五种认证方式的关键维度:
| Method | Security | Complexity | Stateless | Best For |
|---|---|---|---|---|
| API Key | Low-Medium | Low | Yes | Server-to-server, public APIs |
| JWT Bearer | Medium-High | Medium | Yes | SPAs, mobile apps, microservices |
| OAuth 2.0 | High | High | Yes | Third-party integrations, SSO |
| Session/Cookie | Medium-High | Medium | No | Traditional web apps |
| Basic Auth | Low | Low | Yes | Internal APIs, dev/staging |
令牌存储策略
令牌存储位置直接影响安全性。三种主要选项各有不同的漏洞特征:
存储在 httpOnly Cookie 中的令牌无法被 JavaScript 访问,因此免受 XSS 攻击。浏览器会自动在请求中携带它们。但 Cookie 容易受到 CSRF 攻击,所以必须使用 SameSite 属性和 CSRF 令牌。
实现简单,与 SPA 配合良好。但 localStorage 可被页面上的任何 JavaScript 访问,容易受到 XSS 攻击。如果攻击者注入恶意脚本,就能窃取令牌。
对 XSS 和 CSRF 最安全的选项,因为令牌只存在于 JavaScript 闭包或变量中。缺点是页面刷新时令牌会丢失,需要静默重新认证。
// httpOnly Cookie 方式(服务端)
res.cookie('access_token', token, {
httpOnly: true, // JS 无法读取此 Cookie
secure: true, // 仅 HTTPS
sameSite: 'strict', // 禁止跨站发送
maxAge: 900000, // 15 分钟
path: '/',
});
// localStorage 方式(客户端)— 安全性较低
localStorage.setItem('access_token', token);
// 使用 localStorage 令牌发起 fetch 请求
fetch('/api/data', {
headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` }
});
// 内存变量方式(客户端)— 最安全
let accessToken = null; // 仅存在于闭包中
async function login(email, password) {
const res = await fetch('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await res.json();
accessToken = data.accessToken; // 仅存储在内存中
}
async function fetchProtected(url) {
return fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` },
});
}令牌刷新策略
短期访问令牌提高了安全性,但需要机制来获取新令牌而无需重新认证。以下是主要的刷新策略:
每次使用刷新令牌时,都会颁发新的刷新令牌并使旧令牌失效。如果被盗的刷新令牌在合法用户已经刷新后被使用,服务器会检测到重用并使整个令牌族失效,强制重新认证。
每次成功请求都会延长令牌过期时间。如果用户正在活跃使用应用,则保持登录状态。经过一段时间的不活动后,令牌过期。这在基于 Session 的系统中很常见。
SPA 使用隐藏的 iframe 调用授权服务器,在无需用户交互的情况下获取新的访问令牌。出于安全考虑,这种方式正在被带有 PKCE 的刷新令牌轮换所取代。
// Express.js — 刷新令牌轮换
const refreshTokens = new Map(); // 生产环境使用 Redis/数据库
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
const payload = jwt.verify(refreshToken, SECRET);
// 检查此刷新令牌是否已被使用过(轮换检测)
const stored = refreshTokens.get(payload.sub);
if (!stored || stored.token !== refreshToken) {
// 检测到令牌重用!使该用户的所有令牌失效
refreshTokens.delete(payload.sub);
return res.status(401).json({ error: 'Token reuse detected' });
}
// 颁发新的令牌对
const newAccessToken = jwt.sign(
{ sub: payload.sub, role: payload.role },
SECRET,
{ expiresIn: '15m' }
);
const newRefreshToken = jwt.sign(
{ sub: payload.sub, type: 'refresh' },
SECRET,
{ expiresIn: '7d' }
);
// 轮换:存储新刷新令牌,使旧令牌失效
refreshTokens.set(payload.sub, {
token: newRefreshToken,
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
} catch {
return res.status(401).json({ error: 'Invalid refresh token' });
}
});速率限制与节流
速率限制保护你的 API 免受滥用并确保客户端之间的公平使用。对于使用 API Key 的公共 API 尤为重要,否则单个密钥可能消耗所有可用资源。
大多数速率限制器使用滑动窗口或令牌桶算法。当客户端超过限制时,服务器返回 HTTP 429 Too Many Requests 和 Retry-After 头,指示客户端何时可以重试。
// Express.js — 按 API Key 的速率限制
const rateLimit = require('express-rate-limit');
// 全局速率限制
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每窗口 100 个请求
standardHeaders: true, // 在头部返回速率限制信息
legacyHeaders: false,
message: { error: '请求过多,请稍后重试' },
keyGenerator: (req) => {
// 按 API Key 或 IP 进行速率限制
return req.headers['x-api-key'] || req.ip;
},
});
app.use('/api/', globalLimiter);
// 认证端点的更严格限制
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 每 15 分钟仅 5 次登录尝试
message: { error: '登录尝试过多' },
});
app.use('/auth/login', authLimiter);
// 自定义响应头:
// X-RateLimit-Limit: 100
// X-RateLimit-Remaining: 73
// X-RateLimit-Reset: 1672531200
// Retry-After: 900(秒,仅在 429 时)安全最佳实践
- 始终使用 HTTPS — 通过 HTTP 发送的认证令牌可被网络上的任何人截获。永远不要部署通过明文 HTTP 接受凭证的 API。
- 使用短期访问令牌 — 将访问令牌过期时间设为 15-60 分钟。配合刷新令牌处理更长的会话。泄露的短期令牌限制了攻击窗口。
- 限制作用域和权限 — 遵循最小权限原则。API Key 和 OAuth 令牌应只具有所需的最小权限。使用细粒度作用域如 read:users 而非宽泛的 admin 访问。
- 实施密钥轮换 — API Key 和密钥应定期轮换。在轮换期间支持多个活跃密钥,以便客户端平滑过渡无需停机。
- 验证和清理所有输入 — 即使有认证,也永远不要信任客户端输入。验证令牌格式,检查过期时间,核实 audience 和 issuer 声明。
- 记录和监控认证事件 — 跟踪失败的登录尝试、令牌刷新和异常访问模式。为暴力攻击和令牌重用设置告警。
- 使用时序安全比较 — 比较密钥、API Key 或令牌时,始终使用恒定时间比较函数以防止时序攻击。
- 永远不要在客户端代码中暴露密钥 — 客户端密钥、签名密钥和数据库凭证永远不能出现在前端 JavaScript、移动应用包或公共仓库中。
FAQ
什么时候该用 API Key,什么时候该用 OAuth 2.0?
对于需要识别调用应用的简单服务器间集成,使用 API Key。当需要委托用户授权时使用 OAuth 2.0 — 即第三方应用需要代表用户操作时。OAuth 提供细粒度的作用域和用户同意,而 API Key 是全有或全无的。
JWT 比基于 Session 的认证更好吗?
两者没有绝对优劣之分。JWT 适合无状态的分布式系统(微服务、移动 API),因为不需要服务端存储。基于 Session 的认证在需要轻松撤销(直接删除 Session)、更小的负载和更简单的安全模型时更好。许多生产系统同时使用两者:JWT 用于 API 认证,Session 用于 Web UI。
如何在前端应用中保护 API Key?
你无法在前端代码中安全存储 API Key — 客户端 JavaScript 中的任何密钥都会暴露。替代方案:创建持有 API Key 并转发请求的后端代理,使用 OAuth PKCE 进行面向用户的认证,或使用带有域名/IP 白名单和严格速率限制的受限 API Key。
认证(Authentication)和授权(Authorization)有什么区别?
认证(authn)验证身份 — 确认调用者是谁(例如验证 JWT 签名或检查凭证)。授权(authz)确定权限 — 已认证的调用者被允许做什么(例如检查用户是否具有 "admin" 角色)。认证总是在前;授权依赖于认证。
应该自建认证系统还是使用现有服务?
对于大多数应用,使用经过验证的认证服务或库(Auth0、Firebase Auth、Supabase Auth、Passport.js)。自建认证容易出错且属于安全关键部分。仅当你有非常特殊的需求且有安全经验丰富的团队来维护时,才自建认证。