JSON Web Token (JWT) 详尽教程
#### 阶段1: 什么是JSON Web Token (JWT)? JSON Web Token (JWT) 是一种开放标准(RFC 7519),定义了一种紧凑、自包含的方式,用于在各方之间安全传输信息作为JSON对象。这种信息可以被验证和信任,因为它是数字签名的。 JWT的核心目标是解决HTTP无状态协议的问题:在客户端和服务器之间传递用户身份、权限等声明(claims),而无需服务器维护会话状态。
为什么使用JWT?
- 紧凑性:JWT是URL安全的字符串,便于在HTTP头、查询参数或Cookie中传输。
- 自包含:所有必要信息(如用户ID、过期时间)都编码在token中,服务器无需查询数据库。
- 安全性:通过签名(或加密)确保完整性和真实性。
- 跨域友好:适用于Web、移动和微服务架构,支持单点登录(SSO)。
| 方面 | JWT (Token-based) | Session-based |
|---|---|---|
| 状态管理 | 无状态:token自带所有信息,服务器不存储 | 有状态:服务器存储session数据 |
| 可扩展性 | 高:易于水平扩展,无需共享session存储 | 中等:需要共享session(如Redis),扩展复杂 |
| 性能 | 高:无数据库查询,验证仅需签名检查 | 中等:每请求需查询session存储 |
| 安全性 | 签名防篡改,但需防范XSS/CSRF;易窃取token | 依赖Cookie安全(HttpOnly/Secure),防CSRF强 |
| 适用场景 | API、微服务、移动App、SSO | 传统Web应用、需即时注销的场景 |
| 缺点 | 难以即时注销(需黑名单);token较大 | 服务器负载高;跨域问题(Cookie限制) |
#### 阶段2: JWT的结构详解
JWT是一个字符串,由三部分组成,用点(.)分隔:header.payload.signature。每个部分都是Base64Url编码的JSON对象。
示例JWT:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header(头部):描述token类型和签名算法。
{
"alg": "HS256", // 签名算法:HMAC SHA-256
"typ": "JWT" // 类型:JWT
}
Base64Url编码后:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ92. Payload(负载):包含声明(claims),是实际数据。分为三类(RFC 7519):
- 注册声明(Registered Claims):标准字段,避免冲突。
iss(Issuer):发行者,例如"https://example.com"。sub(Subject):主题,通常用户ID。aud(Audience):受众,指定token目标API。exp(Expiration Time):过期时间(Unix时间戳),服务器必须拒绝过期token。nbf(Not Before):生效时间。iat(Issued At):发行时间。jti(JWT ID):唯一ID,防重放。- 公共声明(Public Claims):IANA注册,避免命名冲突,如
email。 - 私有声明(Private Claims):自定义,如
role: "admin"。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
Base64Url编码后:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ警告:Payload是Base64编码,不是加密!敏感信息(如密码)绝不能放入。
3. Signature(签名):使用Header中算法,对header.payload + 密钥计算签名,确保完整性。
- 计算公式:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) - 示例:使用密钥
your-256-bit-secret,结果为SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c。
+为-,/为_,去除=填充,确保URL安全。#### 阶段3: JWT的工作流程
JWT认证流程如下:
1. 用户登录:客户端发送凭证(用户名/密码)。
2. 服务器验证:检查凭证,生成JWT(包含claims),用密钥签名,返回给客户端。
3. 后续请求:客户端在HTTP头(Authorization: Bearer )携带JWT。
4. 服务器验证:
- 检查签名:使用密钥重新计算,确保匹配。
- 检查claims:如
exp、aud、iss。 - 如果有效,允许访问;否则,拒绝(401 Unauthorized)。
刷新Token:使用长效refresh token换取短效access token,减少安全风险。
#### 阶段4: JWT的签名算法 JWT支持多种算法(RFC 7518),分为对称和非对称:
- 对称(HMAC):HS256/HS384/HS512,使用共享密钥。简单,但密钥泄露风险高。
- 非对称(RSA/ECDSA):RS256/RS512/ES256,使用私钥签名、公钥验证。适合分布式系统。
- 无签名:none(仅调试,生产禁用)。
#### 阶段5: JWT在不同语言中的实现示例 基于搜索结果,提供Node.js、Python、Java示例。使用标准库,确保生产级安全(短效token、HTTPS)。
Node.js 示例(使用jsonwebtoken库)
安装:npm install jsonwebtoken express
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const SECRET_KEY = 'your-super-secret-key'; // 生产用环境变量,256位+
// 登录:生成JWT
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 验证用户(伪代码)
if (username === 'admin' && password === 'pass') {
const payload = { sub: 'admin', role: 'admin', iat: Date.now() };
const token = jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
// 保护路由:验证JWT
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: 'Token required' });
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
};
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: 'Access granted', user: req.user });
});
app.listen(3000, () => console.log('Server running on port 3000'));
测试:POST /login 获取token,然后GET /protected 携带Authorization: Bearer 。Python 示例(使用PyJWT库)
安装:pip install PyJWT flask
from flask import Flask, request, jsonify
import jwt
from datetime import datetime, timedelta
app = Flask(__name__)
SECRET_KEY = 'your-super-secret-key'
# 登录
@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
if username == 'admin' and password == 'pass':
payload = {'sub': 'admin', 'role': 'admin', 'iat': datetime.utcnow()}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return jsonify({'token': token})
return jsonify({'error': 'Invalid credentials'}), 401
# 验证
def authenticate_token(f):
def wrapper(*args, **kwargs):
token = request.headers.get('Authorization', '').split(' ')[1]
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
request.user = payload
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 403
return f(*args, **kwargs)
return wrapper
@app.route('/protected')
@authenticate_token
def protected():
return jsonify({'message': 'Access granted', 'user': request.user})
if __name__ == '__main__':
app.run(port=3000)
Java 示例(使用jjwt库) Maven依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import java.util.Date;
@Path("/auth")
public class AuthResource {
private static final SecretKey KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION_TIME = 3600000; // 1小时
@POST
@Path("/login")
public Response login(String credentials) {
// 验证逻辑(伪代码)
if (credentials.equals("admin:pass")) {
String jwt = Jwts.builder()
.setSubject("admin")
.claim("role", "admin")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(KEY)
.compact();
return Response.ok().entity("{\"token\":\"" + jwt + "\"}").build();
}
return Response.status(401).entity("{\"error\":\"Invalid\"}").build();
}
// 验证:类似Spring Security集成
}
#### 阶段6: 安全最佳实践(RFC 8725) JWT强大但易误用。以下是关键实践:
- 使用强密钥:至少256位随机密钥,定期轮换。避免硬编码。
- 始终验证签名:检查
alg、iss、aud、exp。禁用none算法。 - 短效token:Access token 5-15分钟,refresh token长效但安全存储。
- HTTPS传输:防止中间人攻击。
- 存储安全:客户端用HttpOnly/Secure Cookie,避免localStorage(防XSS)。
- 注销机制:黑名单或短效token。
- 避免敏感数据:Payload仅公共信息。
| 漏洞类型 | 描述 | 缓解措施 |
|---|---|---|
| None算法攻击 | 攻击者设alg: "none",绕过签名。 | 硬编码算法,拒绝none。 |
| 算法混淆 | 改RS256为HS256,用公钥作为密钥签名。 | 固定算法,验证密钥类型。 |
| Kid注入 | kid参数注入SQLi或路径遍历,窃取密钥。 | 验证/白名单kid,用预编译查询。 |
| 弱密钥 | 密钥易猜(如"secret"),暴力破解。 | 用强随机密钥,哈希存储。 |
| JWK/JKU攻击 | 嵌入假公钥或URL窃取密钥。 | 验证来源,禁用动态密钥。 |
| 重放攻击 | 重复使用旧token。 | 用jti唯一ID + 黑名单。 |
#### 阶段8: 高级主题与最佳实践审查
- JWE(加密JWT):用JWE加密Payload,防泄露。
- OAuth 2.0集成:JWT常作ID/Access Token。
- 性能优化:缓存公钥验证。
- 审查标准:是否超出预期?是否A+级?迭代直到完美。
参考:基于RFC 7519、Auth0、OWASP等来源。实践时,始终测试安全!