SpringSecurity(十五):会话管理


当浏览器登录后,服务器和浏览器之间会建立一个会话(Session), 浏览器在每次发送请求时都会携带一个SessionId,服务端根据这个SessionId判断用户身份。浏览器关闭后Session不会自动销毁。需要开发者手动在服务端调用Session销毁方法,或者等待Session过期自动销毁。

在Spring Security中与HttpSession有关的功能由SessionManagementFilter和SessionAuthenticationStrategy接口处理,SessionManagementFilter将Session相关操作委托给SessionAuthenticationStrategy接口去完成。

会话并发管理

会话并发管理是指在当前系统中,同一个用户可以同时创建多少个会话,如果一台设备对应一个会话,也可以理解为同一个用户可以同时在多少个设备上进行登录。

在Spring Security中默认情况下,同一个用户在多少个设备上登录并没有限制,但是我们可以自己设置。


public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .maximumSessions(1);
    }
}

(1)在configure(HttpSecurity)方法中通过sessionManagerment()方法开启会话配置,并设置并发数为1
(2)提供一个httpSessionEventPublisher实例。Spring Security中通过一个Map集合来维护当前HttpSession记录,进而实现会话的并发管理,当用户登录成功后,就向集合添加一条HttpSession记录,会话销毁后,就从集合移除一条HttpSession记录。HttpSessionEventPublisher实现了HttpSessionListener接口,可以监听到HttpSession的创建和销毁事件,并将HttpSession的创建和销毁事件发布出去,这样当有HttpSession销毁时,Spring Security就可以感知到该事件了

我们也可以自定义销毁后的行为


public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .maximumSessions(1)
                .expiredUrl("/expireSession");
    }
}

表明被挤下线后访问/expireSession这个请求,我们可以在controller中为这个请求返回页面或json数据

当然这一种“挤下线”的行为,我们也可以定义后者无法登陆


public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
                .expiredUrl("/expireSession");
    }
}

会话固定攻击与防御

会话固定攻击是一种潜在的风险,恶意攻击者可能通过访问当前应用程序来创建会话,然后诱导用户以相同的会话id登陆(通常是将会话ID作为请求参数放在请求连接中然后诱导用户去点击) 进而获取用户的登录身份

举一个简单的例子:
(1)攻击者自己可以正常访问javaboy网站,在访问的过程中,网站给攻击者分配了一个sessionId
(2)攻击者利用自己的SessionId构造了一个javaboy的链接,并把链接发给受害者
(3)受害者访问该链接,登录网站,一个合法的会话就建立了
(4)攻击者利用手里的sessionId冒充受害者

如果网站支持URL重写就更容易了!(支持把sessionId放在请求地址中)

Spring Security从三个方面防范会话固定攻击:
(1)Spring Security自带Http防火墙,如果sessionid放在地址栏中,这个请求就会直接被拦截下来
(2)在Http响应的Set-Cookie字段中有httpOnly属性,这样避免了通过XSS攻击来获取Cookie中的会话信息,进而达成会话固定攻击
(3)既然会话固定攻击是由于sessionid不变导致的,那么其中一个解决方案就是在用户登录成功后,改变sessionid,Spring Security中默认实现了这种方案,实现类就是ChangeSessionIdAuthenticationStrategy

前两种都是默认行为,一般来说不需要更改。第三种方案在Spring Security中有几种不同的配置策略,我们先来看以下配置方式:

http.sessionManagement().sessionFixation().changeSessionId();

通过sessionFixation()方法开启会话固定攻击防御的配置,一共有四种不同的策略,不同策略对应了不同的SessionAuthenticationStrategy:
(1)changeSessionId():用户登录成功后,直接修改HttpSession的SessionId即可,默认方案即此,对应的处理类是ChangeSessionIdAuthenticationStrategy
(2)none():用户登录成功后,HttpSession不做任何变化,对应的处理类是NullAuthenticatedSessionStrategy
(3)migrateSession():用户登录成功后,创建一个新的HttpSession对象,并将旧的HttpSession中的数据拷贝到新的HttpSession中,对应的处理类是SessionFixationProtectionStrategy
(4)newSession():用户登录成功后,穿件一个新的HttpSession对象,对应的处理类也是SessionFixationProtectionStrategy,只不过将其里边的migrateSessionAttributes属性设置为false。需要注意的是,该方法并非所有的属性都不可拷贝,一些Spring Security使用的属性,如请求缓存,还是会从旧的HttpSession上复制到新的HttpSession。

Session共享

需要注意的是,我们这里讨论的范畴是有状态登录,如果用户采用无状态的认证方式,那么就不涉及会话。也不存在我们接下来要讨论的问题。

为了解决集群环境下的会话问题,我们有三种方案:
(1)Session复制:多个服务之间互相复制Session信息,这样每个服务中都包含有所有的Session信息了,Tomcat通过IP组播对这种方案提供支持。但是这种方案占用带宽,有时延,服务数量越多效率越低,所以使用较少
(2)Session粘滞:也叫会话保持,就是在Nginx上通过一致性Hash,将Hash结果相同的请求总是分发到一个服务上去,这种方案可以解决一部分集群会话带来的问题,但是无法解决集群会话并发管理问题
(3)Session共享:Session共享就是将不同服务的会话统一放在一个地方,所有的服务共享一个会话,一般使用一些Key-Value数据库来存储Session,例如在redis中,比较常见的方案是使用redis存储,session共享方案由于其简便性和稳定性,是目前使用较多的方案。

Session共享目前使用较多的是spring-session,利用spring-session可以方便的实现session的管理

1.引入redis,spring security,spring session的依赖
2.配置redis连接信息
3.修改配置类


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    FindByIndexNameSessionRepository sessionRepository;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .maximumSessions(1)
                .sessionRegistry(sessionRegistry());
    }
    @Bean
    SpringSessionBackedSessionRegistry sessionRegistry(){
        return  new SpringSessionBackedSessionRegistry(sessionRepository);
    }
}

在这段配置中,我们首先注入一个FindByIndexNameSessionRepository对象,这是一个会话的存储和加载工具。在前面的案例中,会话信息是保存在内存中,现在的会话信息保存在redis中,具体的保存和加载工程则由FindByIndexNameSessionRepository接口的实现类来完成,默认是RedisIndexedSessionRepository即我们一开始注入的实际是一个RedisIndexedSessionRepository类型的对象

接下来我们还配置了一个SpringSessionBackedSessionRegistry实例,构建时传入sessionRepository,SpringSessionBackedSessionRegistry继承自SessionRegistry,用来维护会话信息注册表

最后在HttpSecurity中配置sessionRegistry即可,相当于spring-session提供的SpringSessionBackedSessionRegistry接管了会话信息注册表的维护工作。

需要注意的是引入spring-session后不需要在配置HttpSessionEventPublicsher实例,引入spring-session通过SessionRepositoryFilter将请求对象重新封装为SessionRepositoryRequestWrapper,并重写了getSession方法,在重写的getSession方法中,最终返回的是HttpSessionWrapper实例,而在HttpSessionWrapper定义时,就重写了invalidate方法,当调用会话的invalidate方法去销毁会话时,就会调用RedisIndexedSessionRepository中的方法,从Redis中移除相应的会话信息,所以不再需要HttpSessionEventPublisher实例。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM