如何在Django中实现用户单端登录限制:新登录触发旧端自动登出?
嘿,这个需求我之前在项目里落地过,核心思路就是跟踪用户的有效会话,每次新登录时 invalidate 旧会话。下面给你一步步拆解具体实现方案,都是基于Django原生机制,不用额外第三方库:
核心思路
Django默认用django_session表存储用户会话,每个会话对应唯一的session_key。我们要给每个用户绑定当前有效的会话key,当用户新登录时,找到旧会话并标记为过期,同时更新用户的当前会话key。
具体实现步骤
1. 扩展用户模型,添加会话跟踪字段
首先需要给用户模型加一个字段存储当前有效的会话key。如果用默认的User模型,推荐继承AbstractUser扩展:
# your_app/models.py from django.contrib.auth.models import AbstractUser from django.db import models class CustomUser(AbstractUser): # 存储当前有效的会话key,允许为空(用户未登录时) current_session_key = models.CharField(max_length=40, null=True, blank=True)
记得在settings.py里指定自定义用户模型:
AUTH_USER_MODEL = 'your_app.CustomUser'
然后执行迁移命令:
python manage.py makemigrations python manage.py migrate
2. 用信号处理登录事件,自动失效旧会话
Django提供了user_logged_in信号,我们可以监听这个信号,在用户登录成功后处理旧会话:
# your_app/signals.py from django.contrib.auth.signals import user_logged_in, user_logged_out from django.contrib.sessions.models import Session from django.dispatch import receiver from django.utils import timezone @receiver(user_logged_in) def invalidate_old_session_on_login(sender, request, user, **kwargs): # 获取用户之前的有效会话key old_session_key = user.current_session_key if old_session_key: try: # 找到旧会话并设置为已过期 old_session = Session.objects.get(session_key=old_session_key) old_session.expire_date = timezone.now() old_session.save() except Session.DoesNotExist: # 旧会话已经过期或被删除,无需处理 pass # 更新用户当前的会话key为新登录的会话 user.current_session_key = request.session.session_key user.save() @receiver(user_logged_out) def clear_session_key_on_logout(sender, request, user, **kwargs): # 用户主动登出时,清空会话key记录 if user.is_authenticated: user.current_session_key = None user.save()
需要在apps.py里注册信号,确保Django启动时加载:
# your_app/apps.py from django.apps import AppConfig class YourAppConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'your_app' def ready(self): # 导入信号模块 import your_app.signals
3. 前端处理会话失效后的跳转
当旧会话被标记为过期后,用户在旧标签页/设备操作时,Django会返回未登录状态。为了提升用户体验,可以在前端定时检查登录状态,发现失效后自动跳转登录页:
第一步:写一个检查登录状态的API视图
# your_app/views.py from django.http import JsonResponse from django.contrib.auth.decorators import login_required @login_required def check_authentication(request): # 只要用户登录,就返回True return JsonResponse({'is_authenticated': True})
在urls.py里配置路由:
# your_app/urls.py from django.urls import path from . import views urlpatterns = [ # ... 其他路由 path('api/check-auth/', views.check_authentication, name='check_auth'), ]
第二步:在前端模板添加检测逻辑
在你的base模板(所有页面都继承的模板)里加入这段JS:
<script> function checkAuthStatus() { fetch('{% url "check_auth" %}') .then(response => { if (!response.ok) { // 非200状态码,说明未登录 window.location.href = '{% url "login" %}'; } }) .catch(error => { console.error('检查登录状态失败:', error); }); } // 每隔30秒检查一次,可根据需求调整间隔 setInterval(checkAuthStatus, 30000); // 页面加载时也检查一次 window.addEventListener('load', checkAuthStatus); </script>
额外优化点
- 同一浏览器标签页的限制:默认情况下,同一浏览器的不同标签页共享会话,所以用户在同一浏览器开新标签页登录不会触发旧会话失效。如果需要限制到标签页级别,得结合前端的
localStorage或sessionStorage生成唯一标识,每次请求携带这个标识,后端验证标识是否与用户绑定的一致——不过这种方案复杂度较高,通常单端登录需求不需要这么细粒度。 - 会话安全性:在
settings.py里开启SESSION_COOKIE_SECURE = True(生产环境用HTTPS时),SESSION_COOKIE_HTTPONLY = True,防止会话被XSS攻击窃取。 - 处理会话过期:如果用户的会话自然过期(超过
SESSION_COOKIE_AGE设置的时间),current_session_key可能还存在,但对应的会话已经被Django清理,不过我们的信号里已经用try-except处理了这种情况,不会报错。
内容的提问来源于stack exchange,提问作者Dhruv Singhal
相关产品推荐
相关产品推荐

