首页 › tutorials › JWT认证:Token到OAuth2实战

Claude Code 认证鉴权实战:从 JWT 到 OAuth2.0 四步交付

学习目标:掌握使用 Claude Code 从零构建完整的认证鉴权系统——涵盖密码哈希、JWT 签发验证、Refresh Token 轮转、OAuth2.0 授权码流程,以及 RBAC 权限控制。读完即可在项目中直接复用。

适用人群:有 Node.js 基础、正在使用 Claude Code 进行后端开发的中级开发者。

前置条件

项目 要求
Node.js ≥ 20.x
Claude Code 最新版(claude update)
数据库 SQLite(教程内使用,生产换 PostgreSQL)
浏览器 用于 OAuth2 回调测试

在项目根目录准备好 CLAUDE.md,写入以下内容让 Claude Code 了解项目上下文:

# 项目说明
- 技术栈:Node.js + Express + SQLite
- 认证方案:JWT + OAuth2.0
- 密码哈希:argon2
- Token 存储:httpOnly cookie
- 项目结构遵循 MVC 分层

第一步:项目脚手架与密码安全

操作说明

在终端中启动 Claude Code,输入以下提示词:

帮我初始化一个 Express 项目,要求:
1. 使用 ES Module(type: module)
2. 安装 argon2、jsonwebtoken、cookie-parser、cors、dotenv
3. 创建以下目录结构:
   src/
   ├── routes/auth.js
   ├── controllers/authController.js
   ├── services/authService.js
   ├── middleware/auth.js
   ├── models/user.js
   └── utils/token.js
4. 生成 .env 文件,包含 JWT_SECRET 和 PORT
5. 创建 server.js 入口文件

Claude Code 会一次性生成完整的项目骨架。核心的密码注册逻辑如下:

// src/services/authService.js
import argon2 from 'argon2';
import db from '../models/user.js';

export async function register({ email, password, name }) {
  // 检查邮箱唯一性
  const existing = db.findByEmail(email);
  if (existing) {
    throw new Error('EMAIL_DUPLICATE');
  }

  // argon2id 是 OWASP 推荐的密码哈希算法
  const hash = await argon2.hash(password, {
    type: argon2.argon2id,
    memoryCost: 65536,   // 64 MB
    timeCost: 3,          // 3 次迭代
    parallelism: 4        // 4 线程
  });

  const user = db.create({ email, passwordHash: hash, name });
  return { id: user.id, email: user.email, name: user.name };
}

export async function verifyPassword(hash, plain) {
  return argon2.verify(hash, plain);
}

预期结果

运行 npm run dev 后,服务器在 localhost:3000 启动。用 curl 测试注册:

curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"Str0ng!Pass","name":"Test"}'

返回:

{"user":{"id":1,"email":"[email protected]","name":"Test"}}

常见问题

问题 原因 解决
argon2 安装报错 缺少编译工具链 macOS: xcode-select --install; Linux: apt install build-essential
EMAIL_DUPLICATE 邮箱已注册 前端做邮箱格式校验 + 后端捕获错误返回 409
密码明文入库 忘记调用 hash 永远不要存明文,用 argon2.hash 处理

第二步:JWT 签发与验证中间件

操作说明

这一步是整个认证系统的核心。向 Claude Code 输入:

实现 JWT 认证,要求:
1. Access Token: 15分钟过期,存在 httpOnly cookie
2. Refresh Token: 7天过期,存在 httpOnly cookie,同时存入数据库
3. 登录接口 POST /api/auth/login
4. Token 刷新接口 POST /api/auth/refresh
5. 登出接口 POST /api/auth/logout(清除 cookie + 吊销 refresh token)
6. 认证中间件,从 cookie 取 token 验证,将 user 信息挂载到 req.user

Claude Code 生成的 Token 工具模块:

// src/utils/token.js
import jwt from 'jsonwebtoken';

const ACCESS_SECRET = process.env.JWT_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
const ACCESS_EXPIRY = '15m';
const REFRESH_EXPIRY = '7d';

export function signAccessToken(payload) {
  return jwt.sign(payload, ACCESS_SECRET, {
    expiresIn: ACCESS_EXPIRY,
    issuer: 'myapp',
    audience: 'myapp-users'
  });
}

