You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 01:54:06