JWT 认证安全最佳实践 + OAuth2 vs SAML

Choyeon· 2026年9月12日· 2 分钟阅读· 133 阅读· 590 字· 2,317 字符· 更新于 2026年10月1日
JWT 认证安全最佳实践 + OAuth2 vs SAML

JWT 因其无状态、跨服务易用被广泛采用,但配置不严谨会成攻击重灾区。现代系统常需第三方登录或企业 SSO,选对联邦协议也是架构师必修课。

JWT 安全实现要点

绝对不要使用对称密钥 HMAC 直接在前端签发(密钥会泄漏),生产用 RS256/ES256 非对称,公钥公开验证。严格白名单允许算法、用 timingSafeEqual 做 iss/aud 对比防时序攻击,jti 黑名单支持吊销。

import { createSign, createVerify, randomBytes, timingSafeEqual } from "node:crypto";

export interface JwtConfig {
    algorithm: "RS256";
    privateKeyPem: string;
    publicKeyPem: string;
    issuer: string;
    audience: string;
    accessTokenTtlSec: number;
    allowedAlgos: readonly string[];
}
export interface TokenPayload {
    sub: string; scopes: readonly string[]; jti: string;
    exp: number; nbf: number; iat: number; iss: string; aud: string;
}
const B64 = {
    enc: (b: Buffer) => b.toString("base64url"),
    dec: (s: string) => Buffer.from(s, "base64url"),
};
const DENIED = new Set<string>();

export function createJwt(cfg: JwtConfig, claims: Partial<TokenPayload> & { sub: string; scopes: readonly string[] }): string {
    const now = Math.floor(Date.now() / 1000);
    const header = { alg: cfg.algorithm, typ: "JWT", kid: "k1" };
    const payload: TokenPayload = {
        sub: claims.sub, scopes: claims.scopes,
        jti: randomBytes(16).toString("hex"),
        iat: now, nbf: now, exp: now + cfg.accessTokenTtlSec,
        iss: cfg.issuer, aud: cfg.audience,
    };
    const input = `${B64.enc(Buffer.from(JSON.stringify(header)))}.${B64.enc(Buffer.from(JSON.stringify(payload)))}`;
    const s = createSign("RSA-SHA256"); s.update(input);
    return `${input}.${B64.enc(s.sign(cfg.privateKeyPem))}`;
}

export function verifyJwt(cfg: JwtConfig, token: string): TokenPayload {
    const parts = token.split(".");
    if (parts.length !== 3) throw new Error("malformed");
    const [hB64, pB64, sB64] = parts;
    const header = JSON.parse(B64.dec(hB64).toString());
    if (!cfg.allowedAlgos.includes(header.alg) || header.alg === "none") throw new Error("alg not allowed");
    const ver = createVerify("RSA-SHA256"); ver.update(`${hB64}.${pB64}`);
    if (!ver.verify(cfg.publicKeyPem, B64.dec(sB64))) throw new Error("signature mismatch");
    const p = JSON.parse(B64.dec(pB64).toString()) as TokenPayload;
    if (!timingSafeEqual(Buffer.from(p.iss), Buffer.from(cfg.issuer))) throw new Error("iss");
    if (p.aud !== cfg.audience) throw new Error("aud");
    const now = Math.floor(Date.now() / 1000);
    if (p.exp < now) throw new Error("expired");
    if (p.nbf > now) throw new Error("not yet valid");
    if (DENIED.has(p.jti)) throw new Error("revoked");
    return p;
}

OAuth2 / OIDC / SAML 对比

OAuth2 是授权框架不是认证协议,OpenID Connect 在其之上加了 id_token 才是登录。SAML 2.0 是传统企业联邦协议,XML 格式兼容性强但移动端体验差。

特性 OAuth2 + PKCE OIDC (基于OAuth2) SAML 2.0
本质 授权(delegation) 认证(authentication) 认证+授权联邦
适用 移动端/SPA Web/移动/API 企业内部SSO
Token access_token access_token + id_token(JWT) SAML Assertion(XML)
安全性 需PKCE防code注入 原生支持nonce 签名XML流程复杂
移动端体验 好(App转场) 好 差(重定向多)
集成复杂度 中 低 高(双方元数据)

最佳实践

JWT 存敏感信息:短 TTL(5-15min) + refresh_token 轮换。SPA 用 HttpOnly Cookie 承载 refresh_token 避免 XSS。第三方登录场景统一用 OIDC。

本文作者

评论 (0)

暂无评论,来抢沙发吧。