Django中SESSION_EXPIRE_AT_BROWSER_CLOSE生效但expire_date未更新问题
首先明确:SESSION_EXPIRE_AT_BROWSER_CLOSE = True 仅控制浏览器端的Session Cookie为会话级Cookie(关闭浏览器即删除),但并不会主动通知服务器销毁对应的数据库Session记录,所以数据库中Session实例的expire_date字段仍会遵循SESSION_COOKIE_AGE的默认值(默认2周),不会因用户关闭浏览器而改变。这就是为什么你查询expire_date__gte=timezone.now()的会话时,会包含已登出用户ID的原因——服务器端的Session还没到过期时间,只是浏览器端已经没有对应的Cookie了,用户无法再使用这个Session。
要判断用户是否已关闭浏览器登出,核心难点在于:浏览器关闭不会主动向服务器发送通知,因此无法100%精准判断,但可以通过以下几种方案间接实现:
方案一:前端监听关闭事件,主动通知后端销毁Session
通过前端的beforeunload事件监听浏览器关闭/页面刷新动作,发送异步请求通知后端销毁当前用户的Session,这样数据库中的Session会被直接删除,后续查询就不会包含该用户ID。
前端代码示例
window.addEventListener('beforeunload', function() { // 使用sendBeacon确保请求能在页面卸载时发送成功 navigator.sendBeacon('/api/destroy-session/'); });
后端实现
- 编写视图处理销毁请求:
from django.contrib.auth import logout from django.http import HttpResponse def destroy_session(request): if request.user.is_authenticated: logout(request) # 销毁当前用户的Session return HttpResponse(status=204)
- 在
urls.py中配置路由:
from django.urls import path from .views import destroy_session urlpatterns = [ # ... 其他路由 path('api/destroy-session/', destroy_session, name='destroy_session'), ]
注意:这种方式并非绝对可靠,比如浏览器崩溃、进程被强制杀死时,beforeunload事件不会触发,此时Session仍会留在数据库中直到自然过期。
方案二:记录用户最后活跃时间,间接判断活跃状态
通过维护用户的最后活跃时间,结合Session的存在情况,过滤出真正活跃的用户。
步骤1:添加用户最后活跃字段
在用户模型中添加last_active字段(如果使用默认User模型,需要通过扩展或自定义用户模型实现):
from django.contrib.auth.models import AbstractUser from django.db import models from django.utils import timezone class CustomUser(AbstractUser): last_active = models.DateTimeField(default=timezone.now)
步骤2:编写中间件更新活跃时间
创建中间件,在用户每次发起请求时更新last_active:
from django.utils import timezone class LastActiveMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if request.user.is_authenticated: # 仅当字段超过一定时间未更新时才保存,减少数据库操作 if (timezone.now() - request.user.last_active).total_seconds() > 60: request.user.last_active = timezone.now() request.user.save(update_fields=['last_active']) response = self.get_response(request) return response
然后在settings.py的MIDDLEWARE列表中添加该中间件:
MIDDLEWARE = [ # ... 其他中间件 'yourapp.middleware.LastActiveMiddleware', ]
步骤3:过滤真正活跃的会话
查询会话时,同时校验用户的最后活跃时间(比如认为超过30分钟无活跃则判定为已登出):
from django.contrib.auth.models import User from django.contrib.sessions.models import Session from django.utils import timezone from datetime import timedelta # 设置无活跃阈值,比如30分钟 inactive_threshold = timezone.now() - timedelta(minutes=30) # 获取最近30分钟内有活跃的用户ID集合 active_user_ids = set( User.objects.filter(last_active__gte=inactive_threshold) .values_list('id', flat=True) ) sessions = Session.objects.filter(expire_date__gte=timezone.now()) uid_list = [] for session in sessions: data = session.get_decoded() uid = data.get('_auth_user_id') # 仅保留活跃用户的会话ID if uid and int(uid) in active_user_ids: uid_list.append(uid) print("#", uid)
方案三:改用缓存型Session存储
如果使用Redis或Memcached作为Session存储后端,缓存会自动清理过期的Session,但本质上还是无法实时感知浏览器关闭,只是过期的Session会被缓存自动回收,减少无效数据的留存。
内容的提问来源于stack exchange,提问作者Behnam Mosayebi

