mcpField
J
jwt

jwt

github.com/claude-dev-suite安全4637 次安装2 天前更新
代码仓库

关于 jwt

面向 Node.js 应用的 JWT 认证参考技能,涵盖令牌处理、验证、轮换、撤销及生产环境安全实践。

主要功能

  1. 01JWT 结构与声明
  2. 02Node.js 令牌生成与中间件
  3. 03安全存储与算法建议
  4. 04刷新令牌轮换与撤销
  5. 05常见令牌问题排查

适用场景

  1. 01构建 Bearer 令牌认证
  2. 02审查 JWT 存储与过期策略
  3. 03添加刷新令牌轮换和撤销

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill jwt

安装后在 Claude Code 中输入/jwt确认已加载。

附带文件quick-ref/implementation.mdquick-ref/security.md

详细介绍

JWT 核心知识

深度知识:使用 mcp__documentation__fetch_docs,并指定技术:jwt,获取完整文档。

令牌结构

header.payload.signature

Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "1234", "name": "John", "iat": 1516239022 }
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)

Node.js 实现

import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET!;

// Generate token
function generateToken(user: User): string {
  return jwt.sign(
    { sub: user.id, email: user.email },
    SECRET,
    { expiresIn: '1h' }
  );
}

// Verify token
function verifyToken(token: string): JwtPayload {
  return jwt.verify(token, SECRET) as JwtPayload;
}

// Refresh token pattern
function generateRefreshToken(user: User): string {
  return jwt.sign(
    { sub: user.id, type: 'refresh' },
    SECRET,
    { expiresIn: '7d' }
  );
}

中间件

const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  const token = authHeader.split(' ')[1];
  try {
    req.user = verifyToken(token);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

不要使用此技能的情况

  • 基于会话的身份验证 - 使用传统的服务端会话和 Cookie
  • OAuth 2.0 流程 - 使用 oauth2 技能进行第三方身份验证
  • NextAuth.js - 使用 nextauth 技能进行 Next.js 身份验证
  • 简单的内部 API - API 密钥可能就足够了

最佳实践

应做不应做
使用 HTTPS存储在 localStorage 中(使用 httpOnly Cookie)
设置较短的有效期(15 分钟至 1 小时)在载荷中放入敏感数据
验证所有声明使用弱密钥
使用刷新令牌忽略过期时间

反模式

反模式不良之处正确做法
将 JWT 存储在 localStorage 中容易受到 XSS 攻击使用 httpOnly Cookie
长期有效的访问令牌泄露后会带来安全风险设置 15 分钟有效期 + 使用刷新令牌
弱密钥(< 32 字节)容易遭到暴力破解使用 256 位随机密钥
忽略算法验证可能遭受算法混淆攻击明确指定允许的算法
在载荷中放入密码令牌经过 Base64 编码,并未加密仅包含非敏感声明
没有令牌撤销机制无法让用户登出实现黑名单或令牌版本控制

快速故障排除

问题原因解决方案
"Invalid signature"密钥或算法错误确认 JWT_SECRET 匹配,并检查算法
"Token expired"exp 声明已过期实现刷新令牌流程
"Missing token"未发送 Authorization 标头检查 Authorization: Bearer <token>
无法识别令牌令牌格式错误确认格式为 header.payload.signature
使用 Cookie 时发生 CORS 错误SameSite/Secure 标志设置不当设置 sameSite:'strict', secure:true
登出不起作用令牌是无状态的使用 Redis/DB 实现撤销机制

标准声明

声明用途
sub主题(用户 ID)
iat签发时间
exp过期时间
iss签发者
aud受众

生产环境就绪情况

安全配置
// Use asymmetric keys (RS256) for production
import * as jose from 'jose';

// Generate key pair (run once, store securely)
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem

const privateKey = await jose.importPKCS8(
  process.env.JWT_PRIVATE_KEY!,
  'RS256'
);
const publicKey = await jose.importSPKI(
  process.env.JWT_PUBLIC_KEY!,
  'RS256'
);

// Sign token
async function generateToken(user: User): Promise<string> {
  return new jose.SignJWT({
    sub: user.id,
    email: user.email,
  })
    .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
    .setIssuedAt()
    .setIssuer(process.env.JWT_ISSUER!)
    .setAudience(process.env.JWT_AUDIENCE!)
    .setExpirationTime('15m')  // Short-lived access token
    .sign(privateKey);
}

// Verify token
async function verifyToken(token: string): Promise<jose.JWTPayload> {
  const { payload } = await jose.jwtVerify(token, publicKey, {
    issuer: process.env.JWT_ISSUER!,
    audience: process.env.JWT_AUDIENCE!,
  });
  return payload;
}
安全的令牌存储
// Server-side: HttpOnly cookie for access token
res.cookie('access_token', token, {
  httpOnly: true,     // Prevents XSS access
  secure: true,       // HTTPS only
  sameSite: 'strict', // CSRF protection
  maxAge: 15 * 60 * 1000, // 15 minutes
  path: '/',
});

// Refresh token in separate cookie
res.cookie('refresh_token', refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
  path: '/api/auth/refresh', // Only sent to refresh endpoint
});
令牌轮换与撤销
// Refresh token rotation
async function refreshTokens(refreshToken: string) {
  // Verify refresh token
  const payload = await verifyRefreshToken(refreshToken);

  // Check if refresh token is in blacklist (revoked)
  if (await isTokenRevoked(refreshToken)) {
    throw new Error('Token revoked');
  }

  // Revoke old refresh token
  await revokeToken(refreshToken);

  // Generate new tokens
  const user = await db.users.findUnique({ where: { id: payload.sub } });
  return {
    accessToken: await generateToken(user),
    refreshToken: await generateRefreshToken(user),
  };
}

