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

一、原理

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

转载于:https://my.oschina.net/NamiZone/blog/871542

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值