JWT技术详解:从入门到精通
什么是 JSON Web Token(JWT)?
JSON Web Token(JWT)是一个开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。这些信息可以被验证和信任,因为它是数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
虽然 JWT 可以加密以在各方之间提供保密性,但我们将重点关注已签名的令牌。已签名的令牌可以验证其中包含的声明的完整性,而加密的令牌则向其他方隐藏这些声明。当使用公钥/私钥对签名令牌时,签名还证明只有持有私钥的一方才是签名者。
何时应该使用 JSON Web Token?
以下是一些 JSON Web Token 有用的场景:
1. 授权(Authorization)
这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录(SSO)是现在广泛使用 JWT 的功能,因为它的开销很小,并且可以轻松地在不同域之间使用。
2. 信息交换(Information Exchange)
JSON Web Token 是在各方之间安全传输信息的好方法。因为 JWT 可以被签名——例如使用公钥/私钥对——你可以确保发送者是它们所说的那个人。此外,由于签名是使用头部和有效负载计算的,你还可以验证内容是否被篡改。
JSON Web Token 的结构
在紧凑形式中,JSON Web Token 由三个部分组成,用点(.)分隔,它们是:
- Header(头部)
- Payload(有效负载)
- Signature(签名)
因此,JWT 通常如下所示:
xxxxx.yyyyy.zzzzz让我们详细分解不同的部分。
Header(头部)
头部通常由两部分组成:令牌的类型(即 JWT)和使用的签名算法,如 HMAC SHA256 或 RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}然后,这个 JSON 被 Base64Url 编码以形成 JWT 的第一部分。
Payload(有效负载)
令牌的第二部分是有效负载,其中包含声明(claims)。声明是关于实体(通常是用户)和其他数据的陈述。有三种类型的声明:注册声明、公共声明和私有声明。
注册声明(Registered Claims)
这些是一组预定义的声明,虽然不是强制性的,但建议使用,以提供一组有用的、可互操作的声明。其中一些是:iss(签发者)、exp(过期时间)、sub(主题)、aud(受众)等。
注意:声明名称只有三个字符长,因为 JWT 旨在紧凑。
公共声明(Public Claims)
这些可以由使用 JWT 的人随意定义。但为了避免冲突,应在 IANA JSON Web Token Registry 中定义它们,或定义为包含抗冲突命名空间的 URI。
私有声明(Private Claims)
这些是为了在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公共声明。
一个示例有效负载可能是:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"exp": 1516242622
}然后将有效负载进行 Base64Url 编码以形成 JSON Web Token 的第二部分。
请注意,对于已签名的令牌,此信息虽然受到防篡改保护,但任何人都可以读取。不要在 JWT 的有效负载或头部元素中放置秘密信息,除非它是加密的。
Signature(签名)
要创建签名部分,你必须采用编码的头部、编码的有效负载、密钥、头部中指定的算法,并对其进行签名。
例如,如果你想使用 HMAC SHA256 算法,签名将通过以下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)签名用于验证消息在传输过程中没有被更改,并且,在使用私钥签名的令牌的情况下,它还可以验证 JWT 的发送者是否是它声称的那个人。
完整示例
以下显示了一个 JWT,它具有之前编码的头部和有效负载,并使用密钥进行了签名。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c如果你想要实际操作 JWT 并将这些概念付诸实践,可以使用 jwt.io Debugger 来解码、验证和生成 JWT。
JSON Web Token 的工作原理
在身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web Token。由于令牌是凭据,必须非常小心以防止安全问题。通常,你不应该保留令牌超过所需的时间。
你也不应该因为缺乏安全性而在浏览器存储中存储敏感的会话数据。
每当用户想要访问受保护的路由或资源时,用户代理应该发送 JWT,通常在授权头中使用Bearer模式。头的内容应如下所示:
Authorization: Bearer <token>在某些情况下,这可能是一种无状态的授权机制。服务器的受保护路由将检查 Authorization 头中的有效 JWT,如果存在,则允许用户访问受保护的资源。如果 JWT 包含必要的数据,则可能减少查询数据库以执行某些操作的需要,尽管情况可能并非总是如此。
请注意,如果你通过 HTTP 头发送 JWT 令牌,应尝试防止它们变得太大。某些服务器不接受超过 8 KB 的头。如果你试图在 JWT 令牌中嵌入太多信息,例如通过包含用户的所有权限,你可能需要替代解决方案,如 Auth0 Fine-Grained Authorization。
如果令牌在 Authorization 头中发送,跨域资源共享(CORS)不会成为问题,因为它不使用 cookie。
以下图表显示了如何获取 JWT 并使用它来访问 API 或资源:
1. 应用程序或客户端向授权服务器请求授权
这是通过不同的授权流之一执行的。例如,典型的符合 OpenID Connect 的 Web 应用程序将通过使用授权代码流的 `/oauth/authorize` 端点。
2. 当授权被授予时,授权服务器向应用程序返回访问令牌
3. 应用程序使用访问令牌访问受保护的资源(如 API)请注意,对于已签名的令牌,令牌中包含的所有信息都会暴露给用户或其他方,即使他们无法更改它。这意味着你不应将秘密信息放在令牌中。
为什么应该使用 JSON Web Token?
让我们谈谈JSON Web Token(JWT) 与 Simple Web Token(SWT) 和 Security Assertion Markup Language Token(SAML) 相比的优势。
由于 JSON 不如 XML 冗长,当它被编码时,其大小也更小,使 JWT 比 SAML 更紧凑。这使得 JWT 成为在 HTML 和 HTTP 环境中传递的良好选择。
在安全性方面,SWT 只能使用 HMAC 算法通过共享密钥对称签名。但是,JWT 和 SAML 令牌可以使用 X.509 证书形式的公钥/私钥对进行签名。与签名 JSON 的简单性相比,使用 XML 数字签名签名 XML 而不引入模糊的安全漏洞非常困难。
JSON 解析器在大多数编程语言中很常见,因为它们直接映射到对象。相反,XML 没有自然的文档到对象的映射。这使得使用 JWT 比 SAML 断言更容易。
关于使用,JWT 在互联网规模上使用。这突出了在多个平台上(尤其是移动平台)进行 JSON Web 令牌客户端处理的便利性。
| 特性对比 | JWT | SAML | SWT |
|---|---|---|---|
| 格式 | JSON | XML | 简单文本 |
| 大小 | 小 | 大 | 小 |
| 签名算法 | HMAC/RSA/ECDSA | 公钥/私钥对 | 仅 HMAC |
| 客户端处理 | 容易 | 困难 | 容易 |
| 跨域使用 | 容易 | 复杂 | 容易 |
验证与验证的区别
JSON Web Token(JWT)验证和验证对于安全性至关重要,但它们处理 JWT 安全性的不同方面:验证确保令牌格式正确并包含可执行的声明;验证确保令牌是真实的且未被修改。
让我们更详细地探讨验证和验证如何不同:
JWT 验证(Validation)
JWT 验证通常指检查 JWT 的结构、格式和内容:
- 结构:确保令牌具有标准的三个部分(头部、有效负载、签名),用点分隔
- 格式:验证每个部分是否正确编码(Base64URL),以及有效负载是否包含预期的声明
- 内容:检查有效负载中的声明是否正确,如过期时间(exp)、签发时间(iat)、生效时间(nbf)等,以确保令牌未过期、未在生效时间之前使用等
JWT 验证(Verification)
另一方面,JWT 验证涉及确认令牌的真实性和完整性:
- 签名验证:这是验证的主要方面,其中 JWT 的签名部分根据头部和有效负载进行检查。这是使用头部中指定的算法(如 HMAC、RSA 或 ECDSA)与密钥或公钥完成的。如果签名与预期不符,令牌可能已被篡改或来自不受信任的来源
- 签发者验证:检查 iss 声明是否与预期的签发者匹配
- 受众检查:确保 aud 声明与预期的受众匹配
实际应用
你验证 JWT 以确保令牌有意义,符合预期标准,包含正确的数据。
你验证 JWT 以确保令牌未被恶意更改并来自受信任的来源。
在许多系统中,这些步骤通常组合在一起,可能被称为"JWT 验证",它包含验证和验证以进行全面的安全检查。尽管如此,它们的区别仍然存在。
编码与解码的区别
编码 JWT
编码 JWT 涉及将头部和有效负载转换为紧凑的、URL 安全的格式。声明签名算法和令牌类型的头部,以及包括主题、过期和签发时间等声明的有效负载,都转换为 JSON,然后进行 Base64URL 编码。这些编码的部分然后用点连接,之后使用头部指定的算法和密钥或私钥生成签名。此签名也进行 Base64URL 编码,从而生成表示令牌的最终 JWT 字符串,该格式适合传输或存储。
解码 JWT
解码 JWT 通过将 Base64URL 编码的头部和有效负载转换回 JSON 来反转此过程,允许任何人读取这些部分而无需密钥。但是,此上下文中的"解码"通常扩展到包括令牌签名的验证。此验证步骤涉及使用最初使用的相同算法和密钥重新签名解码的头部和有效负载,然后将此新签名与 JWT 中包含的签名进行比较。如果它们匹配,则确认令牌的完整性和真实性,确保自签发以来未被篡改。
常见 Claims 详解
JWT 规范中定义了一些标准的注册声明(Registered Claims),它们虽然不是必需的,但在大多数情况下都很有用:
| Claim | 全称 | 说明 | 示例 |
|---|---|---|---|
| iss | Issuer | 签发者,标识签发 JWT 的主体 | "iss": "https://auth.example.com" |
| sub | Subject | 主题,标识 JWT 的主体,通常是用户 ID | "sub": "1234567890" |
| aud | Audience | 受众,标识 JWT 的预期接收者 | "aud": "api.example.com" |
| exp | Expiration Time | 过期时间,JWT 过期的时间戳(秒) | "exp": 1516239022 |
| nbf | Not Before | 生效时间,JWT 生效的时间戳(秒) | "nbf": 1516239022 |
| iat | Issued At | 签发时间,JWT 签发的时间戳(秒) | "iat": 1516239022 |
| jti | JWT ID | JWT 的唯一标识符 | "jti": "550e8400-e29b-41d4-a716-446655440000" |
签名算法
JWT 支持多种签名算法,主要分为两类:
对称加密算法(HMAC)
使用相同的密钥进行签名和验证:
- HS256:HMAC SHA256(最常用)
- HS384:HMAC SHA384
- HS512:HMAC SHA512
优点:简单、快速
缺点:密钥需要安全共享
非对称加密算法(RSA/ECDSA)
使用公钥/私钥对:
- RS256:RSA SHA256(最常用)
- RS384:RSA SHA384
- RS512:RSA SHA512
- ES256:ECDSA P-256 SHA256
- ES384:ECDSA P-384 SHA384
- ES512:ECDSA P-512 SHA512
优点:密钥分发更安全,私钥只在服务端
缺点:计算开销更大
实际应用示例
Node.js 实现示例
使用 jsonwebtoken 库:
const jwt = require('jsonwebtoken');
// 生成 JWT
const secret = 'your-secret-key';
const payload = {
sub: '1234567890',
name: 'John Doe',
admin: true
};
// 生成令牌
const token = jwt.sign(payload, secret, {
expiresIn: '1h',
issuer: 'myapp',
audience: 'myapp-users'
});
console.log('生成的 JWT:', token);
// 验证令牌
try {
const decoded = jwt.verify(token, secret, {
issuer: 'myapp',
audience: 'myapp-users'
});
console.log('验证成功:', decoded);
} catch (error) {
console.error('验证失败:', error.message);
}
// 仅解码(不验证签名)
const decodedWithoutVerify = jwt.decode(token);
console.log('解码结果:', decodedWithoutVerify);Python 实现示例
使用 PyJWT 库:
import jwt
import datetime
# 密钥
secret = 'your-secret-key'
# 生成 JWT
payload = {
'sub': '1234567890',
'name': 'John Doe',
'admin': True,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow(),
'iss': 'myapp',
'aud': 'myapp-users'
}
# 生成令牌
token = jwt.encode(payload, secret, algorithm='HS256')
print('生成的 JWT:', token)
# 验证令牌
try:
decoded = jwt.decode(token, secret, algorithms=['HS256'],
issuer='myapp', audience='myapp-users')
print('验证成功:', decoded)
except jwt.ExpiredSignatureError:
print('令牌已过期')
except jwt.InvalidTokenError as e:
print(f'验证失败: {e}')
# 仅解码(不验证签名)
decoded_without_verify = jwt.decode(token, options={"verify_signature": False})
print('解码结果:', decoded_without_verify)Java 实现示例
使用 jjwt 库:
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
public class JWTExample {
private static final String SECRET = "your-secret-key-must-be-at-least-256-bits";
private static final Key key = Keys.hmacShaKeyFor(SECRET.getBytes());
// 生成 JWT
public static String generateToken(String userId, String name, boolean isAdmin) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
long expMillis = nowMillis + 3600000; // 1小时后过期
Date exp = new Date(expMillis);
return Jwts.builder()
.setSubject(userId)
.claim("name", name)
.claim("admin", isAdmin)
.setIssuedAt(now)
.setExpiration(exp)
.setIssuer("myapp")
.setAudience("myapp-users")
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
// 验证 JWT
public static Claims validateToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.requireIssuer("myapp")
.requireAudience("myapp-users")
.build()
.parseClaimsJws(token)
.getBody();
return claims;
} catch (Exception e) {
System.err.println("验证失败: " + e.getMessage());
return null;
}
}
}Go 实现示例
使用 github.com/golang-jwt/jwt 库:
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
var secretKey = []byte("your-secret-key")
type Claims struct {
UserID string `json:"sub"`
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.RegisteredClaims
}
// 生成 JWT
func generateToken(userID, name string, isAdmin bool) (string, error) {
claims := Claims{
UserID: userID,
Name: name,
Admin: isAdmin,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "myapp",
Audience: []string{"myapp-users"},
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secretKey)
}
// 验证 JWT
func validateToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return secretKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("token is not valid")
}
return claims, nil
}前端 JavaScript 实现示例
// 发送请求时携带 JWT
async function fetchWithJWT(url, options = {}) {
const token = localStorage.getItem('jwt_token');
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(url, {
...options,
headers,
});
// 如果令牌过期,尝试刷新
if (response.status === 401) {
const newToken = await refreshToken();
if (newToken) {
headers['Authorization'] = `Bearer ${newToken}`;
return fetch(url, { ...options, headers });
}
}
return response;
}
// 刷新令牌
async function refreshToken() {
try {
const refreshToken = localStorage.getItem('refresh_token');
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('jwt_token', data.access_token);
return data.access_token;
}
} catch (error) {
console.error('刷新令牌失败:', error);
// 跳转到登录页
window.location.href = '/login';
}
return null;
}最佳实践
1. 安全存储
客户端(浏览器):
- ✅ HttpOnly Cookie:最佳选择,防止 XSS 攻击
- ✅ 内存存储:单页应用可以存储在内存中
- ❌ localStorage:容易受到 XSS 攻击
- ❌ sessionStorage:同样不安全
// 不推荐:存储在 localStorage
localStorage.setItem('token', jwt);
// 推荐:使用 HttpOnly Cookie(服务端设置)
// Set-Cookie: token=<jwt>; HttpOnly; Secure; SameSite=Strict
// 推荐:SPA 中存储在内存(需要时从 Cookie 读取)
let tokenInMemory = null;
async function getToken() {
if (!tokenInMemory) {
const response = await fetch('/api/auth/token', {
credentials: 'include'
});
tokenInMemory = await response.text();
}
return tokenInMemory;
}2. 令牌过期时间
设置合理的过期时间:
// 短期令牌:15分钟 - 1小时(访问令牌)
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
// 长期令牌:7天 - 30天(刷新令牌)
const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });3. 刷新令牌机制
实现令牌刷新以避免频繁登录:
// 服务端:验证访问令牌,如果过期则使用刷新令牌生成新令牌
app.post('/api/auth/refresh', async (req, res) => {
const { refresh_token } = req.body;
try {
const decoded = jwt.verify(refresh_token, refreshSecret);
const newAccessToken = jwt.sign(
{ sub: decoded.sub, name: decoded.name },
secret,
{ expiresIn: '15m' }
);
res.json({ access_token: newAccessToken });
} catch (error) {
res.status(401).json({ error: '无效的刷新令牌' });
}
});4. 令牌撤销
对于需要立即撤销令牌的场景,可以使用令牌黑名单或短过期时间:
// 使用 Redis 存储撤销的令牌
const revokedTokens = new Set();
function revokeToken(token) {
const decoded = jwt.decode(token);
if (decoded && decoded.exp) {
// 将令牌加入黑名单直到过期
revokedTokens.add(token);
// 或使用 Redis:redis.setex(`revoked:${token}`, decoded.exp - Date.now()/1000, '1');
}
}
function isTokenRevoked(token) {
return revokedTokens.has(token);
}5. 载荷大小限制
不要在 JWT 中存储过多数据,保持载荷精简:
// ❌ 不推荐:存储过多信息
const payload = {
sub: user.id,
name: user.name,
email: user.email,
roles: user.roles,
permissions: user.permissions, // 可能有几十个权限
profile: user.profile,
settings: user.settings
// ... 太多数据
};
// ✅ 推荐:只存储必要信息
const payload = {
sub: user.id,
roles: user.roles.map(r => r.name), // 只存储角色名称
// 其他详细信息从数据库查询
};6. 使用 HTTPS
生产环境必须使用 HTTPS 传输 JWT,防止令牌被中间人攻击窃取:
// 设置 Cookie 时使用 Secure 标志
res.cookie('token', jwt, {
httpOnly: true,
secure: true, // 只在 HTTPS 下传输
sameSite: 'strict' // 防止 CSRF
});7. 算法选择
根据场景选择合适的算法:
// 单服务架构:使用 HMAC(HS256)
const token = jwt.sign(payload, secret, { algorithm: 'HS256' });
// 微服务架构:使用 RSA(RS256),公钥可以公开分发
const privateKey = fs.readFileSync('private.key');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 验证时使用公钥(不需要私钥)
const publicKey = fs.readFileSync('public.key');
const decoded = jwt.verify(token, publicKey, { algorithm: 'RS256' });安全注意事项
1. 密钥管理
- 使用强密钥:至少 256 位(32 字节)
- 密钥轮换:定期更换密钥,同时支持多个密钥版本
- 环境变量:不要将密钥硬编码在代码中
- 密钥存储:使用密钥管理服务(如 AWS KMS、HashiCorp Vault)
// ❌ 不推荐:硬编码密钥
const secret = 'my-secret-key';
// ✅ 推荐:从环境变量读取
const secret = process.env.JWT_SECRET;
if (!secret || secret.length < 32) {
throw new Error('JWT_SECRET 必须至少 32 个字符');
}
// ✅ 更好:使用密钥管理服务
const secret = await getSecretFromVault('jwt-secret-key');2. 敏感信息泄露
JWT 的 Header 和 Payload 是 Base64 编码,任何人都可以解码查看:
// ❌ 危险:不要存储敏感信息
const payload = {
sub: user.id,
password: user.password, // 绝对不要!
creditCard: user.creditCard, // 绝对不要!
ssn: user.socialSecurityNumber // 绝对不要!
};
// ✅ 安全:只存储非敏感的用户标识信息
const payload = {
sub: user.id,
name: user.name,
roles: user.roles
};3. 算法混淆攻击
防止攻击者将算法改为 none 绕过签名验证:
// ❌ 危险:没有指定算法
const decoded = jwt.verify(token, secret);
// ✅ 安全:明确指定允许的算法
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256', 'RS256'] // 只允许这些算法
});
// ✅ 更安全:在中间件中验证算法
function verifyToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '未提供令牌' });
}
try {
const header = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString());
if (header.alg === 'none') {
return res.status(401).json({ error: '不允许的算法' });
}
// 继续验证...
} catch (error) {
return res.status(401).json({ error: '无效的令牌' });
}
}4. 时间验证
始终验证令牌的过期时间和生效时间:
function verifyToken(token, secret) {
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'myapp',
audience: 'myapp-users',
// 自动验证 exp 和 nbf
});
// 额外的自定义验证
const now = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp < now) {
throw new Error('令牌已过期');
}
if (decoded.nbf && decoded.nbf > now) {
throw new Error('令牌尚未生效');
}
return decoded;
}5. CORS 配置
如果使用 Cookie 存储 JWT,需要正确配置 CORS:
// Express.js CORS 配置
const cors = require('cors');
app.use(cors({
origin: 'https://your-frontend-domain.com',
credentials: true, // 允许携带 Cookie
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));常见问题解答
Q1: JWT 和 Session 有什么区别?
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务端 |
| 服务器状态 | 无状态 | 有状态 |
| 扩展性 | 好(无需共享存储) | 需要共享存储(Redis) |
| 性能 | 每次请求都需要验证签名 | 查询会话存储 |
| 撤销 | 困难(需要黑名单) | 简单(删除会话) |
| 跨域 | 容易 | 需要配置 Cookie |
使用建议:
- JWT:适用于微服务、移动应用、无状态 API
- Session:适用于需要快速撤销、单服务架构
Q2: JWT 如何实现注销?
由于 JWT 是无状态的,实现注销有几种方式:
- 客户端删除令牌(推荐用于短期令牌)
- 令牌黑名单(使用 Redis 存储)
- 刷新令牌机制(只撤销刷新令牌)
// 方法 2:令牌黑名单
const redis = require('redis');
const client = redis.createClient();
async function logout(token) {
const decoded = jwt.decode(token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await client.setex(`blacklist:${token}`, ttl, '1');
}
}
async function isBlacklisted(token) {
const result = await client.get(`blacklist:${token}`);
return result === '1';
}Q3: JWT 可以加密吗?
JWT 支持两种安全机制:
- 签名(Signing):验证完整性,内容可见(默认)
- 加密(Encryption):隐藏内容(JWE - JSON Web Encryption)
// 使用 JWE 加密(需要额外的库,如 jose)
const jose = require('jose');
// 加密
const secretKey = await jose.generateSecret('HS256');
const encrypted = await new jose.EncryptJWT(payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(secretKey);
// 解密
const { payload: decrypted } = await jose.jwtDecrypt(encrypted, secretKey);Q4: 如何处理令牌刷新?
实现令牌刷新机制:
// 服务端:验证刷新令牌并生成新的访问令牌
app.post('/api/auth/refresh', async (req, res) => {
const { refresh_token } = req.body;
if (!refresh_token) {
return res.status(400).json({ error: '缺少刷新令牌' });
}
try {
// 验证刷新令牌
const decoded = jwt.verify(refresh_token, refreshSecret, {
issuer: 'myapp',
audience: 'myapp-users'
});
// 检查是否在黑名单中
if (await isBlacklisted(refresh_token)) {
return res.status(401).json({ error: '刷新令牌已被撤销' });
}
// 生成新的访问令牌
const newAccessToken = jwt.sign(
{
sub: decoded.sub,
name: decoded.name,
roles: decoded.roles
},
secret,
{
expiresIn: '15m',
issuer: 'myapp',
audience: 'myapp-users'
}
);
// 可选:生成新的刷新令牌(令牌轮换)
const newRefreshToken = jwt.sign(
{ sub: decoded.sub },
refreshSecret,
{
expiresIn: '7d',
issuer: 'myapp',
audience: 'myapp-users'
}
);
// 将旧的刷新令牌加入黑名单
await logout(refresh_token);
res.json({
access_token: newAccessToken,
refresh_token: newRefreshToken,
token_type: 'Bearer',
expires_in: 900 // 15分钟
});
} catch (error) {
res.status(401).json({ error: '无效的刷新令牌' });
}
});Q5: JWT 令牌大小有限制吗?
虽然没有硬性限制,但建议保持令牌大小在合理范围内:
- HTTP 头限制:大多数服务器限制请求头大小为 4KB-8KB
- 性能考虑:令牌越大,每次请求传输的数据越多
- 推荐大小:小于 1KB,最好在 500 字节以内
如果需要在令牌中存储大量信息,考虑:
- 只存储用户 ID,其他信息从数据库查询
- 使用短期的访问令牌 + 长期的刷新令牌
- 将权限信息存储在数据库中,通过 API 查询
Q6: 如何在微服务中使用 JWT?
在微服务架构中,JWT 非常适合实现无状态认证:
// API 网关:验证 JWT 并转发请求
app.use('/api/*', async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '未授权' });
}
try {
// 使用公钥验证(RS256)
const publicKey = await getPublicKey();
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'auth-service',
audience: 'api-gateway'
});
// 将用户信息添加到请求头,传递给下游服务
req.headers['x-user-id'] = decoded.sub;
req.headers['x-user-roles'] = JSON.stringify(decoded.roles);
next();
} catch (error) {
res.status(401).json({ error: '无效的令牌' });
}
});
// 微服务:从请求头读取用户信息(无需再次验证 JWT)
app.get('/api/users/profile', (req, res) => {
const userId = req.headers['x-user-id'];
// 直接使用,因为已经在网关验证过
res.json({ userId });
});Q7: 如何测试 JWT?
// 使用 Jest 进行单元测试
const jwt = require('jsonwebtoken');
describe('JWT 功能测试', () => {
const secret = 'test-secret';
test('生成和验证 JWT', () => {
const payload = { sub: '123', name: 'Test User' };
const token = jwt.sign(payload, secret, { expiresIn: '1h' });
expect(token).toBeDefined();
const decoded = jwt.verify(token, secret);
expect(decoded.sub).toBe('123');
expect(decoded.name).toBe('Test User');
});
test('过期令牌应该被拒绝', () => {
const payload = { sub: '123' };
const token = jwt.sign(payload, secret, { expiresIn: '-1h' }); // 已过期
expect(() => {
jwt.verify(token, secret);
}).toThrow('jwt expired');
});
test('无效签名应该被拒绝', () => {
const token = jwt.sign({ sub: '123' }, secret);
const wrongSecret = 'wrong-secret';
expect(() => {
jwt.verify(token, wrongSecret);
}).toThrow('invalid signature');
});
});JWT 调试工具
在线工具
jwt.io:最流行的 JWT 调试工具
- 在线解码和验证 JWT
- 支持多种算法
- 可视化 JWT 结构
JWT Decoder:另一个在线 JWT 调试工具
命令行工具
# 安装 jwt-cli
npm install -g jwt-cli
# 解码 JWT
jwt decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# 验证 JWT
jwt verify eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... --secret your-secret
# 生成 JWT
jwt encode --secret your-secret --payload '{"sub":"123","name":"John"}'实际项目示例
Express.js 中间件示例
// middleware/auth.js
const jwt = require('jsonwebtoken');
const redis = require('redis');
const redisClient = redis.createClient();
// JWT 认证中间件
const authenticateToken = async (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '未提供访问令牌' });
}
try {
// 检查是否在黑名单中
const isBlacklisted = await redisClient.get(`blacklist:${token}`);
if (isBlacklisted) {
return res.status(401).json({ error: '令牌已被撤销' });
}
// 验证令牌
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE
});
// 将用户信息添加到请求对象
req.user = {
id: decoded.sub,
name: decoded.name,
roles: decoded.roles || []
};
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期' });
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ error: '无效的令牌' });
}
return res.status(500).json({ error: '服务器错误' });
}
};
// 权限检查中间件
const requireRole = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '未认证' });
}
const userRoles = req.user.roles || [];
const hasRole = roles.some(role => userRoles.includes(role));
if (!hasRole) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
};
module.exports = { authenticateToken, requireRole };// routes/protected.js
const express = require('express');
const router = express.Router();
const { authenticateToken, requireRole } = require('../middleware/auth');
// 受保护的路由
router.get('/profile', authenticateToken, (req, res) => {
res.json({
message: '这是受保护的路由',
user: req.user
});
});
// 需要管理员权限的路由
router.delete('/users/:id', authenticateToken, requireRole('admin'), (req, res) => {
// 删除用户的逻辑
res.json({ message: '用户已删除' });
});
module.exports = router;登录接口示例
// routes/auth.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const User = require('../models/User');
// 登录
router.post('/login', async (req, res) => {
const { email, password } = req.body;
try {
// 查找用户
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: '无效的邮箱或密码' });
}
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ error: '无效的邮箱或密码' });
}
// 生成访问令牌(短期)
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
roles: user.roles
},
process.env.JWT_SECRET,
{
expiresIn: '15m',
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE
}
);
// 生成刷新令牌(长期)
const refreshToken = jwt.sign(
{ sub: user.id },
process.env.JWT_REFRESH_SECRET,
{
expiresIn: '7d',
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE
}
);
// 将刷新令牌存储在数据库中(可选)
user.refreshToken = refreshToken;
await user.save();
res.json({
access_token: accessToken,
refresh_token: refreshToken,
token_type: 'Bearer',
expires_in: 900 // 15分钟
});
} catch (error) {
console.error('登录错误:', error);
res.status(500).json({ error: '服务器错误' });
}
});
// 登出
router.post('/logout', authenticateToken, async (req, res) => {
const token = req.headers['authorization'].split(' ')[1];
try {
// 将令牌加入黑名单
const decoded = jwt.decode(token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redisClient.setex(`blacklist:${token}`, ttl, '1');
}
// 清除用户的刷新令牌(可选)
const user = await User.findById(req.user.id);
if (user) {
user.refreshToken = null;
await user.save();
}
res.json({ message: '已成功登出' });
} catch (error) {
res.status(500).json({ error: '服务器错误' });
}
});
module.exports = router;总结
JSON Web Token(JWT)是一种强大的身份认证和信息交换标准,在现代 Web 应用中有着广泛的应用。通过本文的详细介绍,我们了解了:
核心概念
- JWT 结构:Header、Payload、Signature 三部分
- 签名算法:HMAC(对称)和 RSA/ECDSA(非对称)
- 标准声明:iss、sub、aud、exp、iat、nbf 等
关键优势
- ✅ 无状态:服务器不需要存储会话信息
- ✅ 跨域友好:适合微服务和分布式系统
- ✅ 紧凑格式:比 SAML 更小,比 SWT 更灵活
- ✅ 标准化:RFC 7519 标准,广泛支持
最佳实践
- 安全存储:使用 HttpOnly Cookie 或内存存储
- 合理过期时间:短期访问令牌 + 长期刷新令牌
- 密钥管理:使用强密钥,定期轮换
- 算法验证:明确指定允许的算法,防止算法混淆攻击
- HTTPS 传输:生产环境必须使用 HTTPS
- 令牌大小:保持载荷精简,避免超过 HTTP 头限制
适用场景
- ✅ 微服务架构的无状态认证
- ✅ 单点登录(SSO)
- ✅ 移动应用 API 认证
- ✅ 跨域身份验证
- ✅ 信息交换和授权
注意事项
- ⚠️ JWT 无法直接撤销(需要配合黑名单机制)
- ⚠️ 载荷内容可见,不要存储敏感信息
- ⚠️ 不适合存储大量数据
- ⚠️ 需要妥善管理密钥
掌握 JWT 的工作原理和最佳实践,能够帮助你构建更安全、更可扩展的现代 Web 应用。在实际项目中,应根据具体需求选择合适的认证方案,并结合其他安全措施(如 HTTPS、CORS、CSRF 保护等)来构建完整的身份认证系统。
参考资料: