之前我听说的单点登录有两种:
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() {}
}
这样,整个功能基本就做完了。我也做了测试,也实现了我想要的功能。代码里之前打了多余的日志,几乎也去掉了。
剩余的地方可以优化优化。
这个做饭我从思考时到做完觉得还可以,几乎不用侵入业务代码。非常的低耦合。