DevToolBox免费
博客

API 认证:OAuth 2.0 vs JWT vs API Key

13 分钟阅读作者 DevToolBox

选择正确的 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') }
// });

认证方式对比表

使用此表快速比较五种认证方式的关键维度:

MethodSecurityComplexityStatelessBest For
API KeyLow-MediumLowYesServer-to-server, public APIs
JWT BearerMedium-HighMediumYesSPAs, mobile apps, microservices
OAuth 2.0HighHighYesThird-party integrations, SSO
Session/CookieMedium-HighMediumNoTraditional web apps
Basic AuthLowLowYesInternal APIs, dev/staging

令牌存储策略

令牌存储位置直接影响安全性。三种主要选项各有不同的漏洞特征:

httpOnly Cookie(推荐)

存储在 httpOnly Cookie 中的令牌无法被 JavaScript 访问,因此免受 XSS 攻击。浏览器会自动在请求中携带它们。但 Cookie 容易受到 CSRF 攻击,所以必须使用 SameSite 属性和 CSRF 令牌。

localStorage

实现简单,与 SPA 配合良好。但 localStorage 可被页面上的任何 JavaScript 访问,容易受到 XSS 攻击。如果攻击者注入恶意脚本,就能窃取令牌。

内存变量(JavaScript Variable)

对 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}` },
  });
}

令牌刷新策略

短期访问令牌提高了安全性,但需要机制来获取新令牌而无需重新认证。以下是主要的刷新策略:

刷新令牌轮换(Refresh Token Rotation)

每次使用刷新令牌时,都会颁发新的刷新令牌并使旧令牌失效。如果被盗的刷新令牌在合法用户已经刷新后被使用,服务器会检测到重用并使整个令牌族失效,强制重新认证。

滑动窗口过期

每次成功请求都会延长令牌过期时间。如果用户正在活跃使用应用,则保持登录状态。经过一段时间的不活动后,令牌过期。这在基于 Session 的系统中很常见。

静默刷新(适用于 SPA)

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)。自建认证容易出错且属于安全关键部分。仅当你有非常特殊的需求且有安全经验丰富的团队来维护时,才自建认证。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

JWTJWT DecoderB64Base64 Encoder/Decoder#Hash Generator>>cURL to Code Converter

相关文章

JWT 工作原理:JSON Web Token 完全指南

了解 JWT 认证的工作原理,理解 header、payload 和 signature 的结构,安全地在应用中实现 JWT。

REST API 最佳实践:2026 完整指南

学习 REST API 设计最佳实践,包括命名规范、错误处理、认证、分页、版本控制和安全头。