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 服务器跳过了“用户登录”步骤,直接生成授权码。
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 过期等场景。
- 依赖隔离: 使客户端开发不再受真实授权服务器的网络或配置影响。