← 中文资源

简体中文版本

构建极简 JWT 认证服务

用 HMAC-SHA256 签发与校验 JWT、保护路由、强制过期与签发者声明,并对每种失败返回精确错误。

阅读英文原文

项目目标

本项目从零实现 JWT 的紧凑序列化,让签名、编码与时间校验逐层可见,再用测试钉死每种失败模式。服务在登录时签发令牌,在受保护路由上校验,并把过期、被篡改与密钥错误区分为不同错误码。

核心特性

  • 签发带 subject、issuer 与过期时间的令牌
  • 在受保护请求上校验签名、过期与签发者
  • 区分被篡改与密钥错误的具体错误码
  • 通过登录端点暴露令牌签发

架构与分层

声明模块构建与校验声明集;签发模块用 node:crypto 生成紧凑 JWS;校验模块以恒定时间比对签名再检查时间声明;中间件把 subject 附加到请求。不使用第三方 JWT 库,使每一步可读。

实施阶段

六个阶段为:base64url 辅助、声明构建、签名、校验、受保护路由中间件、服务与测试。任务以恒定时间比对是本阶段的关键。

测试与验收

用 vitest 运行。签名测试断言三段结构且改动一字节即失效;校验测试覆盖有效、过期、签发者错误与密钥错误,各断言不同错误码;时序测试断言使用恒定时间比较。

延伸方向

基于角色的声明扩展声明集并加一条中间件检查;刷新令牌是第二个更长有效期的签名产物;把 HMAC 换成非对称签名只替换签发与校验原语。

本译文由 AI 依据英文原文完整重写,覆盖目标、特性、架构、分阶段实现、测试与扩展方向,供中文读者对照英文原稿阅读。

代码示例

import { createHmac } from 'node:crypto';

export function sign(payload, secret, { header = { alg: 'HS256', typ: 'JWT' } } = {}) {
  const encodedHeader = encodeJson(header);
  const encodedPayload = encodeJson(payload);
  const data = encodedHeader + '.' + encodedPayload;
  const signature = createHmac('sha256', secret).update(data).digest('base64url');
  return data + '.' + signature;
}
mkdir jwt-service && cd jwt-service
npm init -y
npm install -D vitest@^2.1.0
{
  "name": "jwt-service",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "test": "vitest run"
  }
}
export function encodeJson(value) {
  return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
}

export function decodeJson(segment) {
  const json = Buffer.from(segment, 'base64url').toString('utf8');
  return JSON.parse(json);
}
export function buildClaims(subject, { issuer, ttlSeconds = 900, now = Date.now() }) {
  const iat = Math.floor(now / 1000);
  return { sub: subject, iss: issuer, iat, exp: iat + ttlSeconds };
}

export function validateClaims(claims, { issuer, now = Date.now(), clockSkew = 30 }) {
  if (claims.iss !== issuer) return { ok: false, code: 'bad_issuer' };
  const seconds = Math.floor(now / 1000);
  if (typeof claims.exp !== 'number' || seconds > claims.exp + clockSkew) {
    return { ok: false, code: 'expired' };
  }
  return { ok: true };
}
import { createHmac } from 'node:crypto';
import { encodeJson } from './base64url.js';

export function sign(payload, secret) {
  const header = encodeJson({ alg: 'HS256', typ: 'JWT' });
  const body = encodeJson(payload);
  const data = header + '.' + body;
  const signature = createHmac('sha256', secret).update(data).digest('base64url');
  return data + '.' + signature;
}
import { createHmac, timingSafeEqual } from 'node:crypto';
import { decodeJson } from './base64url.js';
import { validateClaims } from './claims.js';

export function verify(token, secret, { issuer } = {}) {
  const parts = token.split('.');
  if (parts.length !== 3) return { ok: false, code: 'malformed' };
  const data = parts[0] + '.' + parts[1];
  const expected = createHmac('sha256', secret).update(data).digest();
  const provided = Buffer.from(parts[2], 'base64url');
  if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
    return { ok: false, code: 'bad_signature' };
  }
  const claims = decodeJson(parts[1]);
  const check = validateClaims(claims, { issuer });
  return check.ok ? { ok: true, claims } : check;
}
import { verify } from './verify.js';

export function authenticate(req, { secret, issuer }) {
  const header = req.headers.authorization ?? '';
  const match = /^Bearer (.+)$/.exec(header);
  if (!match) return { ok: false, status: 401, body: { error: 'missing_token' } };
  const result = verify(match[1], secret, { issuer });
  if (!result.ok) return { ok: false, status: 401, body: { error: result.code } };
  return { ok: true, subject: result.claims.sub };
}
import { createServer } from 'node:http';
import { sign } from './sign.js';
import { buildClaims } from './claims.js';
import { authenticate } from './middleware.js';

const SECRET = process.env.JWT_SECRET ?? 'dev-only-secret';
const ISSUER = 'jwt-service';

const server = createServer((req, res) => {
  if (req.url === '/login' && req.method === 'POST') {
    const claims = buildClaims('user-1', { issuer: ISSUER });
    return send(res, 200, { token: sign(claims, SECRET) });
  }
  if (req.url === '/me') {
    const auth = authenticate(req, { secret: SECRET, issuer: ISSUER });
    return auth.ok ? send(res, 200, { sub: auth.subject }) : send(res, auth.status, auth.body);
  }
  send(res, 404, { error: 'not_found' });
});

function send(res, status, body) {
  res.writeHead(status, { 'content-type': 'application/json' });
  res.end(JSON.stringify(body));
}

server.listen(process.env.PORT ?? 3000);
export { server };