原创

Spring Security(5)—— 控制用户并发登入,并剔除前一个用户(附完整项目代码)

1. 安全框架Security实现:只允许一个用户登入

项目系统的安全框架Security实现系统只允许一个用户登入。现在市场上有两个非常大的应用软件和网站分别对应两种并发登录控制。

1.我们使用QQ时候,在公司电脑登录上QQ客户端后,就会挤掉家里电脑的QQ客户端登入状态,家里电脑就会退出。
2.爱奇艺视频网站会员登录有数量限制,一个会员账号不能在不同手机上登录,超过限制后就无法登录(会员网站防止一个会员账号被无数人使用,影响会员权益收入)。

那么针对这两种使用场景,Spring Security安全框架如何实现呢?

这里,空白就给大家带来这两种场景的完整Demo项目。其实Security安全框架提供了非常简单方便的实现方式:全程配置话方式。项目地址获取方式会在项目末尾提供。

从算法和数据结构的角度,我在最后的部分给出了我的思考,大家可以参考看看。

2. 两种控制方式:只允许一个用户登入

第一种:用户新登录后,剔除上一次登录的用户,可以通过下面简单的配置:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:on
        http.authorizeRequests()
                .antMatchers("/css/**", "/js/**", "/fonts/**").permitAll()  // 允许访问资源
                .antMatchers("/", "/home", "/about", "/login").permitAll() //允许访问这三个路由
                .antMatchers("/admin/**").hasAnyRole("ADMIN")   // 满足该条件下的路由需要ROLE_ADMIN的角色
                .antMatchers("/user/**").hasAnyRole("USER")     // 满足该条件下的路由需要ROLE_USER的角色
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
//                .loginProcessingUrl("/login")
//                .usernameParameter("username")
//                .passwordParameter("password")
//                .successForwardUrl("/admin")
                .defaultSuccessUrl("/admin")//默认的成功跳转到的url
                .failureUrl("/403")
                .and()
                .rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(300)//有效时间为30s
//                    .userDetailsService(userDetailsService())//userdetailservice使用了另外一种方式注册进去。
                .and()
                .logout()
                .permitAll()
                .and()
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()
                .csrf().disable()
                .sessionManagement()
                .maximumSessions(1);
//                .maxSessionsPreventsLogin(true);

    }

起到关键的配置项是sessionManagement().maximumSessions(1),配置完后启动项目,分别使用不同的不同的浏览器登录,第一次登录和第二次登录都会成功,但是第一次登录的页面刷新后,就会出现如下提示。

This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

提示内容是,登录的session对象过期了,说明第一次登录session失效了。

第二种:第一次登录后,禁止相同用户再次登录。

Security框架实现这种场景的方式也非常简单,配置项比第一种情况多一个,如下。


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:on
        http.authorizeRequests()
                .antMatchers("/css/**", "/js/**", "/fonts/**").permitAll()  // 允许访问资源
                .antMatchers("/", "/home", "/about", "/login").permitAll() //允许访问这三个路由
                .antMatchers("/admin/**").hasAnyRole("ADMIN")   // 满足该条件下的路由需要ROLE_ADMIN的角色
                .antMatchers("/user/**").hasAnyRole("USER")     // 满足该条件下的路由需要ROLE_USER的角色
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
//                .loginProcessingUrl("/login")
//                .usernameParameter("username")
//                .passwordParameter("password")
//                .successForwardUrl("/admin")
                .defaultSuccessUrl("/admin")//默认的成功跳转到的url
                .failureUrl("/403")
                .and()
                .rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(300)//有效时间为30s
//                    .userDetailsService(userDetailsService())//userdetailservice使用了另外一种方式注册进去。
                .and()
                .logout()
                .permitAll()
                .and()
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()
                .csrf().disable()
                .sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true);

    }

多出的配置项是:.maxSessionsPreventsLogin(true)。禁止后续相同用户的登录操作。

用户第一次登录成功后,再使用其他浏览器再次尝试登录,就一直不成功。

3. 用户登入控制出现的问题及解决方案

这里会有一个问题,就是在第二种场景下,用户登录后就禁止相同用户再次登录。但是注销第一次登录后,再次登录也会一直失败,导致这个用户就再也登录不了了。大家可以复现一下这个问题。

分析原因

登录状态注销时候,security框架中session会话信息清除机制是靠监听session的销毁事件来实现的。用户注销后虽然清除了spring中的session对象,但是并没有及时清除security中该session对象的信息。因此当有用户再次登录的时候,就不再允许用户登录。

那怎样才能及时清除security中的session信息?

清除session对象时候,spring都会将调用所有注册的实现了HttpSessionListener接口的bean。具体的逻辑在实现类中实行。HttpSessionEventPublisher具体逻辑如下:

public void sessionCreated(HttpSessionEvent event) {
    HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession());
    getContext(event.getSession().getServletContext()).publishEvent(e);
}
public void sessionDestroyed(HttpSessionEvent event) {
    HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());
    getContext(event.getSession().getServletContext()).publishEvent(e);
}

因此,注册了这个Bean后,spring每次session的消除,都会调用这个bean的sessionDestroyed方法,该方法发布一个session消除的事件,而事件就会被sping Security框架监听到,并消除对应session的信息。

解决方案如下,在配置类中添加下面的代码。

@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

4. 简单谈谈并发登入控制逻辑的设计

作为coder,以探究事务本质的思维方式来简单窥探下Security实现并发session控制逻辑的原理概要。任何机制功能实现都离不开两个要素:算法和数据结构。那么并发控制的数据结构会是什么?算法机制又会是什么?在往下看之前大家可以自己思考1分钟。

数据结构:用户名在系统中是唯一的,而用户每次登入都会创建一个session对象,如果用户多次登入就会有多个session对象,
那么我们就需要使用map数据结构,以username为key来映射登录session对象数组。每次登入创建的新session对象放入到数组中,如果退出或过期则将session对象从数组中删除。

算法机制:用户新登入,没有并发限制,就将新session对象放入数组,如果有并发控制挤掉上一个用户,则让数组中所有session失效并插入新session;如果并发控制有数量限制,则判断此次登录是否超过限制,再做相应的处理。

以上就是从数据结构和算法机制的思维角度来分析Security框架的并发控制机制。

完整的demo项目,请关注公众号“前沿科技bot“并发送"SEC-SIX"获取。

alt

正文到此结束
本文目录