SpringBoot项目使用监听器实现对在线用户的管理

您所在的位置:网站首页 网站做好怎么上线下线 SpringBoot项目使用监听器实现对在线用户的管理

SpringBoot项目使用监听器实现对在线用户的管理

2024-05-29 21:52| 来源: 网络整理| 查看: 265

文章目录 需求思路实现在线用户监听实现方向具体过程 用户踢下线实现方向具体过程 存在的问题

需求

JavaWeb项目(SpringBoot),需要实现一个查看用户列表时,显示哪个用户在线,同时有将该用户踢下线的功能.以及需要统计当前在线用户的数量. 每部分功能其实都有很多种做法,这里只记录我实际用到的做法,选取了适合我项目的方法,其实在不同的场景都有更好的方法. 参考资料: 监听Session属性: https://www.cnblogs.com/siv8/p/5904105.html 根据ID获取Session: https://blog.csdn.net/sihai12345/article/details/81098765

思路

由于一开始提的需求是统计在线用户数量,方法有很多,比如监听Session统计Session数量.但是组长发我的一篇博客说,这种方法当同一终端开启多个浏览器时,会造成统计多次.那篇博客推荐使用IP进行统计,可是IP也存在一个问题就是内网内是同一外网IP,也会造成统计错误. 考虑到我们的项目需要实现用户下线功能,和某用户上线状态的显示,所以我觉得其实Session方法更好,至于同一终端多个浏览器登录同一个用户的方法,可以靠约束用户只能单一登录实现.当同一账号被第二次登录时,将前一个登陆的账号顶掉即可. 所以,实现功能主要分为两个部分

实现Session监听,统计在线用户数量.由于需要统计的是已登录的用户,所以需要监听的是Session属性的变化,使用HttpSessionBindingListener来实现,而不是HttpSessionListener.根据用户的SessionID获取到其Session,并将其登录属性移除,实现踢下线功能. 实现 在线用户监听 实现方向

做的时候找了很多资料,大部分都是使用HttpSessionListener,重写sessionCreated方法和sessionDestoryed方法,实现对session的监听.这种方法监听到的是每一个会话,但业务中是存在登录的,我们只需要监听真正登录了的会话即可,查阅了其他资料,决定使用HttpSessionBindingListener来监听特定属性绑定到session.

具体过程

参考的这篇博客的做法:https://www.cnblogs.com/siv8/p/5904105.html 具体实现是自定义一个UserOnlineListener类实现HttpSessionBindingListener接口,重写其中的valueBound和 valueUnbound方法,在属性绑定和解绑时执行不同的操作.实现每一个会话都对应一个监听器.在application中定义一个在线用户集合userOnlineList.当将监听器对象绑定/解绑到当前会话上时,将当前登录的用户从application中定义的添加/移除.

