理解 JWT 令牌:完整指南

· 12分钟阅读

目录

什么是 JSON Web 令牌 (JWT)?

JSON Web 令牌 (JWT) 已成为现代 Web 应用程序中各方之间安全传输信息的事实标准。由 RFC 7519 定义,JWT 提供了一种紧凑、URL 安全的方法来表示要在两方之间传输的声明。

与服务器维护状态的传统基于会话的身份验证不同,JWT 是自包含的。这意味着令牌本身携带验证用户身份和权限所需的所有信息。这种无状态特性使 JWT 在分布式系统、微服务架构和移动应用程序中特别有价值。

JWT 在现代应用程序中有两个主要用途:

JWT 的美妙之处在于其简单性和多功能性。它们可以在不同的编程语言和平台之间无缝工作,使其成为异构环境的理想选择,其中您的前端可能是 React,后端是 Node.js,移动应用程序是 Swift 或 Kotlin。

专业提示: 虽然 JWT 非常有用,但它们并不是所有身份验证场景的万能解决方案。了解何时使用 JWT 而不是传统会话对于构建安全、可扩展的应用程序至关重要。

JWT 详细结构和剖析

JWT 由三个不同的部分组成,用点 (.) 分隔,形成如下结构:xxxxx.yyyyy.zzzzz。每个部分都有特定的用途,它们共同创建了一个防篡改的信息包。

头部

头部通常由两部分组成:令牌类型 (JWT) 和正在使用的签名算法,例如 HMAC SHA256 或 RSA。此信息告诉接收方如何验证令牌的签名。

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

然后头部被 Base64Url 编码以形成 JWT 的第一部分。您会遇到的常见算法包括:

载荷

载荷包含声明,这些声明是关于实体(通常是用户)和附加元数据的陈述。声明分为三种类型:

注册声明: 这些是预定义的声明,不是强制性的,但建议使用以提供互操作性。它们包括:

公共声明: 这些是您为应用程序定义的自定义声明。为避免冲突,它们应在 IANA JSON Web Token Registry 中定义或使用抗冲突名称(如命名空间 URI)。

私有声明: 创建的自定义声明,用于在同意使用它们的各方之间共享信息。这些既不是注册声明也不是公共声明。

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

然后载荷被 Base64Url 编码以形成 JWT 的第二部分。

快速提示: 切勿在 JWT 载荷中存储敏感信息,如密码或信用卡号。虽然 JWT 已签名,但默认情况下它们不加密,这意味着任何人都可以解码和读取载荷。

签名

签名是通过获取编码的头部、编码的载荷、密钥(对于 HMAC 算法)或私钥(对于 RSA/ECDSA),并使用头部中指定的算法对它们进行签名来创建的。

例如,如果使用 HMAC SHA256,签名将像这样创建:

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

签名确保令牌未被更改。如果有人更改头部或载荷中的单个字符,签名验证将失败。

完整的 JWT 示例

当您将所有三个部分放在一起时,您会得到一个完整的 JWT,如下所示:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

您可以使用我们的 JWT 解码器工具 解码和检查任何 JWT,以查看其头部、载荷并验证其签名。

在实践中创建和使用 JWT

使用正确的库创建 JWT 非常简单。让我们探讨如何在不同的编程语言和场景中生成和使用 JWT。

在 Node.js 中创建 JWT

jsonwebtoken 库是 Node.js 应用程序最流行的选择:

const jwt = require('jsonwebtoken');

// 创建令牌
const payload = {
  sub: user.id,
  email: user.email,
  role: user.role
};

const secret = process.env.JWT_SECRET;
const options = {
  expiresIn: '1h',
  issuer: 'myapp.com'
};

const token = jwt.sign(payload, secret, options);

// 验证令牌
try {
  const decoded = jwt.verify(token, secret);
  console.log(decoded);
} catch (error) {
  console.error('无效令牌:', error.message);
}

在 Python 中创建 JWT

Python 开发人员通常使用 PyJWT 库:

import jwt
import datetime