export function signRefreshToken(payload) {
  return jwt.sign(payload, REFRESH_SECRET, {
    expiresIn: REFRESH_EXPIRY,
    issuer: 'myapp',
    audience: 'myapp-refresh'
  });
}

export function verifyAccessToken(token) {
  return jwt.verify(token, ACCESS_SECRET, {
    issuer: 'myapp',
    audience: 'myapp-users'
  });
}

export function verifyRefreshToken(token) {
  return jwt.verify(token, REFRESH_SECRET, {
    issuer: 'myapp',
    audience: 'myapp-refresh'
  });
}

认证中间件:

// src/middleware/auth.js
import { verifyAccessToken } from '../utils/token.js';

export function requireAuth(req, res, next) {
  const token = req.cookies?.access_token;
  if (!token) {
    return res.status(401).json({ error: 'NO_TOKEN', message: '未登录' });
  }

  try {
    const decoded = verifyAccessToken(token);
    req.user = { id: decoded.sub, email: decoded.email, role: decoded.role };
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'TOKEN_EXPIRED', message: 'Token 已过期' });
    }
    return res.status(401).json({ error: 'INVALID_TOKEN', message: '无效 Token' });
  }
}

// RBAC 权限检查
export function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: 'UNAUTHORIZED' });
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'FORBIDDEN', message: '权限不足' });
    }
    next();
  };
}

Cookie 设置逻辑(在 Controller 中):

// src/controllers/authController.js
const COOKIE_OPTIONS = {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict',
  path: '/'
};

const ACCESS_COOKIE = { ...COOKIE_OPTIONS, maxAge: 15 * 60 * 1000 };      // 15 分钟
const REFRESH_COOKIE = { ...COOKIE_OPTIONS, maxAge: 7 * 24 * 60 * 60 * 1000, path: '/api/auth/refresh' };

function setTokenCookies(res, { accessToken, refreshToken }) {
  res.cookie('access_token', accessToken, ACCESS_COOKIE);
  res.cookie('refresh_token', refreshToken, REFRESH_COOKIE);
}

function clearTokenCookies(res) {
  res.clearCookie('access_token', { path: '/' });
  res.clearCookie('refresh_token', { path: '/api/auth/refresh' });
}

预期结果

# 登录
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"email":"[email protected]","password":"Str0ng!Pass"}'

# 访问受保护资源
curl http://localhost:3000/api/me -b cookies.txt

# 刷新 Token
curl -X POST http://localhost:3000/api/auth/refresh -b cookies.txt -c cookies.txt

常见问题

问题 原因 解决
Cookie 不生效 前后端跨域 CORS 配置 credentials: true,前端 fetch 加 credentials: 'include'
Token 过期后无限重定向 前端没有区分 401 错误类型 检查 TOKEN_EXPIRED 错误码,自动触发 refresh
Refresh Token 被盗用 无绑定机制 加入设备指纹或 IP 绑定,异常时吊销全部 refresh token

第三步:OAuth2.0 授权码流程

操作说明

向 Claude Code 输入:

实现 OAuth2.0 授权码流程(GitHub OAuth),要求:
1. GET /api/auth/github — 重定向到 GitHub 授权页
2. GET /api/auth/github/callback — 处理回调,换取 access_token,获取用户信息
3. 如果用户已注册,直接登录;如果未注册,自动创建账号
4. 用 state 参数防 CSRF
5. 在 .env 中添加 GITHUB_CLIENT_ID 和 GITHUB_CLIENT_SECRET

Claude Code 生成的 OAuth 路由:

// src/routes/oauth.js
import { Router } from 'express';
import crypto from 'crypto';
import { getGithubUser, findOrCreateUser } from '../services/oauthService.js';
import { signAccessToken, signRefreshToken } from '../utils/token.js';

const router = Router();

// 内存存储 state(生产环境用 Redis)
const states = new Map();

router.get('/github', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  states.set(state, { createdAt: Date.now() });

  const params = new URLSearchParams({
    client_id: process.env.GITHUB_CLIENT_ID,
    redirect_uri: `${process.env.BASE_URL}/api/auth/github/callback`,
    scope: 'user:email',
    state
  });

  res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});

