MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JWT的最佳实践与安全建议

2022-11-093.4k 阅读

JWT 基础概述

JSON Web Token(JWT)是一种用于在网络应用环境间安全传输信息的开放标准(RFC 7519)。它通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),中间用点(.)分隔,即 xxxxx.yyyyy.zzzzz 的形式。

JWT 的结构

  1. 头部(Header):头部通常由两部分组成:令牌的类型(即 JWT)和所使用的签名算法,如 HMAC SHA256 或 RSA。示例如下:
{
  "alg": "HS256",
  "typ": "JWT"
}

然后将这个 JSON 对象使用 Base64Url 编码,就形成了 JWT 的第一部分。

  1. 载荷(Payload):载荷是 JWT 的第二部分,也是包含实际需要传输的数据的部分。这些数据被称为声明(claims)。声明分为三种类型:注册声明(如 iss - 发行人、exp - 过期时间、sub - 主题等)、公共声明和私有声明。示例载荷如下:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

同样,将这个 JSON 对象使用 Base64Url 编码,就得到了 JWT 的第二部分。

  1. 签名(Signature):要创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret)和头部中指定的签名算法。例如,如果使用 HMAC SHA256 算法,签名将按如下方式创建:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

签名用于验证消息在传输过程中没有被更改,并且,当使用私钥签名时,还可以验证 JWT 的发送者的身份。

JWT 在后端开发中的应用场景

  1. 身份验证:最常见的应用场景就是身份验证。当用户登录成功后,后端服务器生成一个 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


  1. 信息交换:JWT 还可以用于在不同系统之间安全地交换信息。由于 JWT 可以被验证和信任,接收方可以放心地使用其中包含的数据。例如,在微服务架构中,一个服务可以将一些必要的信息封装在 JWT 中传递给另一个服务。

JWT 的最佳实践

  1. 密钥管理
    • 选择强密钥:密钥是 JWT 安全性的关键。对于 HMAC 算法,密钥应该足够长且随机。例如,使用至少 256 位的随机字符串作为密钥。在 Python 中,可以使用 os.urandom() 生成随机字节串,并将其转换为字符串作为密钥:
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')
  1. 过期时间设置
    • 合理设置过期时间:为 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


  1. 限制令牌使用范围
    • 绑定令牌到特定请求:可以在 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


  1. 防止重放攻击
    • 使用一次性令牌:可以为每个 JWT 关联一个唯一的标识符,并在服务器端维护一个已使用的标识符列表。每次使用 JWT 时,检查其标识符是否已在列表中。在 Python 中,可以使用 uuid 模块生成唯一标识符:
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


  1. 安全传输令牌
    • 使用 HTTPS:在网络上传输 JWT 时,必须使用 HTTPS 协议,以防止中间人攻击。HTTPS 对传输的数据进行加密,确保 JWT 不会被窃取或篡改。
    • 避免在 URL 中传递令牌:将 JWT 放在 URL 参数中传输是不安全的,因为 URL 可能会被记录在服务器日志、浏览器历史记录等地方。应该将 JWT 放在请求头中,如 Authorization: Bearer <token> 的形式。

JWT 的安全建议

  1. 签名算法选择
    • 优先使用 RS256 或 ES256:对于安全性要求较高的场景,如涉及金融交易或敏感用户数据的应用,应优先选择 RSA 签名算法(如 RS256)或椭圆曲线签名算法(如 ES256)。这些算法基于公钥 - 私钥对,提供了更好的安全性和不可抵赖性。例如,在使用 Python 的 PyJWT 库时,可以这样使用 RS256 算法:
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 安全漏洞及防范

  1. 签名伪造漏洞
    • 漏洞原理:攻击者通过猜测或窃取密钥,伪造 JWT 的签名,从而生成看似合法的 JWT。这可能导致攻击者绕过身份验证,访问受保护的资源。
    • 防范措施:如前文所述,使用强密钥并安全存储,定期更换密钥。同时,严格验证 JWT 的签名和算法,确保只有预期的签名才被接受。
  2. 信息泄露漏洞
    • 漏洞原理:如果 JWT 在传输过程中没有加密(如未使用 HTTPS),或者在客户端存储不安全(如使用 localStorage),攻击者可以截获或窃取 JWT,获取其中包含的敏感信息。
    • 防范措施:使用 HTTPS 进行传输,在客户端使用安全的存储方式,如 HTTP 仅 cookie。同时,避免在 JWT 中存储过于敏感的信息,如密码等。
  3. 重放攻击漏洞
    • 漏洞原理:攻击者截获一个有效的 JWT,并在后续的请求中重复使用它,以获取未授权的访问。
    • 防范措施:采用一次性令牌或时间戳验证等方法,确保每个 JWT 只能使用一次或在一定时间内有效。

不同后端框架中的 JWT 应用

  1. 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


  1. 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)


  1. 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 进行安全认证。