JWT的最佳实践与安全建议
JWT 基础概述
JSON Web Token(JWT)是一种用于在网络应用环境间安全传输信息的开放标准(RFC 7519)。它通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),中间用点(.)分隔,即 xxxxx.yyyyy.zzzzz
的形式。
JWT 的结构
- 头部(Header):头部通常由两部分组成:令牌的类型(即 JWT)和所使用的签名算法,如 HMAC SHA256 或 RSA。示例如下:
{
"alg": "HS256",
"typ": "JWT"
}
然后将这个 JSON 对象使用 Base64Url 编码,就形成了 JWT 的第一部分。
- 载荷(Payload):载荷是 JWT 的第二部分,也是包含实际需要传输的数据的部分。这些数据被称为声明(claims)。声明分为三种类型:注册声明(如 iss - 发行人、exp - 过期时间、sub - 主题等)、公共声明和私有声明。示例载荷如下:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
同样,将这个 JSON 对象使用 Base64Url 编码,就得到了 JWT 的第二部分。
- 签名(Signature):要创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret)和头部中指定的签名算法。例如,如果使用 HMAC SHA256 算法,签名将按如下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
签名用于验证消息在传输过程中没有被更改,并且,当使用私钥签名时,还可以验证 JWT 的发送者的身份。
JWT 在后端开发中的应用场景
- 身份验证:最常见的应用场景就是身份验证。当用户登录成功后,后端服务器生成一个 JWT 并返回给客户端。客户端在后续的请求中,将 JWT 放在请求头或者 URL 参数中发送给服务器。服务器通过验证 JWT 的签名来确认请求是否来自合法用户。例如,在一个 Web 应用中,用户登录后,服务器生成 JWT 并返回:
import jwt
from flask import Flask, request, jsonify
from datetime import datetime, timedelta
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
# 假设这里进行了用户名和密码的验证
if username == 'valid_user' and password == 'valid_password':
expiration = datetime.utcnow() + timedelta(minutes = 30)
payload = {
'username': username,
'exp': expiration
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
return jsonify({'token': token}), 200
else:
return jsonify({'message': 'Invalid credentials'}), 401
- 信息交换:JWT 还可以用于在不同系统之间安全地交换信息。由于 JWT 可以被验证和信任,接收方可以放心地使用其中包含的数据。例如,在微服务架构中,一个服务可以将一些必要的信息封装在 JWT 中传递给另一个服务。
JWT 的最佳实践
- 密钥管理
- 选择强密钥:密钥是 JWT 安全性的关键。对于 HMAC 算法,密钥应该足够长且随机。例如,使用至少 256 位的随机字符串作为密钥。在 Python 中,可以使用
os.urandom()
生成随机字节串,并将其转换为字符串作为密钥:
- 选择强密钥:密钥是 JWT 安全性的关键。对于 HMAC 算法,密钥应该足够长且随机。例如,使用至少 256 位的随机字符串作为密钥。在 Python 中,可以使用
import os
secret_key = os.urandom(32).hex()
- **安全存储密钥**:密钥绝不能硬编码在代码中,尤其是在开源项目中。应该将密钥存储在安全的环境变量中,在服务器启动时加载。在 Python 的 Flask 应用中,可以这样加载密钥:
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY')
- 过期时间设置
- 合理设置过期时间:为 JWT 设置适当的过期时间可以降低令牌被滥用的风险。对于短期的操作,如登录会话,可以设置较短的过期时间,如 30 分钟到 1 小时。对于长期的 API 访问令牌,可以设置较长但合理的过期时间,如几天或几周,并结合刷新令牌机制。
expiration = datetime.utcnow() + timedelta(minutes = 30)
payload = {
'username': username,
'exp': expiration
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
- **刷新令牌**:当 JWT 过期后,用户可以使用刷新令牌获取新的 JWT。刷新令牌应该存储在安全的地方,如 HTTP 仅 cookie 中,并且有自己的过期时间,通常比 JWT 过期时间长。以下是一个简单的刷新令牌示例:
@app.route('/refresh', methods=['POST'])
def refresh():
refresh_token = request.cookies.get('refresh_token')
try:
data = jwt.decode(refresh_token, app.config['SECRET_KEY'], algorithms=['HS256'])
username = data.get('username')
expiration = datetime.utcnow() + timedelta(minutes = 30)
new_payload = {
'username': username,
'exp': expiration
}
new_token = jwt.encode(new_payload, app.config['SECRET_KEY'], algorithm='HS256')
return jsonify({'token': new_token}), 200
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Refresh token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid refresh token'}), 401
- 限制令牌使用范围
- 绑定令牌到特定请求:可以在 JWT 中添加一些自定义声明,如 IP 地址、用户代理等,以限制令牌只能在特定的条件下使用。例如,在生成 JWT 时添加用户的 IP 地址:
ip_address = request.remote_addr
payload = {
'username': username,
'ip': ip_address,
'exp': expiration
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
- **验证令牌的使用范围**:在服务器端验证 JWT 时,检查这些自定义声明。例如,验证请求的 IP 地址是否与 JWT 中的 IP 地址匹配:
@app.route('/protected', methods=['GET'])
def protected():
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'Token is missing'}), 401
token = token.replace('Bearer ', '')
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
ip_address = request.remote_addr
if data.get('ip') != ip_address:
return jsonify({'message': 'Invalid IP address'}), 401
return jsonify({'message': 'This is a protected route'}), 200
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
- 防止重放攻击
- 使用一次性令牌:可以为每个 JWT 关联一个唯一的标识符,并在服务器端维护一个已使用的标识符列表。每次使用 JWT 时,检查其标识符是否已在列表中。在 Python 中,可以使用
uuid
模块生成唯一标识符:
- 使用一次性令牌:可以为每个 JWT 关联一个唯一的标识符,并在服务器端维护一个已使用的标识符列表。每次使用 JWT 时,检查其标识符是否已在列表中。在 Python 中,可以使用
import uuid
unique_id = uuid.uuid4().hex
payload = {
'username': username,
'unique_id': unique_id,
'exp': expiration
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
- **时间戳验证**:除了过期时间,还可以在 JWT 中添加一个时间戳声明,记录令牌生成的时间。服务器在验证 JWT 时,检查当前时间与时间戳的差值是否在合理范围内,以防止旧令牌被重放。
timestamp = datetime.utcnow()
payload = {
'username': username,
'timestamp': timestamp,
'exp': expiration
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
在验证时:
@app.route('/protected', methods=['GET'])
def protected():
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'Token is missing'}), 401
token = token.replace('Bearer ', '')
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
current_time = datetime.utcnow()
timestamp = data.get('timestamp')
if (current_time - timestamp).total_seconds() > 60 * 5: # 假设 5 分钟内有效
return jsonify({'message': 'Token is too old'}), 401
return jsonify({'message': 'This is a protected route'}), 200
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
- 安全传输令牌
- 使用 HTTPS:在网络上传输 JWT 时,必须使用 HTTPS 协议,以防止中间人攻击。HTTPS 对传输的数据进行加密,确保 JWT 不会被窃取或篡改。
- 避免在 URL 中传递令牌:将 JWT 放在 URL 参数中传输是不安全的,因为 URL 可能会被记录在服务器日志、浏览器历史记录等地方。应该将 JWT 放在请求头中,如
Authorization: Bearer <token>
的形式。
JWT 的安全建议
- 签名算法选择
- 优先使用 RS256 或 ES256:对于安全性要求较高的场景,如涉及金融交易或敏感用户数据的应用,应优先选择 RSA 签名算法(如 RS256)或椭圆曲线签名算法(如 ES256)。这些算法基于公钥 - 私钥对,提供了更好的安全性和不可抵赖性。例如,在使用 Python 的
PyJWT
库时,可以这样使用 RS256 算法:
- 优先使用 RS256 或 ES256:对于安全性要求较高的场景,如涉及金融交易或敏感用户数据的应用,应优先选择 RSA 签名算法(如 RS256)或椭圆曲线签名算法(如 ES256)。这些算法基于公钥 - 私钥对,提供了更好的安全性和不可抵赖性。例如,在使用 Python 的
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
import jwt
private_key = rsa.generate_private_key(
public_exponent = 65537,
key_size = 2048,
backend = default_backend()
)
public_key = private_key.public_key()
private_pem = private_key.private_bytes(
encoding = serialization.Encoding.PEM,
format = serialization.PrivateFormat.PKCS8,
encryption_algorithm = serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
encoding = serialization.Encoding.PEM,
format = serialization.PublicFormat.SubjectPublicKeyInfo
)
payload = {
'username': 'user',
'exp': datetime.utcnow() + timedelta(minutes = 30)
}
token = jwt.encode(payload, private_pem, algorithm='RS256')
# 验证时
try:
data = jwt.decode(token, public_pem, algorithms=['RS256'])
print(data)
except jwt.ExpiredSignatureError:
print('Token expired')
except jwt.InvalidTokenError:
print('Invalid token')
- **避免使用 None 算法**:JWT 规范允许使用 `None` 算法,即不进行签名。这在测试环境中可能方便,但在生产环境中绝不能使用,因为没有签名的 JWT 可以被轻易篡改。
2. 验证 JWT 的完整性
- 严格验证签名:在服务器端接收 JWT 时,必须严格验证其签名。不仅要验证签名是否正确,还要验证使用的签名算法是否与预期一致。例如,在 Python 中使用 PyJWT
库验证 JWT 时:
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token expired'}), 401
except jwt.InvalidAlgorithmError:
return jsonify({'message': 'Invalid algorithm'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
- **检查声明内容**:除了验证签名,还应该检查 JWT 中的声明内容。例如,验证 `exp` 声明是否过期,`iss` 声明是否是预期的发行人等。
3. 防止 JWT 泄露
- 保护服务器端:确保服务器的安全配置,防止服务器被攻击导致 JWT 密钥泄露。这包括定期更新服务器软件、安装安全补丁、配置防火墙等。
- 客户端安全:在客户端,要确保 JWT 存储在安全的地方。在 Web 应用中,避免将 JWT 存储在 localStorage 中,因为它容易受到 XSS 攻击。可以使用 HTTP 仅 cookie 来存储 JWT,并且设置 secure
属性,确保 cookie 仅在 HTTPS 连接下传输。
4. 监控和审计
- 日志记录:在服务器端记录 JWT 的使用情况,包括每次验证的结果、请求的 IP 地址等信息。这些日志可以帮助发现异常行为,如频繁的无效 JWT 验证尝试。
import logging
logging.basicConfig(level = logging.INFO)
logger = logging.getLogger(__name__)
@app.route('/protected', methods=['GET'])
def protected():
token = request.headers.get('Authorization')
ip_address = request.remote_addr
if not token:
logger.info(f'No token provided from {ip_address}')
return jsonify({'message': 'Token is missing'}), 401
token = token.replace('Bearer ', '')
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
logger.info(f'Valid token from {ip_address}')
return jsonify({'message': 'This is a protected route'}), 200
except jwt.ExpiredSignatureError:
logger.info(f'Expired token from {ip_address}')
return jsonify({'message': 'Token expired'}), 401
except jwt.InvalidTokenError:
logger.info(f'Invalid token from {ip_address}')
return jsonify({'message': 'Invalid token'}), 401
- **异常检测**:通过分析日志和监控系统,设置警报机制,当发现异常的 JWT 使用模式时,如大量来自同一 IP 的无效 JWT 请求,及时通知管理员。
常见 JWT 安全漏洞及防范
- 签名伪造漏洞
- 漏洞原理:攻击者通过猜测或窃取密钥,伪造 JWT 的签名,从而生成看似合法的 JWT。这可能导致攻击者绕过身份验证,访问受保护的资源。
- 防范措施:如前文所述,使用强密钥并安全存储,定期更换密钥。同时,严格验证 JWT 的签名和算法,确保只有预期的签名才被接受。
- 信息泄露漏洞
- 漏洞原理:如果 JWT 在传输过程中没有加密(如未使用 HTTPS),或者在客户端存储不安全(如使用 localStorage),攻击者可以截获或窃取 JWT,获取其中包含的敏感信息。
- 防范措施:使用 HTTPS 进行传输,在客户端使用安全的存储方式,如 HTTP 仅 cookie。同时,避免在 JWT 中存储过于敏感的信息,如密码等。
- 重放攻击漏洞
- 漏洞原理:攻击者截获一个有效的 JWT,并在后续的请求中重复使用它,以获取未授权的访问。
- 防范措施:采用一次性令牌或时间戳验证等方法,确保每个 JWT 只能使用一次或在一定时间内有效。
不同后端框架中的 JWT 应用
- Flask 框架:前文已经展示了很多 Flask 框架中使用 JWT 的示例。Flask 可以通过
PyJWT
库轻松实现 JWT 的生成、验证等功能。可以将 JWT 的验证逻辑封装成中间件,应用到需要保护的路由上。
from functools import wraps
def jwt_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
if 'Authorization' in request.headers:
token = request.headers['Authorization'].replace('Bearer ', '')
if not token:
return jsonify({'message': 'Token is missing'}), 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated
@app.route('/protected', methods=['GET'])
@jwt_required
def protected():
return jsonify({'message': 'This is a protected route'}), 200
- Django 框架:在 Django 中,可以使用
djangorestframework - simplejwt
库来处理 JWT。首先安装该库:pip install djangorestframework - simplejwt
。然后在settings.py
中配置:
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes = 15),
'REFRESH_TOKEN_LIFETIME': timedelta(days = 1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM':'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes = 5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days = 1),
}
在视图中使用:
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework_simplejwt.tokens import RefreshToken
from django.http import JsonResponse
class CustomTokenObtainPairView(TokenObtainPairView):
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
refresh = RefreshToken.for_user(request.user)
response.data['refresh_token'] = str(refresh)
response.data['access_token'] = str(refresh.access_token)
return JsonResponse(response.data)
class CustomTokenRefreshView(TokenRefreshView):
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
return JsonResponse(response.data)
- Node.js 中的 Express 框架:在 Express 中,可以使用
jsonwebtoken
库。首先安装:npm install jsonwebtoken
。示例代码如下:
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretKey = 'your_secret_key';
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 假设这里进行了用户名和密码的验证
if (username === 'valid_user' && password === 'valid_password') {
const expiration = new Date(Date.now() + 30 * 60 * 1000);
const payload = {
username,
exp: expiration.getTime() / 1000
};
const token = jwt.sign(payload, secretKey, { algorithm: 'HS256' });
res.json({ token });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
app.get('/protected', (req, res) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ message: 'Token is missing' });
}
const cleanToken = token.replace('Bearer ', '');
try {
const decoded = jwt.verify(cleanToken, secretKey, { algorithms: ['HS256'] });
res.json({ message: 'This is a protected route' });
} catch (err) {
if (err.name === 'TokenExpiredError') {
res.status(401).json({ message: 'Token expired' });
} else {
res.status(401).json({ message: 'Invalid token' });
}
}
});
通过以上最佳实践和安全建议,在后端开发中使用 JWT 时可以大大提高系统的安全性,保护用户数据和应用资源。同时,不同后端框架对 JWT 的支持也为开发者提供了便捷的实现方式,根据项目需求选择合适的框架和配置,能更好地应用 JWT 进行安全认证。