router.get('/github/callback', async (req, res) => {
  const { code, state } = req.query;

  // 验证 state
  if (!state || !states.has(state)) {
    return res.status(400).json({ error: 'INVALID_STATE' });
  }
  states.delete(state);

  try {
    // 用 code 换 access_token
    const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
      body: JSON.stringify({
        client_id: process.env.GITHUB_CLIENT_ID,
        client_secret: process.env.GITHUB_CLIENT_SECRET,
        code
      })
    });
    const { access_token } = await tokenRes.json();

    // 用 access_token 获取用户信息
    const ghUser = await getGithubUser(access_token);
    const user = await findOrCreateUser(ghUser);

    // 签发 JWT
    const accessToken = signAccessToken({ sub: user.id, email: user.email, role: user.role });
    const refreshToken = signRefreshToken({ sub: user.id });

    setTokenCookies(res, { accessToken, refreshToken });
    res.redirect(process.env.FRONTEND_URL || '/');
  } catch (err) {
    res.redirect(`${process.env.FRONTEND_URL}/login?error=oauth_failed`);
  }
});

export default router;

OAuth 服务层:

// src/services/oauthService.js
import db from '../models/user.js';

export async function getGithubUser(accessToken) {
  const [profileRes, emailsRes] = await Promise.all([
    fetch('https://api.github.com/user', {
      headers: { Authorization: `Bearer ${accessToken}` }
    }),
    fetch('https://api.github.com/user/emails', {
      headers: { Authorization: `Bearer ${accessToken}` }
    })
  ]);

  const profile = await profileRes.json();
  const emails = await emailsRes.json();
  const primaryEmail = emails.find(e => e.primary)?.email;

  return {
    githubId: profile.id,
    name: profile.name || profile.login,
    email: primaryEmail,
    avatar: profile.avatar_url
  };
}

export async function findOrCreateUser(ghUser) {
  // 先按 GitHub ID 查找
  let user = db.findByGithubId(ghUser.githubId);
  if (user) return user;

  // 再按邮箱查找(已有账号绑定)
  user = db.findByEmail(ghUser.email);
  if (user) {
    db.updateGithubId(user.id, ghUser.githubId);
    return user;
  }

  // 新用户注册
  return db.create({
    email: ghUser.email,
    name: ghUser.name,
    avatar: ghUser.avatar,
    githubId: ghUser.githubId,
    passwordHash: null,   // OAuth 用户无密码
    role: 'user'
  });
}

预期结果

  1. 访问 http://localhost:3000/api/auth/github,重定向到 GitHub 授权页
  2. 授权后回调到 /api/auth/github/callback?code=xxx&state=yyy
  3. 自动完成注册/登录,设置 JWT cookie,重定向到前端页面

常见问题

问题 原因 解决
redirect_uri_mismatch GitHub App 配置的回调地址与代码不一致 在 GitHub Settings → Developer settings 中检查
用户邮箱为 null GitHub 默认不暴露私有邮箱 请求 user:email scope 并调用 emails API
state 验证失败 用户刷新页面或多次回调 state 加过期时间,5 分钟后自动失效

第四步:RBAC 权限控制与安全加固

操作说明

向 Claude Code 输入:

实现 RBAC 权限系统,要求:
1. 用户角色:admin / editor / user
2. 权限表:
   - admin: 所有操作
   - editor: 读写自己创建的内容
   - user: 只读
3. 数据库 users 表增加 role 字段,默认 'user'
4. 新增中间件 requireOwnership(model, paramIdField)
   用于检查资源是否属于当前用户
5. 管理接口 PUT /api/admin/users/:id/role — 修改角色
   仅 admin 可访问

资源归属检查中间件:

// src/middleware/ownership.js
export function requireOwnership(getOwner) {
  return async (req, res, next) => {
    if (req.user.role === 'admin') return next(); // admin 跳过检查

    const ownerId = await getOwner(req);
    if (req.user.id !== ownerId) {
      return res.status(403).json({ error: 'NOT_OWNER', message: '只能操作自己的资源' });
    }
    next();
  };
}

// 使用示例
import { requireAuth, requireRole } from '../middleware/auth.js';
import { requireOwnership } from '../middleware/ownership.js';

// 只有 admin 能修改角色
router.put('/admin/users/:id/role',
  requireAuth,
  requireRole('admin'),
  adminController.updateRole
);

