背景:
公司项目之前使用websocket记录用户登录登出日志及浏览器关闭记录用户登出日志,测试发现仍然存在问题,
问题一:当浏览器每次刷新时websocket其实是会断开重新连接的,因此刷新一下就触发记录登出的日志,其实用户并没有真正退出,
问题二:websocket需要配置,如果线上可能要使用wss等相关nginx都需要运维维护,不熟悉的运维还搞不定,
因此领导要求不要用websocket直接使用shiro不用任何配置,下面是改造后的代码逻辑
第一步:添加创建自定义退出过滤器并发布退出事件
public class CustomLogoutFilter extends LogoutFilter{
private static final Logger log = LoggerFactory.getLogger(CustomLogoutFilter.class);
private ApplicationEventPublisher eventPublisher;
public CustomLogoutFilter(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Override
public boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
eventPublisher.publishEvent(new LogoutEvent(this, request, response));
return super.preHandle(request, response);
}
}
第二步:创建退出监听事件
注”使用监听事件主要是想让代码做到分离,由于项目代码结构的原因,结构简单的可以直接在过滤器中记录日志也没有任何问题
public class LogoutEvent extends ApplicationEvent{
private static final long serialVersionUID = -8347909954763768519L;
private ServletRequest request;
private ServletResponse response;public LogoutEvent(Object source, ServletRequest request, ServletResponse response) {
super(source);
this.request = request;
this.response = response;
}#set,get 省略
}
第三步:将过滤器注入到shiro的配置中
这样当用户退出时就会执行自定义退出过滤器中的方法。
@Configuration
public class ShiroCommonConfig{
static final Logger logger=LoggerFactory.getLogger(ShiroCommonConfig.class);@Bean
ShiroSessionFactory sessionFactory() {
return new ShiroSessionFactory();
}//过滤器退出路径配置
@Bean
CustomLogoutFilter logoutFilter(ApplicationEventPublisher eventPublisher) {
String adminPath = Global.getConfig(KeyConsts.ADMIN_PATH) == null ? DefaultValueConsts.DEFAULT_ADMIN_PATH : Global.getConfig(KeyConsts.ADMIN_PATH);
CustomLogoutFilter logoutFilter = new CustomLogoutFilter(eventPublisher);
//重定向到登录页
logoutFilter.setRedirectUrl(adminPath + "/login");
return logoutFilter;
}
}
第四步:创建退出事件监听器记录退出日志
/***
* @author wxy
* @ desc修复之前使用aop时AllModulesAspect切入不到退出的方法从而记录不到用户退出日志
* @date 20230731
*/
@Component
public class LogoutEventListener implements ApplicationListener<LogoutEvent>{@Override
public void onApplicationEvent(LogoutEvent event) {
Subject subject=SecurityUtils.getSubject();
Session loginSession = subject.getSession();
ServletRequest servletRequest =event.getRequest();
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String username=(String)loginSession.getAttribute("username_");
String staffName=(String)loginSession.getAttribute("staffName_");
String userKind=(String)loginSession.getAttribute("systemUser_");
String userId=(String)loginSession.getAttribute("userid_");
String path =servletRequest.getServletContext().getContextPath();
String ip =httpServletRequest.getRemoteAddr();
String loginType = Constants.LOGOUTMODEL;
String loginTitle = Constants.LOGINDESIGNERTITLE;
String sessionId =loginSession.getId().toString();
Map<String,String> params = new HashMap<String,String>();
params.put("userId", userId);
params.put("username", username);
params.put("staffName", staffName);
params.put("ip", ip);
params.put("loginType", loginType);
params.put("sessionId", sessionId);
params.put("loginTitle", loginTitle);
if(StringUtils.isNoneEmpty(path) && path.contains(Constants.CONTEXT_DESIGNER_NAME)) {
//处理存储存数据库的代码逻辑
CommonLogUtils.saveLog(params);
//处理存es的代码逻辑
handlerLogSaveEs(username,staffName,userKind,userId,httpServletRequest);
}
}
}
第五步:创建登录监听事件
public class LoginSuccessEvent extends ApplicationEvent{
private static final long serialVersionUID = 3055102020280674571L;
private ServletRequest request;
private ServletResponse response;public LoginSuccessEvent(Object source, ServletRequest request, ServletResponse response) {
super(source);
this.request = request;
this.response = response;
}
}
第六步:创建自定义过滤器并登录成功时发布事件
@Component
public class FormAuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter {
@Autowired
private ApplicationEventPublisher eventPublisher;
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
ServletRequest request, ServletResponse response) throws Exception {
eventPublisher.publishEvent(new LoginSuccessEvent(this, request, response));
issueSuccessRedirect(request, response);
return false;
}
}
第七步:创建登录成功的监听器记录登录日志
@Component
public class LogSuccessEventListener implements ApplicationListener<LoginSuccessEvent>{@Override
public void onApplicationEvent(LoginSuccessEvent event) {
Subject subject=SecurityUtils.getSubject();
Session loginSession = subject.getSession();
ServletRequest servletRequest =event.getRequest();
String sessionId =loginSession.getId().toString();
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String username =(String)loginSession.getAttribute("username_");
String userId =(String)loginSession.getAttribute("userid_");
String staffName_ =(String)loginSession.getAttribute("staffName_");
String path =servletRequest.getServletContext().getContextPath();
String ip =httpServletRequest.getRemoteAddr();
String loginType = Constants.LOGINMODEL;
String loginTitle = Constants.LOGINDESIGNERTITLE;
Map<String,String> params = new HashMap<String,String>();
params.put("userId", userId);
params.put("username", username);
params.put("staffName", staffName_);
params.put("ip", ip);
params.put("loginType", loginType);
params.put("sessionId", sessionId);
params.put("loginTitle", loginTitle);
//登录成功后在session对象中放入ip及contextPath名称处理无请求时会话过期
loginSession.setAttribute("ip", ip);
loginSession.setAttribute("contextPath", path);
loginSession.setAttribute("successFlag", "true");
//保存到数据库
if(StringUtils.isNoneEmpty(path) && path.contains(Constants.CONTEXT_DESIGNER_NAME)) {
CommonLogUtils.saveLog(params); ##调用自己的登录接口逻辑
}
}}
第八步:创建会话过期监听记录浏览器关闭时,回话过期记录退出日志
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;public class SessionExpireClearListener implements SessionListener {
private Logger Logger = LoggerFactory.getLogger(SessionExpireClearListener.class);@SuppressWarnings("unchecked")
@Override
public void onStart(Session session) {
Logger.debug("session创建:id为 {}", session.getId());
}@Override
public void onStop(Session session) {
Logger.info("session停止:id为 {}", session.getId());
removeInvalidUser(session);
}@Override
public void onExpiration(Session session) {
Logger.debug("session过期:id为 {}", session.getId());
saveExpireLog(session); ###记录用户会话过期日志逻辑
removeInvalidUser(session);
}@SuppressWarnings("unchecked")
private void removeInvalidUser(Session session) { }
}
这里的回话主要和shiro设置的时间有关
注:代码做了简化处理,只提供思路,具体逻辑还是看自己的项目要求