• websocket实现用户登录登出日志,并解决浏览器关闭问题


    场景:项目中展示用户登录登出日志,遇到一些问题比如浏览器关闭未记录用户退出日志

    项目技术:jsp+springmvc

    后端需求:记录用户登录登出日志,要求浏览器关闭认为用户退出。

    实现方法:

    第一种.使用aop拦截用户登录登出请求,并记录日志 。

    第二种.利用shiro监听器,在shiro认证成功后记录用户登录日志,退出时记录用户登出日志。

    第三种.使用websocket,在websocket建立链接时记录用户登录日志,websocket断开链接时记录用户登出日志。

    遇到的问题

    存在问题一:浏览器并没有专门的处理浏览器关闭事件 

    存在问题二:根据浏览器关闭时间短,刷新请求接口相对时间长的特性来解决浏览器关闭事件

    1. var beginTime = 0;
    2. var differTime = 0;
    3. window.addEventListener('unload', logData, false);
    4. function logData() {
    5. differTime = new Date().getTime()-beginTime;
    6. if (differTime<=5) {
    7. var analyticsData={};
    8. navigator.sendBeacon(ctx_js+"/logout", analyticsData);
    9. }
    10. }
    11. window.addEventListener('beforeunload', logDataBefore, false);
    12. function logDataBefore(){
    13. beginTime = new Date().getTime();
    14. }

    onbeforeunload,后onunload事件分析:

    关闭页面时:先onbeforeunload,后onunload
    刷新页面时:先onbeforeunload,后onunload,再onload

    改版之前用的就是这种方法,但是发现时而正常,时而不正常(因为differTime这个对照时间的设定受多方面影响,网速等。 ),无奈不得不重新想其它方法。

    最终方案:

    思路:使用websocket实现,在websocket建立链接时记录用户登录日志,websocket断开链接时记录用户登出日志。

    前端处理:

    由于老项目前端登录采用的是form表单提交,因此在登录成功后第一次进入首页时,建立websocket

    1. //登录按钮提交
    2. document.getElementById(id).submit();
    3. //登录按钮提交后设置标志firstFlag, 此处为了使websocket在index页面只初始化一次,登录后先清除firstFlag
    4. var storage=window.localStorage;
    5. localStorage.removeItem("firstFlag");

    在首页初始化的时候建立websocket连接

    1. /**
    2. * maogy 2017-7
    3. * 获取菜单定制中的数据
    4. */
    5. $(document).ready(function() {
    6. //在登录后第一次进入index页面时建立websocket连接
    7. var storage=window.localStorage;
    8. if(storage.getItem("firstFlag") == 'false' || storage.getItem("firstFlag")==null){
    9. initLoginWebSocket();
    10. }
    11. });
    12. /********wxy 2022/10/18 登录成功后建立websocket连接*********/
    13. function initLoginWebSocket(){
    14. //开启WebSocket
    15. var websocket = null;
    16. //变量loginWebSocketUrl从后端读取
    17. var websocketUrl = loginWebSocketUrl+"/GisqPlatformExplorer/ws/login.do";
    18. websocket = new WebSocket(websocketUrl);
    19. // 方法定义
    20. websocket.onopen = function(){
    21. //成功建立连接后设置firstFlag为true
    22. var storage=window.sessionStorage;
    23. storage.setItem("firstFlag", "true");
    24. };
    25. // 接收到服务端的消息
    26. websocket.onmessage = function(result) {
    27. //如果已经有弹窗,就不再弹窗
    28. };
    29. websocket.onerror = function(result){
    30. };
    31. websocket.onclose = function() {
    32. console.log("websocket close");
    33. };
    34. }

    后端处理:

    1. package com.gisquest.platform.websocket.config;
    2. import com.gisquest.platform.websocket.handler.LoginWebSocketHandler;
    3. import com.gisquest.platform.websocket.interceptor.LoginOutInterceptor;
    4. import org.springframework.context.annotation.Bean;
    5. import org.springframework.context.annotation.Configuration;
    6. import org.springframework.web.socket.config.annotation.EnableWebSocket;
    7. import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
    8. import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
    9. /**
    10. * @author LiuDehuai
    11. * @fileName:WebSocketConfig
    12. * @Date:2021/11/5 10:56
    13. */
    14. @Configuration
    15. @EnableWebSocket
    16. public class WebSocketConfig implements WebSocketConfigurer {
    17. @Override
    18. public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
    19. webSocketHandlerRegistry
    20. //添加Handler
    21. .addHandler(loginWebSocketHandler(), "/ws/login.do")
    22. .addInterceptors(new LoginOutInterceptor())
    23. //允许跨域
    24. .setAllowedOrigins("*");
    25. }
    26. //登录时添加websocket连接
    27. @Bean
    28. public LoginWebSocketHandler loginWebSocketHandler() {
    29. return new LoginWebSocketHandler();
    30. }
    31. }

    1. package com.gisquest.platform.websocket.handler;
    2. import java.util.Map;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.util.CollectionUtils;
    6. import org.springframework.web.socket.CloseStatus;
    7. import org.springframework.web.socket.WebSocketMessage;
    8. import org.springframework.web.socket.WebSocketSession;
    9. import org.springframework.web.socket.handler.TextWebSocketHandler;
    10. import com.ctrip.framework.apollo.core.utils.StringUtils;
    11. import com.gisquest.platform.common.utils.CommonLogUtils;
    12. import com.gisquest.platform.common.utils.Constants;
    13. import com.gisquest.platform.websocket.util.WebSocketUtil;
    14. /**
    15. * 浏览器关闭使用WebSocket,记录用户退出日志
    16. * @author wxy
    17. * @fileName:LoginWebSocketHandler
    18. * @Date:2021/11/5 10:14
    19. */
    20. public class LoginWebSocketHandler extends TextWebSocketHandler {
    21. public static final Logger LOGGER = LoggerFactory.getLogger(LoginWebSocketHandler.class);
    22. /**
    23. * 用戶登錄时WebSocket连接成功建立
    24. * @param session
    25. * @throws Exception
    26. */
    27. @Override
    28. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    29. if (LOGGER.isDebugEnabled()) {
    30. LOGGER.debug("WebScoket会话已经建立,SessionId:{}", session.getId());
    31. }
    32. String username=StringUtils.EMPTY;
    33. String ip=StringUtils.EMPTY;
    34. String loginType=StringUtils.EMPTY;
    35. String sessionId=StringUtils.EMPTY;
    36. //获取属性
    37. Map<String, Object> attributes = session.getAttributes();
    38. if (!CollectionUtils.isEmpty(attributes)) {
    39. username = attributes.get("username") != null ? attributes.get("username").toString() : "";
    40. ip=attributes.get("loginIp").toString();
    41. loginType = Constants.LOGINMODEL;
    42. sessionId = session.getId();
    43. }
    44. CommonLogUtils.saveLog(username,ip,loginType,sessionId);
    45. //添加Session
    46. WebSocketUtil.put(session.getId(),session);
    47. //获取属性
    48. super.afterConnectionEstablished(session);
    49. }
    50. @Override
    51. public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
    52. if (LOGGER.isDebugEnabled()) {
    53. LOGGER.debug("WebScoket收到前端消息,SessionId:{},消息:{}", session.getId(), message.getPayload());
    54. }
    55. System.out.println(session.getId());
    56. System.out.println(session);
    57. super.handleMessage(session, message);
    58. }
    59. @Override
    60. public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    61. if (LOGGER.isWarnEnabled()) {
    62. LOGGER.warn("WebScoket连接发生错误,SessionId:{},错误:{}", session.getId(), exception.getMessage());
    63. }
    64. System.out.println(session.getId());
    65. System.out.println(session);
    66. super.handleTransportError(session, exception);
    67. }
    68. /**
    69. * 在任一方关闭WebSocket连接后或发生传输错误后调用
    70. * 关闭WebSocket连接
    71. * @param session
    72. * @param status
    73. * @throws Exception
    74. */
    75. @Override
    76. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    77. if (LOGGER.isDebugEnabled()) {
    78. LOGGER.debug("WebScoket连接关闭,SessionId:{}", session.getId());
    79. }
    80. //记录用户关系浏览器和退出日志
    81. String username=StringUtils.EMPTY;
    82. String ip=StringUtils.EMPTY;
    83. String loginType=StringUtils.EMPTY;
    84. String sessionId=StringUtils.EMPTY;
    85. //获取属性
    86. Map<String, Object> attributes = session.getAttributes();
    87. if (!CollectionUtils.isEmpty(attributes)) {
    88. username = attributes.get("username") != null ? attributes.get("username").toString() : "";
    89. ip=attributes.get("loginIp").toString();
    90. loginType = Constants.LOGOUTMODEL;
    91. sessionId = session.getId();
    92. CommonLogUtils.saveLog(username,ip,loginType,sessionId);
    93. }
    94. //移除Session
    95. WebSocketUtil.remove(session.getId());
    96. super.afterConnectionClosed(session, status);
    97. }
    98. }

  • 相关阅读:
    PostgreSQL JIT(Just-In-Time Compilation)With LLVM 的实现原理
    论文笔记: 度量学习之 DML
    java计算机毕业设计航空售票系统MyBatis+系统+LW文档+源码+调试部署
    Flutter 中的照片管理器(photo_manager):简介与使用指南
    【Vim】单行与多行缩进
    数据库注入提权总结(三)
    12.判断一个数据类型是否为数组
    三层交换机实现不同VLAN间通讯
    MFC 实现延时,并且进行消息分发,不阻塞
    航空摄影与正射摄影的区别
  • 原文地址:https://blog.csdn.net/qq_38423256/article/details/127432569