JWT揭秘:理解基于Token的身份认证

JWT Demystified: Everything Developers Need to Know

JSON Web Tokens (JWTs) are ubiquitous in modern authentication systems. They are used in everything from OAuth 2.0 to microservice-to-microservice communication. Yet JWT misimplementation is responsible for a significant class of authentication vulnerabilities. This guide covers the JWT specification, signing algorithms, critical attacks, and best practices for building secure systems.

Decode and inspect any JWT token with our JWT Decoder.

JWT Structure: Header.Payload.Signature

A JWT consists of three base64url-encoded parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload:   eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decoded header:

{"alg": "HS256", "typ": "JWT"}

Decoded payload:

{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}

The signature is computed as:

HMACSHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

The JOSE Header

The JOSE (JSON Object Signing and Encryption) header tells the verifier how the token is signed. Key header parameters:

  • alg: Algorithm used to sign the token (REQUIRED)
  • typ: Token type, usually "JWT"
  • kid: Key ID—a hint indicating which key was used to sign the token, for key rotation
  • jku: JWK Set URL—a URL pointing to a set of JSON public keys (potential SSRF vector!)
  • x5u: X.509 URL (also a potential SSRF vector)

Standard Claims (Registered Claims)

ClaimNameDescription
issIssuerWho issued the token (e.g., "https://auth.example.com")
subSubjectWho the token is about (usually user ID)
audAudienceIntended recipients (e.g., "api.example.com")
expExpirationUnix timestamp after which the token is invalid
nbfNot BeforeUnix timestamp before which the token is invalid
iatIssued AtUnix timestamp when the token was issued
jtiJWT IDUnique identifier for the token (prevents replay attacks)

JWT is NOT Encrypted

This is one of the most important points to understand about JWTs. A signed JWT (JWS—JSON Web Signature) is encoded, not encrypted. The payload can be decoded by anyone who has the token, without knowing the secret key:

// Anyone can decode the payload!
const [header, payload, sig] = token.split('.');
const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
console.log(decoded); // All claims visible

Never put sensitive information in a JWT payload—passwords, PII, credit card numbers, or anything you wouldn't want exposed. Usernames, user IDs, roles, and permissions are acceptable.

If you need encrypted tokens, use JWE (JSON Web Encryption), which has a 5-part structure (header.key.iv.ciphertext.tag) and actually encrypts the payload.

Verify vs Decode: A Critical Distinction

// WRONG: Just decoding - NEVER do this for authentication
const payload = JSON.parse(atob(token.split('.')[1]));
// This accepts any token, even forged ones!

// CORRECT: Verify the signature first
const jwt = require('jsonwebtoken');

// Verification checks:
// 1. Signature matches (proves token wasn't tampered with)
// 2. exp claim hasn't passed
// 3. nbf claim is in the past
// 4. iss matches expected issuer
// 5. aud matches expected audience
try {
  const verified = jwt.verify(token, secret, {
    algorithms: ['HS256'], // Explicitly allow only expected algorithms
    issuer: 'https://auth.example.com',
    audience: 'api.example.com'
  });
  console.log(verified.sub); // Only access claims after successful verification
} catch (err) {
  // Token invalid, expired, or tampered
  console.error('Token verification failed:', err.message);
}

HS256 vs RS256 vs ES256

HS256 (HMAC-SHA256) uses a shared secret. The same key is used to sign and verify. This means every service that needs to verify tokens must also have the signing secret—a significant security surface. Use HS256 only when a single service both issues and verifies tokens.

RS256 (RSA + SHA256) uses asymmetric cryptography. The private key signs tokens; the public key verifies them. Services can verify tokens without access to the signing key. The JWKS endpoint exposes the public key. Downsides: RSA keys are large (2048+ bits) and RSA signing is CPU-intensive.

ES256 (ECDSA with P-256 curve + SHA256) is the modern choice. Smaller keys (256 bits) with equivalent security to RSA-3072. Faster signing and verification than RSA. Used by major OAuth providers. Always prefer ES256 or ES384 for new systems.

