1. 项目概述
在Web开发领域,认证系统是保障应用安全的第一道防线。最近在重构一个电商后台时,我决定抛弃传统的Session认证方式,转而采用JWT(JSON Web Token)方案。这个决定源于几个实际痛点:前后端分离架构下Session的跨域问题、分布式系统中的状态同步难题,以及移动端应用对轻量级认证的需求。
JWT作为一种无状态的认证机制,完美解决了这些问题。但Django默认并不提供完整的JWT支持,需要通过DRF(Django REST Framework)的第三方库或自定义实现。我选择了后者,因为现有的开源方案要么功能过剩(带来了不必要的复杂度),要么灵活性不足(无法满足我们的特殊业务需求)。
提示:选择自定义实现而非直接使用第三方库的决定,需要权衡开发成本与长期维护成本。如果你的项目对认证没有特殊要求,推荐先尝试djangorestframework-simplejwt这类成熟方案。
2. 核心需求解析
2.1 认证流程设计
我们的JWT认证需要支持以下核心场景:
- 用户登录成功后生成包含用户ID和角色的JWT
- 后续请求通过Authorization头携带JWT进行认证
- 自动续签临近过期的Token
- 黑名单机制实现主动注销
python复制# 理想中的认证流程伪代码
def login(request):
if auth_success:
generate_jwt(
user_id=...,
role=...,
expires=datetime.now() + timedelta(hours=2)
)
def auth_middleware(request):
token = request.headers.get('Authorization')
if not verify_jwt(token):
return HttpResponseForbidden()
request.user = get_user_from_jwt(token)
2.2 技术选型对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Session认证 | Django原生支持 | 服务器存储压力大 | 传统单体应用 |
| DRF SimpleJWT | 开箱即用 | 定制化能力有限 | 标准REST API |
| 自定义JWT实现 | 完全可控 | 开发成本高 | 特殊业务需求 |
| OAuth2 | 适合第三方登录 | 复杂度高 | 开放平台 |
最终选择自定义实现,主要考虑因素包括:
- 需要将用户角色信息直接编码到Token中
- 要求特定的Token刷新逻辑(非标准的refresh机制)
- 需要与现有的权限系统深度集成
3. 核心实现细节
3.1 JWT生成与验证
使用PyJWT库作为底层实现,关键参数包括:
- 算法:HS256(对称加密)
- 密钥:从Django配置读取
- 有效期:2小时(access_token)、7天(refresh_token)
python复制import jwt
from datetime import datetime, timedelta
def generate_jwt(payload):
return jwt.encode(
{
**payload,
'exp': datetime.now() + timedelta(hours=2),
'iat': datetime.now()
},
settings.SECRET_KEY,
algorithm='HS256'
)
def verify_jwt(token):
try:
return jwt.decode(
token,
settings.SECRET_KEY,
algorithms=['HS256']
)
except jwt.PyJWTError:
return None
3.2 自定义认证类
继承DRF的BaseAuthentication实现:
python复制from rest_framework.authentication import BaseAuthentication
class JWTAuthentication(BaseAuthentication):
def authenticate(self, request):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if not auth_header.startswith('Bearer '):
return None
token = auth_header[7:]
payload = verify_jwt(token)
if not payload:
return None
user = User.objects.get(pk=payload['user_id'])
return (user, None)
3.3 全局登录中间件
即使不使用DRF的视图,普通Django视图也需要认证支持:
python复制class JWTMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not hasattr(request, 'user'):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if auth_header.startswith('Bearer '):
token = auth_header[7:]
payload = verify_jwt(token)
if payload:
request.user = User.objects.get(pk=payload['user_id'])
return self.get_response(request)
4. 高级功能实现
4.1 自动续签机制
当Token剩余有效期小于30分钟时自动签发新Token:
python复制def jwt_response_payload_handler(token, user=None, request=None):
payload = verify_jwt(token)
remaining = payload['exp'] - datetime.now().timestamp()
if remaining < 1800: # 30分钟
new_token = generate_jwt({
'user_id': user.id,
'role': user.role
})
return {'token': new_token}
return {'token': token}
4.2 黑名单实现
使用Redis存储已注销但未过期的Token:
python复制def logout(request):
token = request.META.get('HTTP_AUTHORIZATION', '')[7:]
payload = verify_jwt(token)
if payload:
remaining = payload['exp'] - datetime.now().timestamp()
if remaining > 0:
redis_client.setex(
f'jwt_blacklist:{token}',
int(remaining),
'1'
)
在验证时增加黑名单检查:
python复制def verify_jwt(token):
if redis_client.exists(f'jwt_blacklist:{token}'):
return None
# ...原有验证逻辑
5. 安全加固措施
5.1 防重放攻击
为每个Token添加唯一标识(jti):
python复制def generate_jwt(payload):
payload['jti'] = str(uuid.uuid4())
# ...其余生成逻辑
5.2 敏感操作二次验证
关键操作需要提供refresh_token进行验证:
python复制def change_password(request):
if not request.META.get('HTTP_X_SECURITY_TOKEN'):
return HttpResponseForbidden()
refresh_token = request.META['HTTP_X_SECURITY_TOKEN']
if not verify_jwt(refresh_token):
return HttpResponseForbidden()
6. 性能优化
6.1 减少数据库查询
在payload中缓存常用用户信息:
python复制def generate_jwt(user):
return jwt.encode({
'user_id': user.id,
'role': user.role,
'name': user.name,
# ...其他常用字段
}, ...)
6.2 异步日志记录
使用Django的async支持:
python复制async def log_auth_attempt(user, success):
await AuthLog.objects.acreate(
user=user,
success=success,
timestamp=datetime.now()
)
7. 测试策略
7.1 单元测试示例
python复制class JWTAuthTest(TestCase):
def test_token_generation(self):
user = User.objects.create(username='test')
token = generate_jwt({'user_id': user.id})
self.assertTrue(verify_jwt(token))
def test_expired_token(self):
token = jwt.encode({
'user_id': 1,
'exp': datetime.now() - timedelta(seconds=1)
}, settings.SECRET_KEY)
self.assertIsNone(verify_jwt(token))
7.2 压力测试建议
使用locust模拟高并发场景:
python复制from locust import HttpUser, task
class AuthUser(HttpUser):
@task
def access_protected(self):
self.client.get(
"/api/protected",
headers={"Authorization": "Bearer valid_token"}
)
8. 部署注意事项
8.1 密钥管理
生产环境应使用独立密钥:
python复制# settings/prod.py
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY') # 不同于SECRET_KEY
8.2 多实例部署
确保所有实例的时钟同步(影响Token有效期验证):
bash复制# 使用NTP服务同步时间
sudo timedatectl set-ntp true
9. 常见问题排查
9.1 Token验证失败
检查清单:
- 时钟是否同步(服务器时间偏差会导致立即过期)
- 密钥是否一致(特别是多环境部署时)
- Token是否被篡改(检查签名)
9.2 性能瓶颈
优化建议:
- 避免在payload中存储过多数据(Token体积过大会增加网络开销)
- 对频繁访问的接口添加缓存
- 使用更快的加密算法(如HS256比RS256快)
10. 扩展思考
10.1 微服务场景下的JWT
在微服务架构中,可以考虑:
- 将公钥集中存储在配置中心
- 实现Token转换网关(将外部Token转换为内部统一格式)
- 添加服务间调用的特殊Token类型
10.2 替代方案评估
对于更高安全要求的场景,可以考虑:
- 双向TLS认证
- OAuth2的客户端凭证模式
- 基于HMAC的请求签名方案
重要提示:JWT一旦签发就无法主动失效,对于需要即时撤销权限的场景(如管理员踢人),必须配合黑名单机制或缩短Token有效期。
