2019独角兽企业重金招聘Python工程师标准>>>
一、原理
1. 异地登陆
同一个账号,在不同的电脑(也可以不同的浏览器)登陆系统,前一个已经登陆的账号session被销毁,用户进行下一步操作时跳转错误页面。
2. 超时登陆
登陆后无操作*分钟后自动销毁session,用户进行下一步操作时跳转错误页面。
3. 区分
异地登陆和超时登陆起效时跳转的错误页面不相同。
二、实现
1. 超时登陆
由系统控制,在web.xml中配置,或者由监听器控制,利用session.setMaxInactiveInterval(interval);方法控制,本次主要展示在web.xml中配置的方法,如下:
30
2. 异地登陆
首先在Controller中判断登陆用户的账号密码是否正确,通过以后判断session是不是异地登陆(过程在监听器中判断,此处判断监听器处理后的返回值),如果是,session销毁,如下:
@RequestMapping("/loginCheck")
@ResponseBody
public Map loginCheck(HttpServletRequest request, String userLoginNumber, String userLoginPasswd) {
Map ret = new HashMap(2);// 存储错误信息的Map容器
String errorMsg = "";// 错误信息
HttpSession session = request.getSession();
/*
代码块,判断账号,并给errorMsg赋值或不赋值(正确)
*/
if (/* 账户判断成功,没有错误信息 */) {
/*
监听器实现HttpSessionListener和HttpSessionAttributeListener接口。
监听器private static一个Map类型的变量。
当监听到对Attribute操作时(登陆验证成功向session添加用户数据,一般都会用到),进入
HttpSessionAttributeListener.attributeAdded()方法,向公共map变量放入数据。
当监听到session销毁操作时,进入HttpSessionListener.sessionDestroyed()方法,将公共map
里的值移除。
LoginListenner.isLogonUser()就是得到map中存储的值
*/
HttpSession isLoginSession = LoginListenner.isLogonUser(userLoginNumber);
if (null != isLoginSession) {// 如果没有,则当前session是一个新的session,之前的session已经销毁
// 异地登陆: 在监听器中区分超时和异地登陆, 在拦截器中判断
isLoginSession.setAttribute("sessionDestroyedStatus", "busy");
isLoginSession.invalidate();// 表示异地登陆,销毁session
}
ret.put("result", "1");
}
return ret;
}
监听器实现(原理在上面),包括区分异地登陆和超时登陆需要跳转不同页面的处理,过程如下:
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nielsen.sfa.common.Constants;
import com.nielsen.sfa.model.User;
import com.nielsen.sfa.utils.CommonUtil;
/* @Description: 登录监听类-处理同一时间只允许账号,单地点登录
*/
public class LoginListenner implements HttpSessionListener,HttpSessionAttributeListener {
/**
* 用于存放账号和session对应关系的map
*/
private static Map map = new HashMap();
Logger log = LoggerFactory.getLogger(LoginListenner.class);
public void sessionDestroyed(HttpSessionEvent se) {
// 如果session销毁, 则从map中移除这个用户
try {
HttpSession session = se.getSession();
System.err.println(session);
// mobileLoginUser.remove(se.getSession());
User user = (User) se.getSession().getAttribute(Constants.USER_INFO);
if(null != user && StringUtils.isNotBlank(user.getUserLoginNumber()))
map.remove(user.getUserLoginNumber());
/*
在这里做区分异地登陆和超时登陆跳转不同的错误页面的区分:
超时登陆:由系统自动销毁,没有标识符
异地登陆:手动销毁,销毁前用setAttribute()标识这个session销毁原因是异地登陆
注意:如果是另外有手动调用session.invalidate(),也需要注明,如下手动退出
然后,因为session是要销毁的,这里我们用一个公共类的公共变量Map存储标识符,
然后去拦截器中判断,并控制跳转错误页面。
*/
// 判断session是怎么被销毁的, 并存入Map给拦截器判断(区分超时登录和异地登陆)
if (null == se.getSession().getAttribute("sessionDestroyedStatus")) {// 超时自动销毁
CommonUtil.sessionStatusMap.remove("sessionDestroyedStatus");
CommonUtil.sessionStatusMap.put("sessionDestroyedStatus", "timeout");
} else if (se.getSession().getAttribute("sessionDestroyedStatus").equals("busy")) {// 异地登陆时setAttribute
CommonUtil.sessionStatusMap.remove("sessionDestroyedStatus");
CommonUtil.sessionStatusMap.put("sessionDestroyedStatus", "busy");
} else if (se.getSession().getAttribute("sessionDestroyedStatus").equals("logout")) {// 手动退出
CommonUtil.sessionStatusMap.remove("sessionDestroyedStatus");
CommonUtil.sessionStatusMap.put("sessionDestroyedStatus", "logout");
}
}catch (Exception e) {
log.error(e.getLocalizedMessage(),e);
}
}
/**
* 当向session中放入数据触发
*/
public void attributeAdded(HttpSessionBindingEvent event) {
String name = event.getName();
if (name.equals(Constants.USER_INFO)) {
User user = (User) event.getValue();
// if (map.get(Constants.USER_INFO) != null) {
// HttpSession session = map.get(user.getUserLoginNumber());
// session.removeAttribute(user.getUserLoginNumber());
// session.invalidate();
// }
map.put(user.getUserLoginNumber(), event.getSession());
}
}
/**
* 当向session中移除数据触发
*/
public void attributeRemoved(HttpSessionBindingEvent event) {
String name = event.getName();
if (name.equals(Constants.USER_INFO)) {
User user = (User) event.getValue();
map.remove(user.getUserLoginNumber());
}
}
public void attributeReplaced(HttpSessionBindingEvent event) {
}
public static HttpSession isLogonUser(String loginNumber) {
return map.get(loginNumber);
}
@Override
public void sessionCreated(HttpSessionEvent se) {
}
}
然后我们在springMVC的拦截器中实现(拦截器配置这里就不写了):
import java.io.PrintWriter;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.nielsen.sfa.model.User;
import com.nielsen.sfa.utils.CommonUtil;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String contextPath = request.getServletContext().getContextPath();
HttpSession session = request.getSession();
User user=(User)session.getAttribute("userInfo");
/*
在监听器中处理好的map,在这里判断,跳转对应的错误页面
注意:跳转前需要把公共map中的值remove掉,否则会报错
*/
if (user == null || StringUtils.isEmpty(user.getUserLoginNumber())) {
/*
这里单独处理拦截AJAX请求的原因:session销毁后如用户正在进行AJAX操作中(打开AJAX页面或控件部件,只差一个提交的情况)可以正常操作
*/
// 拦截AJAX请求
if (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").equalsIgnoreCase("XMLHttpRequest")) {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html");
// 判断session销毁的状态,设置header,在前端ajax error中判断
if (null == CommonUtil.sessionStatusMap.get("sessionDestroyedStatus")
|| CommonUtil.sessionStatusMap.get("sessionDestroyedStatus").equals("timeout")) {// session超时过期
CommonUtil.sessionStatusMap.remove("sessionDestroyedStatus");
response.setHeader("sessionStatus", "timeout");
return false;
} else if (CommonUtil.sessionStatusMap.get("sessionDestroyedStatus").equals("busy")) {// 异地登陆
CommonUtil.sessionStatusMap.remove("sessionDestroyedStatus");
response.setHeader("sessionStatus", "busy");
return false;
}
}
// "sessionDestroyedStatus"在loginController中设置
if (null == CommonUtil.sessionStatusMap.get("sessionDestroyedStatus")
|| CommonUtil.sessionStatusMap.get("sessionDestroyedStatus").equals("timeout")) {// session超时过期
CommonUtil.sessionStatusMap.remove("sessionDestroyedStatus");
String requestStatus = request.getParameter("requestStatus");
/*
文件上传也会拦截不了,所以手动在前端设置标识符
*/
if (requestStatus != null && requestStatus.equals("uploadFile")) {// 如果该请求是文件上传
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html");
PrintWriter out = null;
out = response.getWriter();
out.print("timeout");
out.flush();
} else {
response.sendRedirect(contextPath + "/jsp/login.jsp");
}
return false;
} else if (CommonUtil.sessionStatusMap.get("sessionDestroyedStatus").equals("busy")) {// 异地登陆
CommonUtil.sessionStatusMap.remove("sessionDestroyedStatus");
response.sendRedirect(contextPath + "/jsp/abnormal/notAuthorized.jsp");
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
3. 区分
这里的区分主要是跳转区分,就是根据存储在内存中的公共map标识符,在拦截器中判断跳转,代码如上。
三、建议
百度了一圈发现大家都建议对session的attribute进行操作而不建议销毁整个session,因为会遇到如下的几个坑,还有后期维护不方便,还好我写了很多注释,感觉不加注释以后别人要理解好久。
1. 说一下遇到的坑
因为session调用销毁方法,已经销毁了,你无法判断这个session有没有被销毁,null != session可不行,因为session销毁的原理是:“里面的值被清空,就是Attribute清空,然后这个session对象还在的,但是只是把isValid属性设置为false,并且对象不能被获取了,session id不变,除非你又创建了新的session”。
就是当我们要手动销毁session时,外面无法判断session有没有被销毁,用null != session判断显然是不行的,用session.isNew()和request.isRequestedSessionIDValid()判断,经测试也是不行的,当session已经被销毁,再进行session.invalidate()等对session进行操作的方法操作时,就会报错“session already invalidate”。
而最直观的isValid属性只能在DEBUG模式下看到,而不能直接调用判断,就是这一点比较坑,目前我在百度找的还没有什么解决办法,希望我是抛砖引玉,有大神可以解答,或者我以后找到了解决方案会随时更新。