1. 项目概述
在Web开发中,认证系统是保障应用安全的第一道防线。传统基于Session的认证方式在分布式系统中存在扩展性问题,而JWT(JSON Web Token)作为一种轻量级的认证方案,正逐渐成为现代Web应用的主流选择。我在最近一个Django项目中,就遇到了需要深度定制JWT认证的需求。
这个项目需要实现:
- 完全自定义的JWT签发与验证逻辑
- 全局登录状态检查中间件
- 与DRF(Django REST Framework)的无缝集成
- 细粒度的权限控制
经过多次迭代和实战测试,最终形成了一套稳定可靠的解决方案。下面我将详细分享实现过程中的技术细节和踩坑经验。
2. 核心需求解析
2.1 为什么需要自定义JWT认证?
标准DRF JWT方案存在几个痛点:
- 默认的payload结构过于简单,无法满足业务需求
- 缺乏灵活的token刷新机制
- 无法与现有的用户系统深度集成
- 缺少细粒度的权限控制
2.2 中间件的作用
全局登录中间件主要解决以下问题:
- 统一处理401未认证响应
- 记录用户访问日志
- 实现黑名单功能
- 防止CSRF攻击
3. 技术实现细节
3.1 自定义JWT生成与验证
首先安装依赖:
bash复制pip install djangorestframework-simplejwt
然后创建自定义的token生成类:
python复制from rest_framework_simplejwt.tokens import RefreshToken
from datetime import datetime, timedelta
class CustomToken(RefreshToken):
@classmethod
def for_user(cls, user):
token = super().for_user(user)
# 添加自定义claims
token['user_id'] = user.id
token['role'] = user.role
token['exp'] = datetime.now() + timedelta(days=7) # 自定义过期时间
return token
验证器的实现:
python复制from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.exceptions import AuthenticationFailed
class CustomJWTAuth(JWTAuthentication):
def get_validated_token(self, raw_token):
try:
token = super().get_validated_token(raw_token)
# 自定义验证逻辑
if token.payload.get('blacklisted', False):
raise AuthenticationFailed('Token已被加入黑名单')
return token
except Exception as e:
raise AuthenticationFailed(str(e))
3.2 全局登录中间件实现
创建middleware.py:
python复制import jwt
from django.http import JsonResponse
from django.conf import settings
class JWTAuthMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 排除不需要认证的路径
if request.path in ['/login/', '/refresh/']:
return self.get_response(request)
# 从header获取token
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if not auth_header.startswith('Bearer '):
return JsonResponse(
{'error': '未提供认证token'},
status=401
)
token = auth_header[7:]
try:
payload = jwt.decode(
token,
settings.SECRET_KEY,
algorithms=['HS256']
)
request.user_id = payload['user_id']
except jwt.ExpiredSignatureError:
return JsonResponse(
{'error': 'Token已过期'},
status=401
)
except Exception as e:
return JsonResponse(
{'error': str(e)},
status=401
)
return self.get_response(request)
3.3 DRF集成配置
在settings.py中添加:
python复制REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'yourapp.authentication.CustomJWTAuth',
),
}
MIDDLEWARE = [
...
'yourapp.middleware.JWTAuthMiddleware',
]
4. 关键问题与解决方案
4.1 Token刷新机制
实现安全的token刷新需要考虑:
- 刷新token的有效期应比access token长
- 每次刷新后旧的refresh token应失效
- 需要记录refresh token的使用情况
解决方案:
python复制from django.core.cache import cache
def refresh_token_view(request):
refresh_token = request.data.get('refresh')
# 检查refresh token是否已被使用过
if cache.get(f'refresh_used_{refresh_token}'):
return Response({'error': 'Refresh token已被使用'}, status=400)
try:
token = CustomToken(refresh_token)
cache.set(f'refresh_used_{refresh_token}', True, timeout=3600)
new_access = str(token.access_token)
new_refresh = str(token)
return Response({
'access': new_access,
'refresh': new_refresh
})
except Exception as e:
return Response({'error': str(e)}, status=400)
4.2 权限控制
基于角色的权限控制实现:
python复制from rest_framework.permissions import BasePermission
class IsAdminUser(BasePermission):
def has_permission(self, request, view):
try:
token = request.auth
return token.payload.get('role') == 'admin'
except:
return False
在视图中使用:
python复制from rest_framework.views import APIView
from .permissions import IsAdminUser
class AdminView(APIView):
permission_classes = [IsAdminUser]
def get(self, request):
return Response({'message': '欢迎管理员'})
5. 性能优化与安全加固
5.1 Token黑名单
实现token撤销功能:
python复制from django.db import models
class BlacklistedToken(models.Model):
token = models.CharField(max_length=500, unique=True)
expires_at = models.DateTimeField()
@classmethod
def add_token(cls, token):
try:
payload = jwt.decode(token, verify=False)
expires_at = datetime.fromtimestamp(payload['exp'])
cls.objects.create(token=token, expires_at=expires_at)
except:
pass
在验证器中检查:
python复制class CustomJWTAuth(JWTAuthentication):
def get_validated_token(self, raw_token):
token = super().get_validated_token(raw_token)
if BlacklistedToken.objects.filter(
token=raw_token,
expires_at__gt=timezone.now()
).exists():
raise AuthenticationFailed('Token已被撤销')
return token
5.2 缓存优化
使用Redis缓存用户权限信息:
python复制import redis
from django.conf import settings
redis_conn = redis.StrictRedis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB
)
def get_user_permissions(user_id):
cache_key = f'user_perms_{user_id}'
perms = redis_conn.get(cache_key)
if not perms:
# 从数据库获取
perms = list(Permission.objects.filter(user=user_id).values_list('code', flat=True))
redis_conn.setex(cache_key, 3600, json.dumps(perms))
else:
perms = json.loads(perms)
return perms
6. 测试与部署
6.1 单元测试
测试认证流程:
python复制from django.test import TestCase
from rest_framework.test import APIClient
class AuthTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
username='test',
password='test123'
)
def test_login(self):
response = self.client.post('/login/', {
'username': 'test',
'password': 'test123'
})
self.assertEqual(response.status_code, 200)
self.assertIn('access', response.data)
def test_protected_view(self):
# 先获取token
login_resp = self.client.post('/login/', {
'username': 'test',
'password': 'test123'
})
token = login_resp.data['access']
# 使用token访问受保护视图
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
response = self.client.get('/protected/')
self.assertEqual(response.status_code, 200)
6.2 部署注意事项
- 确保SECRET_KEY安全存储,不要提交到代码仓库
- 生产环境使用HTTPS传输token
- 设置合理的token过期时间
- 定期清理过期的黑名单token
- 监控认证失败日志
7. 经验总结
在实际项目中,我总结了以下几点经验:
-
token有效期设置:access token建议设置为15-30分钟,refresh token可以设置为7天。太短会影响用户体验,太长会增加安全风险。
-
payload大小控制:JWT的payload不宜过大,因为每次请求都会携带。只存储必要的用户标识信息,其他信息可以通过用户ID查询。
-
中间件顺序:登录中间件应该在CSRF中间件之后,但要在视图中间件之前。
-
性能考虑:频繁的数据库查询会影响性能,合理使用缓存可以显著提升响应速度。
-
错误处理:认证失败时应返回清晰的错误信息,但不要暴露系统内部细节。
这套方案已经在多个生产环境中稳定运行,能够满足大多数Web应用的认证需求。根据具体业务场景,可以在此基础上进一步扩展功能,如多因素认证、设备绑定等。
