JWT 令牌详解:结构、安全性和最佳实践

· 12分钟阅读

JSON Web Tokens (JWT) 已成为现代 Web 应用程序中无状态身份验证的事实标准。无论您是在构建 REST API、微服务架构还是单页应用程序,了解 JWT 的工作原理对于实现安全、可扩展的身份验证都至关重要。

本综合指南详细介绍了关于 JWT 您需要了解的一切——从其内部结构到生产就绪的安全实践。读完后,您不仅会理解如何使用 JWT,还会知道何时使用它们以及如何避免导致安全漏洞的常见陷阱。

目录

什么是 JWT?

JSON Web Token (JWT) 是一种紧凑的、URL 安全的方式,用于表示要在两方之间传输的声明。JWT 中的声明被编码为 JSON 对象,并使用 JSON Web Signature (JWS) 进行数字签名。

可以将 JWT 视为用户信息的防篡改容器。当用户登录时,您的服务器会创建一个包含其用户 ID、角色和其他相关数据的 JWT。然后将此令牌发送到客户端,客户端在每个后续请求中都包含它。服务器可以验证令牌的真实性而无需查询数据库,这使得 JWT 非常适合无状态、可扩展的架构。

JWT 由 RFC 7519 定义,并在各种编程语言和框架中得到广泛支持。它们在以下场景中特别受欢迎:

快速提示:使用我们的 JWT 解码器即时检查和调试任何 JWT 令牌。它完全在客户端运行,因此您的令牌永远不会离开您的浏览器。

JWT 结构:头部、载荷和签名

JWT 由三个用点 (.) 分隔的 Base64URL 编码部分组成。以下是真实 JWT 的样子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

分解如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9           ← 头部
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ  ← 载荷
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c    ← 签名
部分 包含内容 示例(解码后)
头部 算法 + 令牌类型 {"alg": "HS256", "typ": "JWT"}
载荷 声明(用户数据) {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
签名 验证哈希 HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

头部

头部通常由两部分组成:令牌类型 (typ) 和签名算法 (alg)。常见算法包括:

载荷

载荷包含声明——关于实体(通常是用户)的陈述和附加元数据。声明不是加密的,只是编码的,因此永远不要在 JWT 载荷中放置密码等敏感信息。

签名

签名确保令牌未被篡改。它是通过获取编码的头部、编码的载荷、密钥,并应用头部中指定的算法来创建的。如果有人修改了头部或载荷,签名验证将失败。

专业提示:使用我们的 JWT 生成器工具生成带有自定义声明的测试 JWT。非常适合开发和测试场景。

标准声明 (RFC 7519)

RFC 7519 定义了几个标准声明,可在不同的 JWT 实现之间提供互操作性。虽然这些声明是可选的,但正确使用它们可以提高安全性和兼容性。

声明 名称 用途 示例
iss 签发者 标识谁创建了令牌 "https://auth.example.com"
sub 主题 标识令牌是关于谁的(通常是用户 ID) "user_12345"
aud 受众 标识令牌的目标接收者 "https://api.example.com"
exp 过期时间 令牌何时过期(Unix 时间戳) 1735689600
nbf 生效时间 令牌在此时间之前无效 1735603200
iat 签发时间 令牌创建时间 1735603200
jti JWT ID 令牌的唯一标识符(用于撤销) "a1b2c3d4-e5f6"

自定义声明

除了标准声明之外,您还可以添加特定于应用程序的自定义声明。常见示例包括:

保持载荷较小——每个字节都会增加网络开销。仅包含授权决策所需的声明。如果需要其他用户数据,请使用 sub 声明作为查找键从数据库中获取。

JWT 身份验证工作原理

了解完整的身份验证流程有助于您正确且安全地实现 JWT。以下是典型的序列:

  1. 用户登录:用户将凭据(用户名 + 密码)发送到您的身份验证端点
  2. 凭据验证:服务器根据用户数据库验证凭据
  3. 令牌生成:服务器创建包含用户声明的 JWT 并使用密钥对其进行签名
  4. 令牌传递:服务器将 JWT 返回给客户端(通常在响应正文中或作为 httpOnly cookie)
  5. 令牌存储:客户端安全地存储 JWT(更多内容请参见存储选项部分)
  6. 已验证请求:客户端在后续请求的 Authorization 头中包含 JWT
  7. 令牌验证:服务器验证签名并检查过期时间——无需数据库查询
  8. 授予访问权限:如果有效,服务器使用令牌中的声明处理请求

以下是可视化表示:

┌────────┐                                  ┌────────┐
│ 客户端 │                                  │ 服务器 │
└───┬────┘                                  └───┬────┘
    │                                           │
    │  POST /login {username, password}         │
    │──────────────────────────────────────────>│
    │                                           │
    │                                    [验证凭据]
    │                                    [生成 JWT]
    │                                           │
    │  200 OK {token: "eyJhbG..."}              │
    │<──────────────────────────────────────────│
    │                                           │
[存储令牌]                                      │
    │                                           │
    │  GET /api/profile                         │
    │  Authorization: Bearer eyJhbG...          │
    │──────────────────────────────────────────>│
    │                                           │
    │                                    [验证签名]
    │                                    [检查过期]
    │                                    [提取声明]
    │                                           │
    │  200 OK {user data}                       │
    │<──────────────────────────────────────────│
    │                                           │

专业提示:JWT 的优点在于步骤 7(令牌验证)不需要数据库查询。服务器可以直接从令牌验证真实性并提取用户信息,使您的 API 具有高度可扩展性。

实现示例

让我们看看流行编程语言和框架中的实际实现。

Node.js 与 jsonwebtoken

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// 登录端点
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // 在数据库中查找用户
  const user = await User.findOne({ username });
  if (!user) {
    return res.status(401).json({ error: '凭据无效' });
  }
  
  // 验证密码
  const validPassword = await bcrypt.compare(password, user.passwordHash);
  if (!validPassword) {
    return res.status(401).json({ error: '凭据无效' });
  }
  
  // 创建 JWT
  const token = jwt.sign(
    { 
      sub: user.id,
      email: user.email,
      role: user.role 
    },
    process.env.JWT_SECRET,
    { 
      expiresIn: '15m',
      issuer: 'https://api.example.com',
      audience: 'https://example.com'
    }
  );
  
  res.json({ token });
});

// 受保护路由中间件
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
  
  if (!token) {
    return res.status(401).json({ error: '未提供令牌' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: '令牌无效或已过期' });
  }
};

// 使用中间件
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ userId: req.user.sub, email: req.user.email });
});

Python 与 PyJWT

import jwt
import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET_KEY = 'your-secret-key'

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # 验证凭据(简化版)
    user = verify_credentials(username, password)
    if not user:
        return jsonify({'error': '凭据无效'}), 401
    
    # 创建 JWT
    payload = {
        'sub': user['id'],
        'email': user['email'],
        'role': user['role'],
        'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15),
        'iat': datetime.datetime.utcnow()
    }
    
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return jsonify({'token': token})

def token_required(f):
    def decorator(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'error': '未提供令牌'}), 401
        
        try:
            # 移除 'Bearer ' 前缀
            token = token.split(' ')[1]
            decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            request.user = decoded
        except jwt.ExpiredSignatureError:
            return jsonify({'error': '令牌已过期'}), 403
        except jwt.InvalidTokenError:
            return jsonify({'error': '令牌无效'}), 403
        
        return f(*args, **kwargs)
    
    return decorator

@app.route('/api/profile')
@token_required
def profile():
    return jsonify({'userId': request.user['sub']})

Go 与 golang-jwt

package main

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID string `json:"sub"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func generateToken(userID, email, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  emai