微人事-session并发问题-踢掉已登录的用户(2)

前言

上一节,了解spring security如何处理session并发。
今天将在微人事项目里处理session并发问题,实现踢掉已登录的用户。

分析

(1)获取session集合通过sessionRegistry对象

	final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
				authentication.getPrincipal(), false);
		

(2)通过阅读源码,sessionRegistry是一个接口,只有一个实现类SessionRegistryImpl,Spring Security中通过SessionRegistryImpl类来实现对会话信息的统一管理

public class SessionRegistryImpl implements SessionRegistry,
  ApplicationListener<SessionDestroyedEvent> {
 /** <principal:Object,SessionIdSet> */
 private final ConcurrentMap<Object, Set<String>> principals;
 /** <sessionId:Object,SessionInformation> */
 private final Map<String, SessionInformation> sessionIds;
 public void registerNewSession(String sessionId, Object principal) {
  if (getSessionInformation(sessionId) != null) {
   removeSessionInformation(sessionId);
  }
  sessionIds.put(sessionId,
    new SessionInformation(principal, sessionId, new Date()));

  principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
   if (sessionsUsedByPrincipal == null) {
    sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
   }
   sessionsUsedByPrincipal.add(sessionId);
   return sessionsUsedByPrincipal;
  });
 }
 public void removeSessionInformation(String sessionId) {
  SessionInformation info = getSessionInformation(sessionId);
  if (info == null) {
   return;
  }
  sessionIds.remove(sessionId);
  principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
   sessionsUsedByPrincipal.remove(sessionId);
   if (sessionsUsedByPrincipal.isEmpty()) {
    sessionsUsedByPrincipal = null;
   }
   return sessionsUsedByPrincipal;
  });
 }

}
  1. 首先大家看到,一上来声明了一个Principals对象,这是一个支持并发访问的map集合,集合的key就是用户的参与者(principal),正常来说,用户的Principal其实就是用户对象,而集合的值则是一个set集合,这个set集合中保存了这个用户对应的sessionid。
  2. 如有新的会话需要添加,就在registerNewSession方法中进行添加,具体是调用principles.compute方法进行添加,key就是principal。
    3.如果用户初始化登录,sessionid需要删除,相关操作在removeSessionInformation方法中完成

看到这里,大家发现一个问题,ConcurrentMap集合的键是主体对象,用对象做键,一定要重写等于方法和hashCode方法,否则第一次存完数据,下次就找不到了,这是JavaSE方面的知识参考
参考: 为啥重写equals 方法和 hashCode 方法

实现

(1)微人事里我们用自定义的过滤器代替了UsernamePasswordAuthenticationFilter,导致前面所讲的关于session的配置,统统中断。所有相关的配置我们都要在新的过滤器LoginFilter中进行配置,包括SessionAuthenticationStrategy也需要我们自己手动配置了。
首先第一步,我们重写Hr类的equals和hashCode方法

public class Hr implements UserDetails {
    ...
    ...
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Hr hr = (Hr) o;
        return Objects.equals(username, hr.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(username);
    }
    ...
    ...
}

(2)接下来在SecurityConfig中进行配置
这里我们要自己提供SessionAuthenticationStrategy,而前面处理会话并发的是ConcurrentSessionControlAuthenticationStrategy,则,我们需要自己提供一个ConcurrentSessionControlAuthenticationStrategy的实例,然后配置给LoginFilter,但是在创建ConcurrentSessionControlAuthenticationStrategy实例的过程中,还需要有一个SessionRegistryImpl对象。

@Bean
SessionRegistryImpl sessionRegistry() {
    return new SessionRegistryImpl();
}
``
然后在LoginFilter中配置SessionAuthenticationStrategy
```java
@Bean
LoginFilter loginFilter() throws Exception {
    LoginFilter loginFilter = new LoginFilter();
    loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                //省略
            }
    );
    loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
                //省略
            }
    );
    loginFilter.setAuthenticationManager(authenticationManagerBean());
    loginFilter.setFilterProcessesUrl("/doLogin");
    ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
    sessionStrategy.setMaximumSessions(1);
    //调用的是AbstractAuthenticationProcessingFilter#setSessionAuthenticationStrategy方法
    loginFilter.setSessionAuthenticationStrategy(sessionStrategy);
    return loginFilter;
}

会话配置还有了一个关键的过滤器叫做ConcurrentSessionFilter,本来这个过滤器是不需要我们管的,但是这个过滤器中也用到了SessionRegistryImpl,而SessionRegistryImpl现在是由我们自己来定义的,所以,该过滤器我们也要重新配置一下,如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            ...
    http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> {
        HttpServletResponse resp = event.getResponse();
        resp.setContentType("application/json;charset=utf-8");
        resp.setStatus(401);
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(RespBean.error("您已在另一台设备登录,本次登录已下线!")));
        out.flush();
        out.close();
    }), ConcurrentSessionFilter.class);
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

在这里,我们重新创建一个ConcurrentSessionFilter的实例,代替系统替换的即可。在创建的新的ConcurrentSessionFilter实例时,需要两个参数:

  1. sessionRegistry就是我们前面提供的SessionRegistryImpl实例。
  2. 第二个参数,是一个处理会话过期后的额外函数,相应的,当用户被另外一个登录踢下线之后,你要给某种的下线提示,就在这里来完成。

(3)最后,我们还需要在处理完登录数据之后,手动向SessionRegistryImpl中添加一条记录:

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Autowired
    SessionRegistry sessionRegistry;
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //省略
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            setDetails(request, authRequest);
            Hr principal = new Hr();
            principal.setUsername(username);
            //注册session
            sessionRegistry.registerNewSession(request.getSession(true).getId(), principal);
            return this.getAuthenticationManager().authenticate(authRequest);
        } 
        ...
        ...
    }
}
到此我们的session并发功能就实现了
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值