spring boot使用监听和拦截器实现用户单点登录

之前我听说的单点登录有两种:

1,用户在 一个地点登录之后,另一个地方再次登录,把之前的用户挤掉。我觉得这个是单点登录。

2,用户再一个地方登录后,他会有很多第三方的接口,也同步登录到第三方。我觉得这个交同步登录。

我们这里说的是1,本系统的单点登录。


项目spring boot,使用监听和拦截器写的。

先说一下思路:

1,用户1登录,创建session,将用户信息放入session,并以username和session作为键值对,放入一个静态变量的map。以供后续比对。用户1在另一个地方再次登录,创建session,将用户信息放入session,用username座位key去map中查找,如果存在,那么说明用户1已经登录了。取到session,清空,然后将新登录用户的session保存进去。

这里涉及到:

    a,session的创建也初始赋值

    b,session的清空及销毁时,加强制下线标识

    c,session属性值变化时,如果是登录操作,判断user,然后进行比对。清空session。

这里由于操作都是session,一个是session的创建,一个是session中值的增加。所以最好得方法我觉得是使用session监听。

这里需要两个session监听,一个监听session,一个监听session的属性。

看一下代码:

package com.wm.springboot.listener;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import com.wm.springboot.sc.entity.User;

import lombok.extern.slf4j.Slf4j;

/**
 * session监听器
 * 当有session被创建时,会进入该监听器
 * 比如:request.getSession(),这时候,会进入。
 * @author maybe
 */
@Slf4j
@WebListener
public class SessionListener implements HttpSessionListener {//实现HttpSessionListener接口的监听,是监听session的创建和销毁

	/**
	 * session创建时,初始化session
	 */
	@Override
	public void sessionCreated(HttpSessionEvent se) {
        log.info("Session{}被创建",se.getSession().getId());
        se.getSession().setAttribute("forcedout", "no");//给一个标识,标识新创建的session给他一个标识没有被强制下线
	}

	/**
	 * session失效时调用。
	 * 我发现这个方法被调用的时间比我设置的超时时间要长1分钟左右。
	 * 我设置5分钟,那么低6分钟才会调用这个方法销毁session。
	 * 具体原因不明,还需要看源码。
	 */
	@Override
	public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
		User user = (User) httpSessionEvent.getSession().getAttribute("user");
		if(user!=null) SinglePointListener.map.remove(user.getUsername());//session销毁时,要将session从map中remove掉
		log.info("Session{}被销毁",httpSessionEvent.getSession().getId());
	}

}
package com.wm.springboot.listener;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;

import com.wm.springboot.sc.entity.User;

import lombok.extern.slf4j.Slf4j;

/**
 * 单点登录
 * 监控session中属性user的变化
 * HttpSessionAttributeListener 监听session范围内属性变化
 * @author maybe
 *
 */
@WebListener
@Slf4j
public class SinglePointListener implements HttpSessionAttributeListener{

	//key:username value:session,用于存放已经登录的用户的session
	public static Map map = new HashMap();
	
	/**
	 * 当属性增加时,触发该方法
	 */
	@Override
	public void attributeAdded(HttpSessionBindingEvent httpSessionBindingEvent) {
		User user = (User) httpSessionBindingEvent.getSession().getAttribute("user");
		if(user!=null) {//登录时需要把user信息放入session以供后续使用。session其他值得变化,不在本方法考虑范围内,
			if(SinglePointListener.map!=null) {
				if(SinglePointListener.map.containsKey(user.getUsername())) { //存在key,把之前的session失效,
					log.info("map中存在key={},取出sessionOld清空数据,并设置属性forcedout强制下线");
					HttpSession sessionOld = SinglePointListener.map.get(user.getUsername());
	            	if (sessionOld !=null) {
	            		Enumeration e = sessionOld.getAttributeNames();  
	            		while(e.hasMoreElements()){  
	            			String sessionKeyName = (String) e.nextElement();  
	            			sessionOld.removeAttribute(sessionKeyName);  
	            		}  
	            		sessionOld.setAttribute("forcedout","yes");
					}
				}
			}
			SinglePointListener.map.put(user.getUsername(), httpSessionBindingEvent.getSession());//最后把这次的user和session放入map以供后续比对。
		}
	}

	@Override
	public void attributeRemoved(HttpSessionBindingEvent se) {}

	@Override
	public void attributeReplaced(HttpSessionBindingEvent se) {}

}

这时候其实已经完成了单点登录的内容。这时候第一次登录的用户,如果在进行任何操作,应该要要提示他,“已经被强制下线。请重新登录”。

但是如果只这样就结束了,那么就会出现继续操作然后少数据,出现各种诡异BUG的情况,但如果我们在每个地方都写session的判断,又不切实际。

所以这里我们使用filter进行访问前拦截,判断session中的强制下线标识,如果是yes,那么代表让其强制下线。直接跳转到重新登录页面。


package com.wm.springboot.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import lombok.extern.slf4j.Slf4j;

/**
 * 单点登录拦截器,只拦截.do的访问
 * 并如果session被销毁,使其返回异常页面
 * @author maybe
 */

@Slf4j
@WebFilter(filterName="loginFilter",urlPatterns="*.do")
public class LoginFilter implements Filter {

	private static final String ERRORURL = "/html/index.html";
	
	/**
	 * 拦截器处理方法
	 */
	@Override
	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain fc)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest)req;
		HttpServletResponse response = (HttpServletResponse)resp;
		String uri = request.getRequestURI();
		if(!uri.equals(ERRORURL)) {
			log.info("进入判断是否只有单点登录");
			String forcedout = (String) request.getSession().getAttribute("forcedout");
			if(null!=forcedout&&!"".equals(forcedout)) {
				if(forcedout.equals("yes")) {
					log.info("该用户已经在异地重新登录,进入异常提示!");
					response.sendRedirect(ERRORURL);
					return;
				}
			}
		}
		fc.doFilter(req, resp);
	}

	/**
	 * 在系统启动时初始化拦截器
	 */
	@Override
	public void init(FilterConfig config) throws ServletException {}
	
	/**
	 * 在系统停止时销毁拦截器
	 */
	@Override
	public void destroy() {}

}

这样,整个功能基本就做完了。我也做了测试,也实现了我想要的功能。代码里之前打了多余的日志,几乎也去掉了。

剩余的地方可以优化优化。

这个做饭我从思考时到做完觉得还可以,几乎不用侵入业务代码。非常的低耦合。



你可能感兴趣的:(spring,java)