Django:追踪并记录用户登录/登出行为至数据库
问题描述
- 需求:将用户登录、登出时间记录至数据库,已通过
login和logout函数实现主动操作记录,需额外检测并记录用户关闭浏览器的登出行为 - 尝试方案:使用WebSocket实现,在
base.html中添加WebSocket连接代码,编写OnlineStatusConsumer处理连接与断开事件 - 现存问题:用户在项目内切换页面时,WebSocket会断开并重连,导致被误判为登出,求可行解决办法
base.html 中的 WebSocket 代码
let wsProtocol = window.location.protocol === "https:" ? "wss" : "ws"; let url = `${wsProtocol}://${window.location.host}/ws/socket-server/` const markSocket = new WebSocket(url) markSocket.onopen = function(e){ markSocket.send(JSON.stringify({ 'message':'CONNECTED' })) } markSocket.onclose = function(event) { console.error("WebSocket closed:", event); };
consumer.py 代码
class OnlineStatusConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() user = self.scope['user'] status = True print("CONNECTED",user) await self.change_online_status(user, status) async def disconnect(self, close_code): user = self.scope['user'] status = False print("DISCONNECTED", user,close_code) await self.change_online_status(user, status) @database_sync_to_async def change_online_status(self, user, status): if status: active_user = ActiveUsers.objects.get_or_create()[0] if not active_user.active_users.filter(id=user.id).exists(): active_user.active_users.add(user) active_user.active_user_count += 1 active_user.save() # 获取用户最近的登录尝试记录 last_login_attempt = SpecificUserLoginDiary.objects.filter(user=user).last() # 如果用户已有登录尝试记录且距离上次登录尝试不超过2分钟,则不创建新记录 if last_login_attempt and (timezone.now() - last_login_attempt.login_date) < timedelta(minutes=2): print("最近登录尝试在2分钟内完成。") else: specific_user_active_log = SpecificUserLoginDiary.objects.create( user=user, login_date=timezone.now() ) else: print("用户已连接") else: active_user = ActiveUsers.objects.get_or_create()[0] if active_user.active_users.filter(id=user.id).exists(): active_user.active_users.remove(user) active_user.active_user_count -= 1 active_user.save() specific_user_active_log = SpecificUserLoginDiary.objects.filter().last() if specific_user_active_log: specific_user_active_log.logout_date = datetime.now() specific_user_active_log.save() else: print("用户已断开连接")
可行解决办法
1. 增加断开后的延迟处理逻辑
在disconnect事件触发时,不要立即更新用户登出状态,而是设置一个5-10秒的延迟任务。如果延迟时间内用户重新建立WebSocket连接,就取消任务,不记录登出;若超时未重连,再执行登出状态更新。
代码示例(修改consumer.py)
from asgiref.sync import sync_to_async import asyncio # 生产环境建议用Redis等分布式存储替代全局字典,避免多进程丢失数据 pending_logout_tasks = {} class OnlineStatusConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() user = self.scope['user'] if not user.is_authenticated: await self.close() return # 取消该用户未执行的延迟登出任务 if user.id in pending_logout_tasks: pending_logout_tasks[user.id].cancel() del pending_logout_tasks[user.id] print(f"取消用户 {user} 的延迟登出任务") await self.change_online_status(user, True) print("CONNECTED", user) async def disconnect(self, close_code): user = self.scope['user'] if not user.is_authenticated: return # 创建延迟10秒的登出任务 task = asyncio.create_task(self.delayed_logout(user)) pending_logout_tasks[user.id] = task print(f"为用户 {user} 创建延迟登出任务,等待10秒") async def delayed_logout(self, user): await asyncio.sleep(10) # 执行登出状态更新 await self.change_online_status(user, False) print(f"执行用户 {user} 的登出记录") if user.id in pending_logout_tasks: del pending_logout_tasks[user.id] # 原有的change_online_status方法保持不变
2. 前端优化:减少页面切换时的连接断开
- 若项目可改造为单页应用(SPA),使用Vue/React等框架通过路由切换页面,无需刷新浏览器,WebSocket连接可保持持久
- 多页应用场景下,可监听前端
beforeunload事件,但该事件无法区分页面刷新和关闭,仍需配合后端延迟处理逻辑使用
3. 基于心跳机制判断真实离线
后端定期向前端发送心跳包,前端收到后立即回复。若后端连续多次收不到心跳回复,再判定用户离线并记录登出时间,避免短时间断开被误判。
4. 优化登录日志关联逻辑
在用户连接WebSocket时,前端生成一个唯一会话标识(如UUID)并发送给后端,后端将该标识与当前登录日志绑定。断开时根据会话标识找到对应日志更新登出时间,避免页面切换重连时覆盖历史日志。
内容的提问来源于stack exchange,提问作者batuhanaltog
相关产品推荐
相关产品推荐