# 创建令牌
payload = {
    'sub': user.id,
    'email': user.email,
    'role': user.role,
    'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
    'iat': datetime.datetime.utcnow()
}

secret = os.environ.get('JWT_SECRET')
token = jwt.encode(payload, secret, algorithm='HS256')

# 验证令牌
try:
    decoded = jwt.decode(token, secret, algorithms=['HS256'])
    print(decoded)
except jwt.ExpiredSignatureError:
    print('令牌已过期')
except jwt.InvalidTokenError:
    print('无效令牌')

在 Java 中创建 JWT

对于 Java 应用程序,Auth0 的 java-jwt 库被广泛使用:

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

// 创建令牌
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
    .withSubject(user.getId())
    .withClaim("email", user.getEmail())
    .withClaim("role", user.getRole())
    .withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
    .withIssuer("myapp.com")
    .sign(algorithm);

// 验证令牌
try {
    DecodedJWT jwt = JWT.require(algorithm)
        .withIssuer("myapp.com")
        .build()
        .verify(token);
} catch (Exception e) {
    System.out.println("无效令牌: " + e.getMessage());
}

令牌生成的最佳实践

创建 JWT 时,请遵循以下准则以确保安全性和可靠性:

专业提示: 考虑使用刷新令牌实现令牌刷新机制。这允许您出于安全考虑保持访问令牌的短期性,同时在不频繁重新身份验证的情况下保持良好的用户体验。

JWT 身份验证工作流程详解

了解 JWT 如何在应用程序中流动对于正确实现它们至关重要。让我们逐步了解典型的身份验证工作流程。

初始身份验证

  1. 用户提交凭据: 用户将其用户名和密码发送到身份验证端点(通常是 POST /api/auth/login)。
  2. 服务器验证凭据: 服务器根据数据库检查凭据,验证密码哈希是否匹配。
  3. 服务器生成 JWT: 验证成功后,服务器创建一个包含用户身份和相关声明的 JWT。
  4. 服务器返回令牌: JWT 被发送回客户端,通常在响应正文中。一些实现还将其设置为 HTTP-only cookie。

后续请求

  1. 客户端包含令牌: 对于每个后续请求,客户端在 Authorization 头中包含 JWT:Authorization: Bearer <token>
  2. 服务器验证令牌: 服务器提取令牌,验证其签名并检查其过期时间。
  3. 服务器处理请求: 如果令牌有效,服务器从载荷中提取用户信息并相应地处理请求。
  4. 服务器返回响应: 请求的数据或操作结果返回给客户端。

令牌刷新流程

当访问令牌过期时,刷新流程启动:

  1. 客户端检测过期: 客户端收到 401 未授权响应或检查令牌的 exp 声明。
  2. 客户端发送刷新令牌: 客户端将刷新令牌发送到专用刷新端点(POST /api/auth/refresh)。
  3. 服务器验证刷新令牌: 服务器验证刷新令牌并检查它是否已被撤销。
  4. 服务器颁发新的访问令牌: 生成新的访问令牌并返回给客户端。
  5. 客户端重试原始请求: 客户端使用新的访问令牌重试失败的请求。

注销流程

使用 JWT 注销需要特别考虑,因为令牌是无状态的:

  1. 客户端启动注销: 用户点击注销,触发对 POST /api/auth/logout 的请求。
  2. 服务器使刷新令牌无效: 服务器将刷新令牌添加到撤销列表或从数据库中删除它。
  3. 客户端丢弃令牌: 客户端从存储中删除访问令牌和刷新令牌。
  4. 客户端重定向: 用户被重定向到登录页面或公共区域。
令牌类型 典型生命周期 存储位置 用途
访问令牌 15 分钟 - 1 小时 内存或 sessionStorage 授权 API 请求
刷新令牌 7 天 - 30 天 HTTP-only cookie 或安全存储 获取新的访问令牌
ID 令牌 与访问令牌相同 内存 用户身份信息 (OpenID Connect)

快速提示: 在客户端应用程序中实现自动令牌刷新以无缝处理令牌过期。这可以防止用户意外注销

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