Critical JWT Attacks

1. Algorithm Confusion: alg:none

// Attack: Forged token with alg:none
const maliciousHeader = btoa(JSON.stringify({"alg": "none", "typ": "JWT"}));
const maliciousPayload = btoa(JSON.stringify({"sub": "admin", "role": "superuser"}));
const maliciousToken = `${maliciousHeader}.${maliciousPayload}.`;
// No signature! If the server accepts alg:none, this is a valid admin token.

// Defense: Always explicitly specify allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] }); // Never omit this!

2. RS256-to-HS256 Algorithm Confusion

// If a server uses RS256 but doesn't enforce the algorithm:
// An attacker can sign a token with the PUBLIC KEY using HS256.
// The server's RSA public key is often publicly available via JWKS.
// If the verifier uses the public key as the HS256 secret, it succeeds.

// Defense: Always pin the expected algorithm
jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // Not ['RS256', 'HS256']!

3. kid Header Injection

// If the kid parameter is used unsafely to look up keys:
// Attacker modifies kid to include SQL injection
// {"kid": "' OR 1=1 --", "alg": "HS256"}
// Or path traversal: {"kid": "/dev/null"} (empty key = trivial HMAC)

// Defense: Whitelist valid kid values, never use kid directly in DB queries

localStorage vs httpOnly Cookie: The Eternal Debate

localStorage/sessionStorage:

  • Accessible via JavaScript (localStorage.getItem('token'))
  • Vulnerable to XSS: any injected script can steal the token
  • Easy to implement with Authorization header
  • Works across subdomains easily

httpOnly Cookie:

  • Not accessible via JavaScript—immune to XSS token theft
  • Automatically sent with requests to the same domain
  • Vulnerable to CSRF (mitigated with SameSite=Strict or CSRF tokens)
  • More complex to implement for single-page apps with APIs on different domains

The security community consensus in 2026: use httpOnly, Secure, SameSite=Strict cookies for session tokens. The XSS risk of localStorage is harder to mitigate than the CSRF risk of cookies (which SameSite largely solves).

Access Token + Refresh Token Rotation

// Pattern: Short-lived access tokens + long-lived refresh tokens
{
  "accessToken": "eyJ...",      // Expires in 15 minutes
  "refreshToken": "eyJ...",     // Expires in 7 days, stored in httpOnly cookie
  "expiresIn": 900              // Seconds
}

// When access token expires:
POST /auth/refresh
Cookie: refreshToken=eyJ...

// Server issues new access token AND rotates refresh token
// Old refresh token is invalidated (detect token reuse)
{
  "accessToken": "eyJ...(new)",
  "refreshToken": "eyJ...(rotated)"
}

JWKS Endpoint and Key Rotation

// Standard JWKS endpoint
GET https://auth.example.com/.well-known/jwks.json

{
  "keys": [
    {
      "kty": "EC",
      "use": "sig",
      "crv": "P-256",
      "kid": "key-2026-04",
      "x": "...",
      "y": "..."
    }
  ]
}

// Libraries fetch and cache JWKS automatically
const jwksClient = require('jwks-rsa');
const client = jwksClient({ jwksUri: 'https://auth.example.com/.well-known/jwks.json' });

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    callback(err, key?.getPublicKey());
  });
}

For hash generation and verification tools, see our Hash Generator. Full Base64 and base64url encoding is available at our Base64 Encoder.

相关工具

🔧 jwt decoder 🔧 base64 encoder 🔧 hash generator

相关文章

Base64编码详解:开发者综合指南

Base64编码深度解析:64字符字母表的工作原理、URL安全变体、填充规则、使用场景以及JavaScript性能基准测试。…

密码安全的数学原理:熵值、哈希与暴力攻击

密码强度背后的真实数学:熵值计算、马尔可夫链攻击、bcrypt与Argon2成本因子,以及为何12个单词的密码短语胜过复杂的8字符密码。…