3. 搭建 Mock OAuth 授权服务器:使用 Node.js + Express 快速搭建本地 Mock 授权端点

在微服务或前后端分离架构中,OAuth 2.0 授权服务器的可用性往往是调试链路中的最大阻塞点。依赖真实的授权服务器(如 Okta、Auth0 或自建 Keycloak)意味着需要网络连通、客户端注册、密钥管理以及复杂的重定向流程。本章将使用 Node.js + Express 在本地搭建一个轻量级 Mock 授权服务器,仅需 50 行核心代码即可模拟授权码模式(Authorization Code Grant)的核心端点,实现依赖隔离。

3.1 Mock 授权服务器的设计目标

在搭建之前,需要明确 Mock 服务器的职责边界。它不需要实现完整的 OAuth 2.0 规范,而是聚焦于调试场景下的最小可用功能。

功能维度 真实授权服务器 Mock 授权服务器(本章目标)
授权端点 /authorize 用户登录、同意授权、生成授权码 直接返回授权码(跳过用户交互)
令牌端点 /token 验证授权码、客户端凭证、颁发 Access Token 验证授权码后直接返回固定 Token
客户端注册 需要注册 client_id 和 client_secret 使用预设的固定 client_id
HTTPS 强制要求 本地 HTTP 即可(调试环境)
持久化 数据库存储 内存存储(进程重启即重置)
调试原则: Mock 服务器的核心价值在于 可预测性。每次请求返回相同结构的响应,让开发者能够专注于客户端(如 SPA、移动端)的授权码交换逻辑,而非被授权服务器的网络波动或配置错误干扰。

3.2 项目初始化与依赖安装

创建一个新的 Node.js 项目,并安装必要的依赖包。

mkdir mock-oauth-server
cd mock-oauth-server
npm init -y
npm install express cors uuid
依赖包 用途
express HTTP 服务器框架,用于定义路由和中间件
cors 跨域资源共享中间件,允许来自不同端口的客户端请求
uuid 生成唯一授权码(authorization code)和 Access Token

3.3 核心代码实现:授权端点与令牌端点

在项目根目录创建 server.js,写入以下代码。该服务器将监听 localhost:9001,并暴露两个核心端点。

const express = require('express');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');

const app = express();
const PORT = 9001;

// 中间件
app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// 内存存储:用于保存已生成的授权码
const authCodeStore = new Map();

// ---------- 1. 授权端点 /authorize ----------
// 模拟授权码模式的第一步:客户端重定向用户至此端点
// Mock 服务器跳过用户登录,直接返回授权码
app.get('/authorize', (req, res) => {
  const { response_type, client_id, redirect_uri, state } = req.query;

  // 参数校验(最小化校验)
  if (response_type !== 'code') {
    return res.status(400).json({ error: 'unsupported_response_type' });
  }
  if (!client_id) {
    return res.status(400).json({ error: 'invalid_request', error_description: 'Missing client_id' });
  }

  // 生成授权码并存储
  const authCode = uuidv4();
  authCodeStore.set(authCode, {
    client_id,
    redirect_uri,
    created_at: Date.now()
  });

  // 构造重定向 URL(模拟浏览器 302 跳转)
  const redirectUrl = new URL(redirect_uri);
  redirectUrl.searchParams.set('code', authCode);
  if (state) {
    redirectUrl.searchParams.set('state', state);
  }

  console.log(`[Mock Auth] 授权码已生成: ${authCode}`);
  res.redirect(302, redirectUrl.toString());
});

// ---------- 2. 令牌端点 /token ----------
// 客户端使用授权码交换 Access Token
app.post('/token', (req, res) => {
  const { grant_type, code, redirect_uri, client_id } = req.body;

  // 参数校验
  if (grant_type !== 'authorization_code') {
    return res.status(400).json({ error: 'unsupported_grant_type' });
  }
  if (!code || !authCodeStore.has(code)) {
    return res.status(400).json({ error: 'invalid_grant', error_description: '授权码无效或已过期' });
  }

  // 验证授权码与客户端匹配
  const stored = authCodeStore.get(code);
  if (stored.client_id !== client_id) {
    return res.status(400).json({ error: 'invalid_grant', error_description: 'client_id 不匹配' });
  }

  // 验证 redirect_uri(可选,增强 Mock 真实性)
  if (stored.redirect_uri && stored.redirect_uri !== redirect_uri) {
    return res.status(400).json({ error: 'invalid_grant', error_description: 'redirect_uri 不匹配' });
  }

  // 删除已使用的授权码(一次性使用)
  authCodeStore.delete(code);

  // 生成 Mock Access Token
  const accessToken = uuidv4();
  const refreshToken = uuidv4();

  console.log(`[Mock Auth] 令牌已颁发: ${accessToken}`);

  res.json({
    access_token: accessToken,
    token_type: 'Bearer',
    expires_in: 3600,
    refresh_token: refreshToken,
    scope: 'openid profile email'
  });
});

