JWT 令牌详解:现代 Web 应用的身份验证

· 12 分钟阅读

目录

JSON Web Token(JWT)已成为现代 Web 应用身份验证的事实标准。它们提供了一种紧凑、URL 安全的方式来表示双方之间的声明,实现了无状态身份验证,无需共享会话存储即可水平扩展。

无论您是在构建 REST API、微服务架构还是单页应用,理解 JWT 对于实现安全、可扩展的身份验证都至关重要。本综合指南解释了 JWT 的工作原理、结构、安全考虑因素以及生产就绪的最佳实践。

什么是 JWT?

JWT(发音为"jot")是一个开放标准(RFC 7519),它定义了一种紧凑、自包含的格式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息使用密钥(HMAC)或公钥/私钥对(RSA 或 ECDSA)进行数字签名,确保令牌的完整性和真实性。

与传统的基于会话的身份验证(服务器在内存或数据库中维护会话状态)不同,JWT 是无状态的。验证用户身份所需的所有信息都包含在令牌本身中。

JWT 的主要用例

专业提示:使用我们的 JWT 解码器在开发过程中检查和验证令牌。它可以帮助您理解结构并在问题进入生产环境之前发现常见问题。

JWT 结构详解

JWT 由三个不同的部分组成,用点(.)分隔:header.payload.signature

完整的 JWT 如下所示:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRGV2ZWxvcGVyIiwiZW1haWwiOiJqYW5lQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzEwNTkwNDAwLCJleHAiOjE3MTA2NzY4MDB9.4Adcj0mYZ8s5vxjKvV8pF7jKX9s8vZ5xJ3kL9mN2pQ4

第一部分:头部

头部通常包含两个描述令牌加密操作的字段:

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

此 JSON 对象经过 Base64Url 编码,形成 JWT 的第一部分。编码使其 URL 安全且紧凑,便于在 HTTP 头中传输。

第二部分:载荷

载荷包含声明——关于用户和附加元数据的陈述。声明分为三种类型:

{
  "sub": "1234567890",
  "name": "Jane Developer",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1710590400,
  "exp": 1710676800
}

注册声明由 JWT 规范预定义,提供标准信息:

公共声明是应在 IANA JSON Web Token 注册表中定义或使用防冲突名称(如 URL)的自定义声明。

私有声明是各方之间约定的自定义声明,如 rolepermissionsorganizationId

重要提示:载荷仅经过 Base64Url 编码,未加密。切勿在 JWT 载荷中存储敏感信息,如密码、信用卡号或社会保险号。

第三部分:签名

签名确保令牌未被更改。它通过获取编码的头部、编码的载荷、密钥,并应用头部中指定的算法来创建:

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

签名允许接收者验证发送者是其声称的身份,以及消息在传输过程中未被更改。

签名算法比较

算法 类型 安全级别 用例 密钥管理
HS256 对称(HMAC) 良好 单一服务、内部 API 共享密钥
RS256 非对称(RSA) 很好 多服务、公共 API 公钥/私钥对
ES256 非对称(ECDSA) 优秀 高安全性要求 公钥/私钥对
PS256 非对称(RSA-PSS) 优秀 现代应用 公钥/私钥对

JWT 身份验证工作原理

理解完整的身份验证流程有助于您正确实现 JWT 并有效排查问题。

完整的身份验证流程

  1. 用户登录:客户端向身份验证端点发送凭据(用户名/密码)
  2. 凭据验证:服务器根据数据库验证凭据
  3. 令牌生成:身份验证成功后,服务器创建包含用户信息和声明的 JWT
  4. 令牌交付:服务器将 JWT 发送回客户端(通常在响应正文中)
  5. 令牌存储:客户端存储 JWT(在内存、localStorage 或 httpOnly cookie 中)
  6. 已验证请求:对于后续请求,客户端在 Authorization 头中包含 JWT
  7. 令牌验证:服务器验证 JWT 签名并检查过期时间
  8. 授予访问:如果有效,服务器使用令牌中的声明处理请求

身份验证请求示例

POST /api/auth/login HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "securePassword123"
}

身份验证响应示例

HTTP/1.1 200 OK
Content-Type: application/json

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresIn": 3600,
  "tokenType": "Bearer"
}

已验证 API 请求示例

GET /api/users/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT 与基于会话的身份验证对比

在 JWT 和基于会话的身份验证之间进行选择取决于应用的架构、规模和需求。

方面 JWT 基于会话
状态 无状态(服务器不存储令牌) 有状态(服务器存储会话数据)
可扩展性 优秀(无需共享存储) 需要会话存储(Redis、数据库)
撤销 困难(需要黑名单或短期过期) 容易(从存储中删除会话)
大小 较大(每次请求都发送) 较小(仅会话 ID)
跨域 容易(CORS 友好) 复杂(cookie 域限制)
移动应用 理想(无需 cookie 支持) 具有挑战性(cookie 处理)
安全性 需要仔细实现 成熟的模式

何时使用 JWT

何时使用会话

在应用中实现 JWT

让我们通过流行编程语言和框架中的实际实现示例来演示。

Node.js 与 Express

const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

const SECRET_KEY = process.env.JWT_SECRET;

// 登录端点
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;
  
  // 验证凭据(伪代码)
  const user = await verifyCredentials(email, password);
  
  if (!user) {
    return res.status(401).json({ error: '凭据无效' });
  }
  
  // 生成 JWT
  const token = jwt.sign(
    {
      sub: user.id,
      email: user.email,
      role: user.role
    },
    SECRET_KEY,
    { expiresIn: '1h' }
  );
  
  res.json({ accessToken: token });
});

// 身份验证中间件
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: '未提供令牌' });
  }
  
  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      return res.status(403).json({ error: '令牌无效' });
    }
    req.user = user;
    next();
  });
};

// 受保护的路由
app.get('/api/users/profile', authenticateToken, (req, res) => {
  res.json({ user: req.user });
});

Python 与 Flask

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

app = Flask(__name__)
SECRET_KEY = os.getenv('JWT_SECRET')

@app.route('/api/auth/login', methods=['POST'])
def login():
    data = request.get_json()
    email = data.get('email')
    password = data.get('password')
    
    # 验证凭据
    user = verify_credentials(email, password)
    
    if not user:
        return jsonify({'error': '凭据无效'}), 401
    
    # 生成 JWT
    token = jwt.encode({
        'sub': user['id'],
        'email': user['email'],
        'role': user['role'],
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    }, SECRET_KEY, algorithm='HS256')
    
    return jsonify({'accessToken': token})

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

@app.route('/api/users/profile')
@token_required
def profile():
    return jsonify({'user': request.user})

快速提示:始终使用环境变量存储密钥。切勿在源代码中硬编码或将其提交到版本控制。使用 密码生成器等工具创建强密钥。

We use cookies for analytics. By continuing, you agree to our Privacy Policy.