项目目标
本项目从零实现 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 };