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方法更好,至于同一终端多个浏览器登录同一个用户的方法,可以靠约束用户只能单一登录实现.当同一账号被第二次登录时,将前一个登陆的账号顶掉即可.
所以,实现功能主要分为两个部分
做的时候找了很多资料,大部分都是使用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<Long> userOnlineList= (List<Long>) 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<Long> userOnlineList= (List<Long>) application.getAttribute("userOnlineList");
//将该用户从列表中移除
userOnlineList.remove(this.userId);
System.out.println("session属性解除绑定=======>"+this.userId);
}
}
在类中定义一个属性,userId
,表示当前登录用户的ID,需要其他也可以定义.之后重写其构造方法.
@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中删除时
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<Long> userOnlineList= (List<Long>) 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<CommonUserNode> 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<String, HttpSession> sessionMap;
private MySessionContext(){
sessionMap=new HashMap<String, HttpSession>();
}
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
进行扫描注入.
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还需要修改,如果使用读写数据库的方法,我可以通过直接查询数据库来实现在线用户数的统计.
具体取舍,还需要再进行实验和测试.