Django实现禁止同一账号多端同时登录现有方案失效求修复
问题根因分析
- 核心不兼容:你当前使用JWT作为登录认证凭证,并没有启用Django原生的Session认证机制,代码中操作的
request.session不会生成有效的session_key,所以prev_session_key永远为None。 - 中间件逻辑顺序错误:你在中间件中先执行了
response = self.get_response(request)将请求转发到视图完成了登录逻辑,之后才做会话重复校验,校验逻辑完全没有拦截到登录请求。 - 异常捕获过于宽泛:创建
LoggedInUser时不加区分捕获所有异常,会掩盖数据库操作的真实错误,增加排查难度。
适配JWT场景的修改方案
我们直接基于JWT的特性实现单设备登录,不需要依赖Session,修改步骤如下:
1. 调整模型(models.py)
我们存储JWT的唯一标识jti来判断活跃会话,修改后代码:
from django.db import models from django.contrib.auth.models import User class LoggedInUser(models.Model): user = models.OneToOneField(User, related_name='logged_in_user', on_delete=models.CASCADE, null=True, blank=True) # 存储当前活跃JWT的唯一标识jti active_jti = models.CharField(max_length=255, null=True, blank=True)
执行makemigrations和migrate同步数据库变更。
2. 重写登录视图(UserLoginView)
登录时直接做重复会话校验,不需要依赖中间件,性能更高:
from rest_framework import generics, status from rest_framework.response import Response from .models import LoggedInUser from .serializers import UserLoginSerializer class UserLoginView(generics.RetrieveAPIView): """ 用户单因素登录视图 接收邮箱和密码的POST请求,校验通过后返回JWT凭证 """ permission_classes = () serializer_class = UserLoginSerializer def post(self, request): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.validated_data["user"] tokens = serializer.validated_data["tokens"] # 校验是否已有活跃会话 logged_in_user, created = LoggedInUser.objects.get_or_create(user=user) if logged_in_user.active_jti is not None: return Response( {"detail": "您已有活跃会话,请先从之前的登录退出"}, status=status.HTTP_400_BAD_REQUEST ) # 存储当前JWT的jti,默认djangorestframework-simplejwt生成的access token payload自带jti字段 from rest_framework_simplejwt.tokens import AccessToken access_token_obj = AccessToken(tokens["access"]) logged_in_user.active_jti = access_token_obj["jti"] logged_in_user.save() response = { "success": "True", "message": "用户登录成功", "token": tokens, "user_name": serializer.validated_data["user_name"], } return Response(response, status=status.HTTP_200_OK)
3. 新增JWT校验中间件(可选,用于拦截旧会话的请求)
如果需要强制旧登录的用户所有请求都失效,可以加这个中间件:
from django.http import JsonResponse from rest_framework_simplejwt.tokens import AccessToken, TokenError from .models import LoggedInUser class SingleDeviceLoginMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # 排除登录接口、公开接口,可根据自己的业务调整 if request.path in ["/api/login/", "/admin/"]: return self.get_response(request) # 从请求头拿JWT auth_header = request.META.get("HTTP_AUTHORIZATION") if auth_header and auth_header.startswith("Bearer "): token = auth_header.split(" ")[1] try: access_token_obj = AccessToken(token) user_id = access_token_obj["user_id"] current_jti = access_token_obj["jti"] # 校验jti是否和当前活跃的一致 logged_in_user = LoggedInUser.objects.filter(user_id=user_id).first() if logged_in_user and logged_in_user.active_jti != current_jti: return JsonResponse( {"detail": "您的账号已在其他设备登录,请重新登录"}, status=401 ) except TokenError: # token无效直接放行,交给JWT认证组件处理 pass return self.get_response(request)
把这个中间件添加到settings.py的MIDDLEWARE列表里,注意放在JWT认证中间件之后。
4. 补充登出逻辑
用户登出时需要清空active_jti,否则用户退出后无法再次登录:
# 登出视图示例 class UserLogoutView(generics.GenericAPIView): def post(self, request): user = request.user logged_in_user = LoggedInUser.objects.filter(user=user).first() if logged_in_user: logged_in_user.active_jti = None logged_in_user.save() # 可选:把当前token加入黑名单避免被复用 return Response({"message": "登出成功"}, status=status.HTTP_200_OK)
额外说明
如果你确实要使用Session方案实现,需要在登录成功后手动调用django.contrib.auth.login(request, user)生成session,同时中间件要把校验逻辑放在get_response之前执行,不过JWT场景下不推荐这种混用的方案。
内容的提问来源于stack exchange,提问作者Dhruv Singhal
相关产品推荐
相关产品推荐