// 编辑者只能修改自己的文章
router.put('/articles/:id',
  requireAuth,
  requireRole('admin', 'editor'),
  requireOwnership((req) => Article.getOwnerId(req.params.id)),
  articleController.update
);

安全响应头中间件:

// src/middleware/security.js
export function securityHeaders(req, res, next) {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '0');   // 现代浏览器不推荐开启
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.removeHeader('X-Powered-By');
  next();
}

预期结果

# 普通用户尝试修改角色 → 403
curl -X PUT http://localhost:3000/api/admin/users/2/role \
  -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"role":"admin"}'
# {"error":"FORBIDDEN","message":"权限不足"}

# admin 修改角色 → 200
curl -X PUT http://localhost:3000/api/admin/users/2/role \
  -b admin-cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"role":"editor"}'
# {"user":{"id":2,"role":"editor"}}

实战案例:完整认证系统的项目结构

把以上四步整合为一个可运行的项目:

auth-demo/
├── .env
├── package.json
├── server.js
├── src/
│   ├── routes/
│   │   ├── auth.js          # 注册、登录、登出、刷新
│   │   └── oauth.js         # GitHub OAuth 回调
│   ├── controllers/
│   │   └── authController.js
│   ├── services/
│   │   ├── authService.js   # 密码哈希、用户查找
│   │   └── oauthService.js  # GitHub API 调用
│   ├── middleware/
│   │   ├── auth.js          # requireAuth, requireRole
│   │   ├── ownership.js     # 资源归属检查
│   │   └── security.js      # 安全头
│   ├── models/
│   │   └── user.js          # SQLite 用户模型
│   ├── utils/
│   │   └── token.js         # JWT 签发/验证
│   └── db/
│       └── init.js          # 建表脚本
└── CLAUDE.md

数据库建表脚本:

// src/db/init.js
import Database from 'better-sqlite3';

const db = new Database('auth.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    password_hash TEXT,              -- OAuth 用户为 NULL
    name TEXT NOT NULL,
    avatar TEXT,
    github_id INTEGER UNIQUE,
    role TEXT NOT NULL DEFAULT 'user',
    created_at TEXT DEFAULT (datetime('now')),
    updated_at TEXT DEFAULT (datetime('now'))
  );

  CREATE TABLE IF NOT EXISTS refresh_tokens (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL REFERENCES users(id),
    token_hash TEXT NOT NULL UNIQUE,
    expires_at TEXT NOT NULL,
    revoked_at TEXT,
    created_at TEXT DEFAULT (datetime('now'))
  );

  CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id);
  CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash);
