(一)案例背景
public class SessionInterceptor implements HandlerInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionInterceptor.class);
/**
* 在进入Handler方法(就是Controller中映射路径对应的方法)执行之前执行本方法
* @return true:执行下一个拦截器,直到所有的拦截器都执行完毕,再执行拦截的Controller中的Handler方法
* false:从当前的拦截器往回执行所有拦截器的afterCompletion()方法,再退出拦截器链,不再执行Handler方法
*/
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
HttpSession session = httpServletRequest.getSession();
//若Session中有User,说明用户已经登录,可以继续执行后续代码
if (session.getAttribute(SessionKeyConst.SESSION_USER) != null){
pushMenuInfoInSession(session);
return true;
}
//验证Cookie中是否有登陆标识
if (validateLoginWithCookie(httpServletRequest)){
pushMenuInfoInSession(session);
return true;
}
//若Session中没有User,说明用户未登录或登录超时,跳转到登录界面
httpServletRequest.getRequestDispatcher("/login/sessionTimeout").forward(httpServletRequest, httpServletResponse);
return false;
}
}
上述代码的主要功能就是:访问服务器内某个页面,首先检查Session中是否有已经登录用户的信息;若没有,再验证Cookie中是否有登录标识;若还是没有,则跳转到登录页面。
这样的逻辑对于普通的form表单请求都没有什么问题,但是对于“Ajax”请求,则会出问题。
(二)问题复现
对于“Ajax”请求,比如当Session超时时,就无法跳转到登录页面,服务器会将登录页面的所有“Html文本”返回给请求作为响应,如下图:
这里我来复现下Session超时:当服务器正常访问时,点击“超级管理员组”,会发送“Ajax”请求,返回响应的Json数据,如下图:
此时我重启服务器,则上图会话过程中创建的Session就失效了,我们认为制造了一次Session超时,此时我们再点击“超级管理员组”,会发送“Ajax”请求,由于Session已经失效,经过本文一开始的Session拦截器,就会跳转到登录页面,但由于是“Ajax”请求,无法跳转到登录页面,服务器会将登录页面的所有“Html文本”返回给请求作为响应:
对于这样的返回结果显然对于使用者是不友好的!
(三)解决办法
需要前后端配合解决!
1.后端
(1)Session拦截器
判断是否是Ajax请求(主要看请求头中是否有”X-Requested-With”),若是,则将跳转路径放到Header中。这里我们做了一个统一的处理,所有的Session超时,统一由请求”/login/sessionTimeout”。
/*
* 判断是否是Ajax请求(主要看请求头中是否有"X-Requested-With"),若是,则将跳转路径放到Header中。
* 这里我做了一个统一的处理,所有的Session超时,统一由请求"/login/sessionTimeout"
*/
if (httpServletRequest.getHeader("X-Requested-With") != null){
String basePath = httpServletRequest.getScheme() + "://" + httpServletRequest.getServerName()
+ ":" +httpServletRequest.getServerPort() + httpServletRequest.getServletContext();
httpServletResponse.addHeader("SessionTimeoutPath", basePath + "/login/sessionTimeout");
}
(2)Session超时的请求处理
@RequestMapping("/login/sessionTimeout")
public String sessionTimeout(Model model){
model.addAttribute(PageCodeEnum.KEY, PageCodeEnum.LOGIN_TIMEOUT);
return "/system/sessionOut";
}
2.前端
(1)封装Ajax请求
//对jquery的ajax方法进行二次封装,增加超时时间和对Session超时的统一处理
common.ajax1 = function (param) {
/*
* 利用Jquery的extend()方法,把传进来的参数,与我这里自定义的参数进行合并,意思是:
* 若外面(比如param就是用户传进来的参数)已规定超时时间,则覆盖里面自定义的“timeout”;
* 若未穿超时时间,则以自定义的超时时间为准
*/
var mergeParam = $.extend({
timeout:10000
}, param, {
/*
* “complete”:只要ajax请求完毕就会调用,不管成功与否
* 问题:前面我们说了,若用户传过来的参数在我们自定义的变量之后(比如param在“timeout”之后),
* 若参数包含了我们这里扩展的变量,则会覆盖我们定义的变量。而我们自定义的“complete”又是在
* param之后,所以所以若用户调用ajax的时候,也用了complete方法怎么办,那么则会被我们自定义的
* complete方法覆盖。遇到这种问题怎么解决?比如:
* common.ajax1({
* ....
* complete ; function(){
* ....
* }
* });
* 解决办法:
* 首先判断Session是否超时,若超时,不论用户调不调用complete方法,都会跳转至登录页面
* 其次,若Session未超时,且用户传参中包括complete,并且complete是方法不是字段,
* 则执行用户调用的complete方法
*/
complete : function (response) {
//Session超时
if (response.getResponseHeader("SessionTimeoutPath")){
//跳转到超时请求
location.href = response.getResponseHeader("SessionTimeoutPath");
} else {
//若Session未超时,且用户传参中包括complete,且complete是方法不是字段
if (param.complete && typeof param.complete === "function"){
//执行用户调用的complete方法
param.complete();
}
}
}
});
$.ajax(mergeParam);
}
到这里,基本处理已经完成,但是又出现问题了,再次模拟Session超时,登录界面始终在框架内显示,无法进入到我们正常的登录界面,如下图:
我们正常的登录界面是:
(2)在sessionTimeout.jsp中进行登录页面的显示处理
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>session超时</title>
<script src="${basePath}/resources/js/common/common.js" type="text/javascript"></script>
<script type="text/javascript">
common.showMessage('${pageCode.msg}');
//当前窗口
var topWindow = window;
//若当前窗口的父窗口不是自己,说明当前窗口不是最外围窗口
while (topWindow.parent !== topWindow){
topWindow = topWindow.parent;
}
//跳转到登录页面
topWindow.location.href = "${basePath}/login";
</script>
</head>
<body>
</body>
</html>