文档章节

Java-实现异地登陆和超时登陆

吃兔纸不吐毛
 吃兔纸不吐毛
发布于 2017/03/31 14:28
字数 1849
阅读 196
收藏 0

一、原理

1. 异地登陆

    同一个账号,在不同的电脑(也可以不同的浏览器)登陆系统,前一个已经登陆的账号session被销毁,用户进行下一步操作时跳转错误页面。

2. 超时登陆

    登陆后无操作*分钟后自动销毁session,用户进行下一步操作时跳转错误页面。

3. 区分

    异地登陆和超时登陆起效时跳转的错误页面不相同。

 

二、实现

1. 超时登陆

    由系统控制,在web.xml中配置,或者由监听器控制,利用session.setMaxInactiveInterval(interval);方法控制,本次主要展示在web.xml中配置的方法,如下:

<session-config>
  <session-timeout>30</session-timeout><!-- 单位:分钟 -->
</session-config>

 

2. 异地登陆

    首先在Controller中判断登陆用户的账号密码是否正确,通过以后判断session是不是异地登陆(过程在监听器中判断,此处判断监听器处理后的返回值),如果是,session销毁,如下:

@RequestMapping("/loginCheck")
@ResponseBody
public Map<String, Object> loginCheck(HttpServletRequest request, String userLoginNumber, String userLoginPasswd) {

    Map<String, Object> ret = new HashMap<String, Object>(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<String, HttpSession> map = new HashMap<String, HttpSession>();  
		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模式下看到,而不能直接调用判断,就是这一点比较坑,目前我在百度找的还没有什么解决办法,希望我是抛砖引玉,有大神可以解答,或者我以后找到了解决方案会随时更新。

© 著作权归作者所有

吃兔纸不吐毛
粉丝 5
博文 63
码字总数 31925
作品 0
杭州
私信 提问
需要整合自己的java项目实现第三方登陆

需要实现这样的第三方。相当于论坛的登陆页面出现一个自己的第三方登陆按钮。然后点击跳转到自己的java项目的登陆验证页面。自己java项目验证用户and密码成功后。重定向到论坛的首页。如果没...

小帅帅丶
2014/06/23
1K
1
Ajax请求session超时处理流程(DWZ)

名人名言:问题不在于告诉他一个真理,而在于教他怎样去发现真理。——卢梭 DWZ-Java框架Ajax请求session超时处理思路: 1)SessionValidateFilter统一验证session是否超时 2)SessionValida...

GIFCOOL
2012/10/19
286
0
在tomcat中配置JNDI数据源 .

在tomcat5.0中配置数据源(全局数据源、局部数据源),通过连接池机制连接数据库 1. odbc-jdbc桥连 2. 通过加载本地驱动连接 3. 在web应用服务器中设置数据源,通过池接技术连接数据库(加载本...

漠、
2012/05/24
268
0
如何在 Jconsole 监控 Jboss Tomcat

Java在jdk5开始就自带有Jconsole了,要想用Jconsol监控且需要添加启 动参数: Linux系统: JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=8950 -Dcom.sun.management.jmxremot......

红薯
2010/04/11
1K
4
Spring Security and Spring Session

扩展基于JSON登陆 由于长时间位于服务器交互,导致客户端与服务器超时,认证失效。但用户不想跳转到登陆界面进行登陆,期望在当前界面弹窗框进行登陆,然后进行下一步操作。 解决方案:当服务...

呼呼南风
05/21
30
0

没有更多内容

加载失败,请刷新页面

加载更多

PostgreSQL 11.3 locking

rudi
今天
5
0
Mybatis Plus sql注入器

一、继承AbstractMethod /** * @author beth * @data 2019-10-23 20:39 */public class DeleteAllMethod extends AbstractMethod { @Override public MappedStatement injectMap......

一个yuanbeth
今天
11
1
一次写shell脚本的经历记录——特殊字符惹的祸

本文首发于微信公众号“我的小碗汤”,扫码文末二维码即可关注,欢迎一起交流! redis在容器化的过程中,涉及到纵向扩pod实例cpu、内存以及redis实例的maxmemory值,statefulset管理的pod需要...

码农实战
今天
4
0
为什么阿里巴巴Java开发手册中不建议在循环体中使用+进行字符串拼接?

之前在阅读《阿里巴巴Java开发手册》时,发现有一条是关于循环体中字符串拼接的建议,具体内容如下: 那么我们首先来用例子来看看在循环体中用 + 或者用 StringBuilder 进行字符串拼接的效率...

武培轩
今天
9
0
队列-链式(c/c++实现)

队列是在线性表功能稍作修改形成的,在生活中排队是不能插队的吧,先排队先得到对待,慢来得排在最后面,这样来就形成了”先进先出“的队列。作用就是通过伟大的程序员来实现算法解决现实生活...

白客C
今天
81
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部