// 启动服务器
app.listen(PORT, () => {
  console.log(`Mock OAuth 授权服务器已启动: http://localhost:${PORT}`);
});
关键设计决策:
  • 授权端点使用 GET 方法: 符合 OAuth 2.0 规范,客户端通过浏览器重定向发起请求。
  • 令牌端点使用 POST 方法: 客户端在后台通过 HTTP POST 发送凭证,避免授权码暴露在 URL 中。
  • 授权码一次性使用: 从 authCodeStore 中删除已使用的授权码,模拟真实服务器的安全策略。
  • 固定 Token 结构: 返回的 access_token 是 UUID 格式,便于调试时识别;实际生产环境应为 JWT。

3.4 授权码模式流程可视化

下面的 SVG 图展示了客户端(如浏览器中的 SPA)与 Mock 授权服务器之间的完整交互流程。注意,Mock 服务器跳过了“用户登录”步骤,直接生成授权码。

Mock 授权服务器 — 授权码模式交互流程 客户端 (SPA) Mock 授权端点 /authorize Mock 令牌端点 /token 内存存储 (Map) 1. GET /authorize ?response_type=code &client_id=mock-client 存储授权码 3. 302 重定向 ?code=AUTH_CODE_123 4. POST /token grant_type=authorization_code &code=AUTH_CODE_123 验证 & 删除 6. 返回 Access Token { access_token: "TOKEN_456" } ⚠ 调试要点: 步骤 2 和步骤 5 展示了 Mock 服务器的核心行为:授权码在内存中生成、存储、验证后立即删除,模拟真实服务器的“一次性使用”安全策略。

3.5 启动服务器与快速验证

在终端中运行以下命令启动 Mock 服务器:

node server.js

服务器启动后,使用 curl 或浏览器模拟完整的授权码流程。

步骤 1:模拟浏览器访问授权端点

# 在浏览器中打开以下 URL,或使用 curl -v 查看重定向
curl -v "http://localhost:9001/authorize?response_type=code&client_id=mock-client&redirect_uri=http://localhost:3000/callback&state=xyz123"

服务器将返回 302 状态码,并重定向到 http://localhost:3000/callback?code=生成的授权码&state=xyz123。

步骤 2:使用授权码交换 Access Token

# 将 CODE_PLACEHOLDER 替换为上一步获取的授权码
curl -X POST "http://localhost:9001/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=CODE_PLACEHOLDER&redirect_uri=http://localhost:3000/callback&client_id=mock-client"

成功响应示例:

{
  "access_token": "550e8400-e29b-41d4-a716-446655440000",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "scope": "openid profile email"
}

3.6 Mock 服务器的扩展与调试技巧

在实际调试过程中,可以根据需要为 Mock 服务器增加以下能力:

扩展场景 实现方式 调试价值
模拟 Token 过期 在 /token 响应中设置 expires_in: 5(5 秒) 测试客户端的 Token 刷新逻辑
模拟错误响应 在 /authorize 或 /token 中添加查询参数 ?error=access_denied 验证客户端的错误处理分支
返回 JWT 格式 Token 使用 jsonwebtoken 库生成签名 Token 测试 API 网关的 JWT 验证逻辑
持久化授权码 将 authCodeStore 替换为 SQLite 或 JSON 文件 调试跨进程或跨机器场景
安全提醒: 此 Mock 服务器仅用于本地开发与调试,严禁在生产环境或公网暴露。它没有实现 PKCE、客户端凭证验证、CSRF 保护等安全机制。在集成测试环境中,请使用专业的 OAuth 2.0 模拟工具(如 OAuth2 Mock Server、WireMock 等)。

3.7 本章小结

通过本章的学习,你已经在本地成功搭建了一个最小可用的 Mock OAuth 授权服务器。它实现了授权码模式的两个核心端点:/authorize 和 /token,并具备以下特点:

  • 零外部依赖: 仅使用 Express 和两个轻量级 npm 包。
  • 可预测行为: 每次请求返回一致的响应结构,便于调试。
  • 易于扩展: 可根据测试需求快速添加错误模拟、Token 过期等场景。
  • 依赖隔离: 使客户端开发不再受真实授权服务器的网络或配置影响。