// Token revocation with Redis
async function revokeToken(token: string): Promise<void> {
  const payload = await jose.decodeJwt(token);
  const ttl = payload.exp! - Math.floor(Date.now() / 1000);
  if (ttl > 0) {
    await redis.set(`revoked:${token}`, '1', 'EX', ttl);
  }
}

// Logout: revoke all user tokens
async function logoutAll(userId: string): Promise<void> {
  // Increment user's token version, invalidating all existing tokens
  await db.users.update({
    where: { id: userId },
    data: { tokenVersion: { increment: 1 } },
  });
}
算法安全
// NEVER allow 'none' algorithm
// ALWAYS specify allowed algorithms explicitly
const { payload } = await jose.jwtVerify(token, publicKey, {
  algorithms: ['RS256'], // Only allow RS256
  issuer: process.env.JWT_ISSUER!,
  audience: process.env.JWT_AUDIENCE!,
});

// Validate token type to prevent token confusion
if (payload.type !== 'access') {
  throw new Error('Invalid token type');
}
监控指标
指标告警阈值
令牌验证失败次数> 100/分钟
刷新令牌重复使用尝试次数> 10/分钟
过期令牌请求次数> 500/分钟
签名无效错误次数> 50/分钟
声明验证
async function validateTokenClaims(payload: jose.JWTPayload): Promise<void> {
  // Check required claims
  if (!payload.sub || !payload.iat || !payload.exp) {
    throw new Error('Missing required claims');
  }

  // Check user still exists and is active
  const user = await db.users.findUnique({ where: { id: payload.sub } });
  if (!user || !user.isActive) {
    throw new Error('User not found or inactive');
  }

  // Check token version (for logout-all functionality)
  if (payload.tokenVersion !== user.tokenVersion) {
    throw new Error('Token invalidated');
  }
}
检查清单
  • 在生产环境中使用 RS256(非对称)
  • 设置较短的访问令牌有效期(15 分钟)
  • 使用带轮换机制的刷新令牌
  • 使用 HttpOnly Cookie(而非 localStorage)
  • 设置 Secure + SameSite Cookie 标志
  • 配置令牌撤销机制
  • 验证签发者和受众
  • 明确指定允许的算法
  • 包含用于“全部登出”的令牌版本
  • 监控验证失败情况
  • 对令牌端点实施速率限制

参考文档

评论

支持 Markdown