管理员如何踢掉登录用户?

本康评科技 2024-06-19 20:31:12

这是 Spring Security 学习小组有小伙伴提的一个问题:

感觉这个问题还有点意思,拿出来和各位小伙伴一起分享下。

一 问题分析

首先大家注意限制条件:常规 Session 方案。

如果不是这几个字,这个问题根本就不是问题,如果是 JWT+Redis 这种方案,这个问题很好解决,自己随随便便几段逻辑处理就行了。问题是常规 Session 方案,也就是 Spring Security 默认的方案,Spring Security 默认情况下,登录用户信息保存在 HttpSession 中,HttpSession 不同用户又是不一样的 HttpSession,相当于你在一个 HttpSession 对象中要使另外一个 HttpSession 对象失效,这是这个小伙伴困惑的地方。

二 解决思路

Spring Security 中提供了一个会话并发管理的功能,就是可以设置同一个用户并发登录的数量,比如 javaboy 的并发登录数量为 1,那么 javaboy 就只能在一台设备上登录,在在其他设备登录就会被拒绝,或者其他设备登录会自动踢掉当前登录。

这一功能实现的原理是 Spring Security 中用了一个会话注册器 SessionRegistry 去统一管理登录用户的会话,当用户登录成功之后,讲用户信息保存在一个类型为 ConcurrentMap<Object, Set<String>> principals 的 Map 中,这里的 key 就是登录的用户对象,value 就是登录用户的 sessionId,当然如果想获取到登录用户会话更为详细的信息,还有一个类型为 Map<String, SessionInformation> sessionIds 的 Map,这个 Map 的 key 则是 sessionId。通过对这两个 Map 中的数据进行管理,就能实现对用户并发登录的控制。

相同的道理,我这里也想借鉴已有的功能,在这个功能的基础上,实现管理员踢出已登录用户,这样就会方便很多。

管理员踢出用户的时候,只需要遍历 principals 集合,根据用户名找出来这个用户登录的 sessionId,然后再根据 sessionId 去 sessionIds 里找到会话对应的 SessionInformation,然后令这些会话失效即可。

三 参考代码

首先需要我们自己提供 SessionRegistry 对象:

@Configurationpublic SecurityConfig { @Bean SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } @Bean UserDetailsService us() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("zhangsan").password("{noop}123").roles("ADMIN").build()); manager.createUser(User.withUsername("lisi").password("{noop}123").roles("ADMIN").build()); return manager; } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(a -> a.anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .csrf(c -> c.disable()) .sessionManagement(s -> s.maximumSessions(Integer.MAX_VALUE).sessionRegistry(sessionRegistry())); return http.build(); } @Bean HttpSessionEventPublisher sessionEventPublisher() { return new HttpSessionEventPublisher(); }}

在配置 SecurityFilterChain 的时候,传入自己配置的 sessionRegistry。

这里有一个需要注意的点,就是要开启会话并发管理,只有开启了会话并发管理,第二小节我们说的那些思路才是有效的,否则这些思路不会生效。那么怎么开启会话并发管理呢?设置会话的最大并发数即可,如果你本身并不想限制,那么这个并发数可以设置为 Integer.MAX_VALUE。

这里涉及到的其他内容我就不多说了,都是课程中讲的关于会话并发管理的内容。

最后,踢出用户的逻辑如下:

@Servicepublic LogoutService { @Autowired SessionRegistry sessionRegistry; public void logout(String username) { List<Object> principals = sessionRegistry.getAllPrincipals(); for (Object principal : principals) { if (principal instanceof User u) { String name = u.getUsername(); if (name.equals(username)) { List<SessionInformation> allSessions = sessionRegistry.getAllSessions(u, false); for (SessionInformation session : allSessions) { session.expireNow(); } } } } }}

参数 username 就是管理员要踢出去的用户名。

sessionRegistry.getAllPrincipals(); 是获取到所有的登录用户信息,然后遍历,根据用户名找到要踢出去的用户,然后调用 sessionRegistry.getAllSessions 方法获取该用户的所有会话信息,遍历这些会话,挨个调用其 expireNow() 方法,使之失效。

这样,当用户被踢下线的感觉就像是会话并发控制的时候,被其他客户端挤下线的感觉。

当然,也可以给用户一个明确提示,类似下面这样:

.sessionManagement(s -> s.maximumSessions(Integer.MAX_VALUE).sessionRegistry(sessionRegistry()).expiredSessionStrategy(event -> { HttpServletResponse response = event.getResponse(); response.setContentType("text/html;charset=utf-8"); response.getWriter() .print("你被管理员踢下线了"); response.flushBuffer();}));

OK,大功告成。

0 阅读:0

本康评科技

简介:感谢大家的关注