`);

export default db;

启动命令:

# 安装依赖
npm install express argon2 jsonwebtoken cookie-parser cors dotenv better-sqlite3

# 启动开发
node server.js

架构流程图

整个认证系统的请求流转如下:

sequenceDiagram
    participant C as Client
    participant S as Server
    participant DB as SQLite
    participant GH as GitHub OAuth

    Note over C,GH: 场景1:邮箱密码注册登录
    C->>S: POST /api/auth/register {email, password}
    S->>DB: argon2.hash(password) → 存储
    S-->>C: {user}

    C->>S: POST /api/auth/login {email, password}
    S->>DB: 查找用户,argon2.verify
    S->>S: signAccessToken + signRefreshToken
    S-->>C: Set-Cookie: access_token, refresh_token

    C->>S: GET /api/me (带 cookie)
    S->>S: verifyAccessToken → req.user
    S-->>C: {user profile}

    Note over C,GH: 场景2:Token 刷新轮转
    C->>S: POST /api/auth/refresh (带 refresh_token cookie)
    S->>DB: 查找 token_hash,检查未吊销
    S->>DB: 吊销旧 refresh token
    S->>S: 签发新 access + refresh token
    S-->>C: Set-Cookie: 新 token pair

    Note over C,GH: 场景3:GitHub OAuth 登录
    C->>S: GET /api/auth/github
    S-->>C: 302 → GitHub 授权页
    C->>GH: 授权确认
    GH-->>C: 302 → /callback?code=xxx&state=yyy
    C->>S: GET /api/auth/github/callback
    S->>GH: POST access_token (换 code)
    S->>GH: GET /api/github/user
    S->>DB: findOrCreateUser
    S->>S: 签发 JWT
    S-->>C: 302 → 前端首页 (带 cookie)

    Note over C,GH: 场景4:RBAC 权限检查
    C->>S: PUT /api/admin/users/:id/role
    S->>S: requireAuth → requireRole('admin')
    alt role = admin
        S->>DB: UPDATE role
        S-->>C: 200 {user}
    else role ≠ admin
        S-->>C: 403 FORBIDDEN
    end

Token 生命周期管理:

stateDiagram-v2
    [*] --> Issued: 用户登录
    Issued --> Active: 首次使用
    Active --> Renewing: access_token 过期
    Renewing --> Active: refresh 成功,新 token pair
    Renewing --> Expired: refresh token 也过期
    Active --> Revoked: 用户登出 / 安全事件
    Expired --> [*]: 需重新登录
    Revoked --> [*]: 需重新登录

Claude Code 提效技巧

在构建认证系统时,以下 Claude Code 用法特别有效:

1. 分层生成,逐层验证

不要一次性让 Claude Code 生成所有代码。按层生成:

第一步:生成 models/user.js 和 db/init.js → 运行 node -e "require('./src/db/init.js')" 验证建表
第二步:生成 services/authService.js → 写测试验证注册逻辑
第三步:生成 routes + controller → 用 curl 集成测试
第四步:生成中间件 → 接入路由验证权限

2. 利用 CLAUDE.md 保持上下文

随着代码增长,在 CLAUDE.md 中追加:

## 已完成的模块
- src/db/init.js: 用户表 + refresh_tokens 表
- src/services/authService.js: register(), verifyPassword()
- src/utils/token.js: signAccessToken, verifyAccessToken
## 当前安全约定
- Access Token 15分钟过期
- Refresh Token 7天过期 + 数据库存储 + 轮转
- Cookie: httpOnly, secure, sameSite=strict

Claude Code 每次对话都会读取这个文件,保持与已有代码的一致性。

3. 让 Claude Code 写测试

为 src/services/authService.js 编写 Vitest 单元测试:
1. 测试注册成功返回用户信息
2. 测试重复邮箱抛出 EMAIL_DUPLICATE
3. 测试密码验证正确和错误两种情况
4. 使用内存 SQLite 数据库,测试完自动清理

4. 安全审计

认证系统写完后,让 Claude Code 做一次安全审查:

审查当前认证系统的安全性,检查以下方面:
1. 密码存储是否安全(哈希算法、加盐)
2. JWT 是否有可能被伪造(密钥强度、算法限制)
3. 是否存在时序攻击(timing attack)风险
4. OAuth state 是否防 CSRF
5. 是否有信息泄露(错误消息过于详细)
输出发现的问题,按严重程度排序

生产环境检查清单

检查项 状态 说明
JWT_SECRET ≥ 256 位随机 ☐ openssl rand -hex 32
argon2 参数 OWASP 合规 ☐ memoryCost≥64MB, timeCost≥3
httpOnly + secure + sameSite ☐ 生产必须 secure: true
Refresh Token 轮转 ☐ 每次刷新生成新 pair,吊销旧的
Rate Limiting ☐ 登录接口 5次/分钟/IP
CORS 白名单 ☐ 禁止 *,明确列出域名
数据库连接加密 ☐ 生产用 PostgreSQL + SSL
日志脱敏 ☐ 不记录密码、token
OAuth state 过期 ☐ 5 分钟自动失效
管理接口二次验证 ☐ 改角色需重新输入密码

训练营适用性评估

维度 评估
适合训练营类型 全栈训练营、安全训练营、Node.js 专项营
预计教学时长 4-6 小时(含实操)
难度曲线 中等,需要异步编程和 HTTP 基础
实操占比 70%(大量代码生成和 curl 测试)
延伸方向 添加 Google OAuth、多因子认证、密码重置流程

参考来源

  1. Extend Claude with skills - Claude Code Docs — Claude Code Skills 官方文档
  2. Skill authoring best practices - Claude API Docs — Anthropic 官方 Skill 编写最佳实践
  3. Claude Code Skills Complete Guide - Duet.so — Skills 生态综合指南(含 MCP、Subagents 对比)
  4. anthropics/skills - GitHub — Anthropic 官方 Skills 参考仓库
  5. SKILL.md Format Specification - DeepWiki — SKILL.md 文件格式完整规格

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入