16、Spring Security 实战 - SpringSecurity 会话管理

当浏览器调用登录接口登录成功后,服务端会和浏览器之间建立一个会话 (Session) 浏览器在每次发送请求时都会携带一个 Sessionld,服务端则根据这个 Sessionld 来判断用户身份。当浏览器关闭后,服务端的 Session 并不会自动销毁,需要开发者手动在服务端调用 Session销毁方法,或者等 Session 过期时间到了自动销毁。在Spring
Security 中,与HttpSession相关的功能由 SessionManagementFiter 和SessionAutheaticationStrateey 接口来处理,SessionManagomentFilter 过滤器将 Session 相关操作委托给 SessionAuthenticationStrateey 接口去完成。

1. 会话并发管理

会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一个设备对应一个会话,那么也可以简单理解为同一个用户可以同时在多少台设备上进行登录。默认情况下,同一用户在多少台设备上登录并没有限制,不过开发者可以在 Spring Security中对此进行配置。

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .sessionManagement() // 开启会话管理
                .maximumSessions(1); // 设置会话并发数为1
    }

    public HttpSessionEventPublisher httpSessionEventPublisher(){
   
     
        return new HttpSessionEventPublisher();
    }
}

  • sessionManagement() 用来开启会话管理、maximumSessions 指定会话的并发数为 1。
  • HttpSessionEventPublisher 提供一一个Htp SessionEvenePubishor-实例。
    Spring Security中通过一个 Map 集合来集护当前的 Http Session 记录,进而实现会话的并发管理。当用户登录成功时,就向集合中添加一条Http Session 记录;当会话销毁时,就从集合中移除一条 Httpsession 记录。HtpSesionEvenPublisher 实现了 Fttp SessionListener 接口,可以监听到HtpSession 的创建和销毀事件,并将 Fltp Session 的创建/销毁事件发布出去,这样,当有 HttpSession 销毀时,Spring Security 就可以感知到该事件了。

2. 会话失效处理

1. 传统 web 开发处理

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
            	// 开启会话管理
                .sessionManagement() 
            	// 允许同一个用户只能创建一个会话
                .maximumSessions(1)  
            	// 会话失效后的跳转路径
                .expiredUrl("/login.html"); 
    }

    public HttpSessionEventPublisher httpSessionEventPublisher(){
   
     
        return new HttpSessionEventPublisher();
    }
}

2. 前后端分离开发处理

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
           		 // 开启会话管理
                .sessionManagement() 
            	 // 允许同一个用户只能创建一个会话
                .maximumSessions(1)  
            	 // 会话失效处理
                .expiredSessionStrategy(event->{
   
     
                    HttpServletResponse response = event.getResponse();
                    response.setContentType("application/json;charset=UTF-8");
                    Map<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已经失效,请重新登录!");
                    String s = new ObjectMapper().writeValueAsString(result);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(s);
                    response.flushBuffer();
                 });
    }

    public HttpSessionEventPublisher httpSessionEventPublisher(){
   
     
        return new HttpSessionEventPublisher();
    }
}

3. 禁止再次登录

默认的效果是一种被 “挤下线”的效果,后面登录的用户会把前面登录的用户 “挤下线”。还有一种是禁止后来者登录,即一旦当前用户登录成功,后来者无法再次使用相同的用户登录,直到当前用户主动注销登录,配置如下:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
           		 // 开启会话管理
                .sessionManagement() 
            	 // 允许同一个用户只能创建一个会话
                .maximumSessions(1)  
            	 // 会话失效处理
                .expiredSessionStrategy(event->{
                    HttpServletResponse response = event.getResponse();
                    response.setContentType("application/json;charset=UTF-8");
                    Map<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已经失效,请重新登录!");
                    String s = new ObjectMapper().writeValueAsString(result);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(s);
                    response.flushBuffer();
                 })
                 // 登录之后禁止再次登录
                .maxSessionsPreventsLogin(true);
    }

    public HttpSessionEventPublisher httpSessionEventPublisher(){
        return new HttpSessionEventPublisher();
    }
}

4. 会话共享

前面所讲的会话管理都是单机上的会话管理,如果当前是集群环境,前面所讲的会话管理方案就会失效。此时可以利用 spring-session 结合 redis 实现 session 共享。

①引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

②SpringBoot 配置:

spring.redis.host=localhost
spring.redis.port=6379

③SpringSecurity 配置:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     
    
    @Autowired
    private FindByIndexNameSessionRepository sessionRepository;
    
    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User
                .withUsername("root")
                .password("{noop}123")
                .roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .sessionManagement() // 开启会话管理
                .maximumSessions(1)  // 允许同一个用户只能创建一个会话
                .expiredSessionStrategy(event->{
   
     
                    HttpServletResponse response = event.getResponse();
                    response.setContentType("application/json;charset=UTF-8");
                    Map<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已经失效,请重新登录!");
                    String s = new ObjectMapper().writeValueAsString(result);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(s);
                    response.flushBuffer();
                 })
                .sessionRegistry(sessionRegistry());
    }
    
    public SpringSessionBackedSessionRegistry sessionRegistry(){
   
     
        return new SpringSessionBackedSessionRegistry(sessionRepository);
    }

    public HttpSessionEventPublisher httpSessionEventPublisher(){
   
     
        return new HttpSessionEventPublisher();
    }
}