自定义监听器实现HttpSessionBindingListener package com.jytd.sdntest.common.listener; import javax.servlet.ServletContext; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; import java.util.ArrayList; import java.util.List; public class UserOnlineListener implements HttpSessionBindingListener { private Long userId; public UserOnlineListener(Long userId){ this.userId=userId; } @Override public void valueBound(HttpSessionBindingEvent event) { HttpSession session=event.getSession(); ServletContext application=session.getServletContext(); //从application获取当前登录用户列表 List userOnlineList= (List) application.getAttribute("userOnlineList"); //如果该属性不存在,则初始化 if(userOnlineList==null){ userOnlineList=new ArrayList(); application.setAttribute("userOnlineList",userOnlineList); } //将当前用户添加至用户列表 userOnlineList.add(this.userId); System.out.println("session属性绑定=======>"+this.userId); } @Override public void valueUnbound(HttpSessionBindingEvent event) { HttpSession session=event.getSession(); ServletContext application=session.getServletContext(); //从application获取当前登录用户列表 List userOnlineList= (List) application.getAttribute("userOnlineList"); //将该用户从列表中移除 userOnlineList.remove(this.userId); System.out.println("session属性解除绑定=======>"+this.userId); } }

在类中定义一个属性,userId,表示当前登录用户的ID,需要其他也可以定义.之后重写其构造方法.

修改登录接口 在登录接口中,在用户登录之后,以用户ID为参数新建该类的对象,并将对象绑定到用户的session中,代码如下: @SysLog("用户登录验证") @RequestMapping(method = RequestMethod.POST, value = "/userLogin") @ApiOperation(value = "用户登录验证", notes = "用户登录验证", httpMethod = "POST") public R userLongin(UserQo userQo, HttpSession httpSession) { //查询用户 CommonUserNode commonUserNode = commonUserService.findUserByUserNameAndPwd(userQo); //判断是否为空 if (commonUserNode == null) { return R.error("用户名或密码有误!"); } //判断用户状态 int state = commonUserNode.getState().intValue(); if (state == 1) { return R.error("该账号已被冻结!"); } httpSession.setAttribute("loginUser", commonUserNode); httpSession.setAttribute("userOnlineListener", new UserOnlineListener(commonUserNode.getId())); System.out.println("当前登录用户的sessionId"+httpSession.getId()); return R.ok(commonUserNode); }

主要是httpSession.setAttribute("userOnlineListener", new UserOnlineListener(commonUserNode.getId()));这一行,首先调用构造方法新建对象,之后执行valueBound方法,将当前登录用户加入到application中定义的用户列表属性中. valueUnbound在这几种情况下会调用: 执行session.invalidate()时; session超时,自动销毁时; 执行session.setAttribute(“userOnlineListener”, “其他对象”);或session.removeAttribute(“userOnlineListener”);将listener从session中删除时

获取在线用户 写好这些,只要从application中获取到之前定义好的userOnlineList即可,我这里写的这个接口也只是一个简单的测试,返回的是在线用户的ID集合,后续可能要根据角色统计每种角色的在线用户数量,需要跟数据库有关联,还没考虑好到底怎么做. @SysLog("获取当前在线用户数量及列表") @RequestMapping(method = RequestMethod.POST, value = "/getUserOnline") @ApiOperation(value = "获取当前在线用户数量及列表", notes = "获取当前在线用户数量及列表", httpMethod = "POST") public R getUserOnline(HttpServletRequest request) { HttpSession session=request.getSession(); ServletContext application=session.getServletContext(); List userOnlineList= (List) application.getAttribute("userOnlineList"); if(userOnlineList!=null){ System.out.println("在线用户数:"+userOnlineList.size()); } return R.ok(userOnlineList); } 用户踢下线 实现方向

所谓将用户踢下线,其实就是将该用户此次session中的登录属性移除,同时使用拦截器,在用户每次请求时验证其登录状态,当验证不通过跳转回登录页面. 那么将移除session,就是通过sessionId获取到该session对象,再将该属性移除即可. 但根据sessionId获取session对象这个过程昨天耗费了一些时间.这里也要仔细记录一下.

具体过程 登录拦截器 这个拦截器也不是最终版 定义一个LoginInterceptor实现HandlerInterceptor接口,重写preHandle方法,在其中实现登录验证的逻辑. 由于之前提需求是要求我如果一个用户被冻结(即修改其数据库中的某个属性),也将该用户踢下线,所以我暂时的解决办法是登录验证时从库中查询其状态,如果是冻结状态则直接拦截器验证不通过. 这种方法的问题是每请求一次就要查一次数据库中的用户数据,十分繁琐,暂时还没决定好最终的方案. package com.jytd.sdntest.common.config; import com.jytd.sdntest.common.utils.Util; import com.jytd.sdntest.sdn.node.domain.CommonUserNode; import com.jytd.sdntest.sdn.node.repository.CommonUserRepository; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Optional; /** * 登录拦截器 */ public class LoginInterceptor implements HandlerInterceptor { private CommonUserRepository commonUserRepository; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(commonUserRepository==null){ BeanFactory beanFactory= WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext()); commonUserRepository=beanFactory.getBean(CommonUserRepository.class); } HttpSession session=request.getSession(); CommonUserNode loginUser=(CommonUserNode)session.getAttribute("loginUser"); if(Util.isEmpty(loginUser)){ //用户未登录则返回主页面 response.sendRedirect("/index"); return false; }else{ Optional checkUser=commonUserRepository.findById(loginUser.getId()); if(checkUser.get().getState().equals(1)){ //用户被冻结,返回主页面 response.sendRedirect("/index"); return false; } } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }

拦截器定义好之后,在WebMvcConfigurer中将拦截器注册. 我是定义了一个WebConfig实现WebMvcConfigurer接口,通过addInterceptors方法注册拦截器,同时该接口还可以配置静态资源访问路径(addResourceHandlers方法)

package com.jytd.sdntest.common.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册拦截器 LoginInterceptor loginInterceptor = new LoginInterceptor(); InterceptorRegistration loginRegistry = registry.addInterceptor(loginInterceptor); // 拦截路径 loginRegistry.addPathPatterns("/**"); // 排除路径 loginRegistry.excludePathPatterns("/"); loginRegistry.excludePathPatterns("/commonuser/userLogin"); ………………其他自行添加 } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/statics/**").addResourceLocations("classpath:/statics/"); } }

完成之后,即可对指定请求和资源进行登录拦截,如果验证不通过则无法访问.

存储用户sessionId 这一步其实我还没考虑太清楚,到底是用前面的application来存储,还是直接读写数据库,还是使用缓存数据库redis来实现.目前来看最好用的应该是直接读写数据库,因为如果直接读写,对于我列表查看相关接口也方便前端去判断.但是这个方法应该是效率最低的一种. 总之,是我需要通过一种方式,将用户ID及其session的ID存储下来,当需要将用户踢下线时,由前端传参给我,用户ID或者sessionID均可,我最终拿到sessionID,获取到session对象,将其属性移除

根据sessionId获取session对象 获取session对象,如果是shiro框架的话,会比较好办,但是现在项目里并没有用. 我参考的是:https://blog.csdn.net/sihai12345/article/details/81098765 这篇博客.

首先是自带方法,

HttpSession session = session.getSessionContext().getSession(sessionId);

但这种方法不被建议,而且我用这种方法并不能获取得到.所以考虑换其他方法.

于是我参照另一篇博客,自定义了一个SessionContext 首先定义一个MySessionContext,在类中定义一个Map来保存session对象,key是sessionId,value就是session对象,同时该类要是用单例模式.

package com.jytd.sdntest.common.utils; import javax.servlet.http.HttpSession; import java.util.HashMap; public class MySessionContext { private static MySessionContext instance; private HashMap sessionMap; private MySessionContext(){ sessionMap=new HashMap(); } public static MySessionContext getInstance(){ if(instance==null){ instance=new MySessionContext(); } return instance; } public synchronized void addSession(HttpSession session){ if(session!=null){ sessionMap.put(session.getId(),session); } } public synchronized void delSession(HttpSession session){ if(session!=null){ sessionMap.remove(session.getId()); } } public synchronized HttpSession getSession(String sessionId){ if(sessionId!=null){ return sessionMap.get(sessionId); }else{ return null; } } }

之后,配置前面提到过的HttpSessionListener监听器,实现对session的监听,新增/结束一个会话时,调用MySessionContext的addSession/delSession方法

package com.jytd.sdntest.common.listener; import com.jytd.sdntest.common.utils.MySessionContext; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; @WebListener public class SessionListener implements HttpSessionListener { private MySessionContext mySessionContext=MySessionContext.getInstance(); @Override public void sessionCreated(HttpSessionEvent se) { HttpSession session=se.getSession(); mySessionContext.addSession(session); } @Override public void sessionDestroyed(HttpSessionEvent se) { HttpSession session=se.getSession(); mySessionContext.delSession(session); } }

Springboot中,监听器使用@WebListener注解即可,但要注意,记得在启动类上添加@ServletComponentScan进行扫描注入.

移除session属性实现下线功能 配置好这些之后,在需要用到的地方直接使用MySessionContext.getInstance()方法获取对象,在调用getSession方法即可获取到session对象,之后将其属性移除即可. @SysLog("根据Session将用户踢下线") @RequestMapping(method = RequestMethod.POST, value = "/kickUserOffline") @ApiOperation(value = "根据Session将用户踢下线", notes = "根据Session将用户踢下线", httpMethod = "POST") public R kickUserOffline(String sessionId,HttpServletRequest request){ MySessionContext mySessionContext=MySessionContext.getInstance(); HttpSession session=mySessionContext.getSession(sessionId); session.removeAttribute("loginUser"); session.removeAttribute("userOnlineListener"); return R.ok("下线成功"); } 存在的问题

最主要的问题.就是如何存储sessionId的问题. 除了获取在线用户,我还有一个需求就是,我要获取用户列表,这个用户列表是我从数据库中查的.如果用户在线,前端要在列表的"操作"这列,显示"下线"按钮,所以我必须在查用户列表同时将其在线状态查出. 暂时有以下三种方案

存入application 像我存储在线用户一样,将在线用户ID和其sessionID同时作为属性存入application中,比如可以存储为用户ID为key,sessionID为value的map的数据结构.再对该map进行操作.

存入redis缓存 redis与application类似,只不过数据结构不太相同,待定

直接读写数据库 直接读写数据库对我的业务来说最为方便,在数据库中,新建属性"sessionId",用户登录时.则修改该属性为当前sessionId;退出登录则将属性置为空.前端查询时,判断该属性是否存在,来控制按钮显隐.踢下线时也将sessionId传入后端即可… 但该方法的弊端是效率太低,频繁数据库读写,终归不太好.

前两种方法效率更高,但查询列表时需要后端对用户列表和在线用户列表进行整合. 目前还没定下来方案.

而且,在线用户数量统计,还要根据角色统计不同角色的在线数量,我的HttpSessionBindingListener还需要修改,如果使用读写数据库的方法,我可以通过直接查询数据库来实现在线用户数的统计. 具体取舍,还需要再进行实验和测试.



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3