Spring Security | 会话管理

895 阅读5分钟

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」。

前言

由于HTTP协议是无状态协议,对于服务器而言每个请求都一样,缺少一个状态去区分请求是否来自于不同的用户,以便服务器提供不同的服务。
所以我们需要利用某种机制来记录不同用户的标识信息。这个机制就是Session
这个时候cookie就体现了它的重要作用

  • 当客户端首次请求服务端时
  • 服务端为该用户生成一个sessionId,并保存在cookie中,带回客户端,客户端保存这个cookie
  • 之后客户端每次请求都带上这个cookie
  • 服务端可以很容易区分是来自哪个用户的请求。

但出于安全考虑,有时用户在浏览器中禁用cookie,这个时候可以利用URL重新,将sessionId拼接在重新的URL后面返回给已经授权的用户。

一、 防御会话固定攻击

会话攻击

  • 攻击者自己正常访问系统,系统给攻击者分配了一个sessionId
  • 攻击者拿着自己手上的sessionId伪造一个系统登录链接
  • 受害者利用链接登陆了,那么sessionId绑定了用户的信息
  • 攻击者可以利用手里的sessionId冒充受害者

当我们登录成功之后重新生成新的 session,即可避免

Spring Security自带该功能,并且自带的HTTP防火墙会帮我们拦截掉哪些拼接的不合法的URL

sessionManagement是一个会话管理的配置器,其中,防御会话固定攻击的策略有四种:

  • none:不做任何变动,登录之后沿用旧的session
  • newSession:登录之后创建一个新的session
  • migrateSession:登录之后创建一个新的session,并将旧的session中的数据复制过来。
  • changeSessionId:不创建新的会话,而是使用由Servlet容器提供的会话固定保护。

默认已经启用migrateSession策略,如有必要,可以做出修改。

@Override
protected void configure(HttpSecurity http) throws Exception {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    http.authorizeRequests()
        .antMatchers("/admin/api/**").hasRole("ADMIN")
        .antMatchers("/user/api/**").hasRole("USER")
        .antMatchers("/app/api/**", "/captcha.jpg").permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        //AuthenticationDetailsSource
        //                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
        .loginPage("/myLogin.html")
        // 指定处理登录请求的路径,修改请求的路径,默认为/login
        .loginProcessingUrl("/mylogin").permitAll()
        .failureHandler(new MyAuthenticationFailureHandler())
        .and()
        //增加自动登录功能,默认为散列加密
        .rememberMe()
        .userDetailsService(myUserDetailsService)
        .tokenRepository(jdbcTokenRepository)
        //设置sessionManagement策略
        .and()
        .sessionManagement()
        .sessionFixation()
        .none()
        .and()
        .csrf().disable();
    	//将过滤器添加在UsernamePasswordAuthenticationFilter之前
    http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
}

二、会话过期

可以通过配置会话过期策略

  • 过期跳转
    .sessionManagement()
      .invalidSessionUrl("/")
    
  • 过期时间
    # 单位秒,最低限制60秒,小于60会被修正为60
    server:
      servlet:
        session:
          timeout: 90
    

三、会话并发控制

  • 异地登录踢掉当前登录用户

    .sessionManagement()
        //设置最大会话数为1
        .maximumSessions(1)
    

    在其它客户端重新登录会挤掉之前登录的账号,并且之前页面会显示

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

    具体实现可看ConcurrentSessionControlAuthenticationStrategy类源码。

  • 已经登录,禁止异地登录

    .sessionManagement()
        //设置最大会话数为1
        .maximumSessions(1)
        //阻止新会话登录,默认为false
        .maxSessionsPreventsLogin(true)
    

    异地登录报错

    {
       "error_code": 401,
       "error_name":"org.springframework.security.web.authentication.session.SessionAuthenticationException",
       "message": "请求失败,Maximum sessions of 1 for this principal exceeded"
    }
    

    看似好像已经没有问题,但是我们将原来登录的用户注销(通过请求/logout),然后再去登录,发现任然登不上 这是因为通过监听session的销毁来触发会话信息的表相关清理工作,但我们还没有注册过相关的监听器,所以导致Spring Security无法正常清理过期或已注销的会话。

    在Servlet中,监听session相关事件的方法是实现HttpSessionListener接口,并在系统中注册该监听 器。

    Spring SecurityHttpSessionEventPublisher类中实现HttpSessionEventPublisher接口,并转化成 Spring的事件机制。

    Spring事件机制中,事件的发布、订阅都交由Spring容器来托管,我们可以很方便地通过注册 bean的方式来订阅关心的事件。

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

注意:principals采用了以用户信息为 key 的设计,必须覆写 UserhashCodeequals 两个方法

四、集群会话解决方案

当系统采用集群部署时,通常请求会先集中在一个中间件上(Nginx),再通过其转发到对应服务上,达到负载均衡的目的。
这样就会出现,我在A服务已经登录过,但是当请求被转发到B的时候,用户又要重新登录,这就是典型的会话状态集群不同步问题。 常见解决方案:

  • session保持
    • 通常采用IP哈希负载策略将来自相同客户端的请求转发至相同的服务器上进行处理。
    • 存在一定程度的负载失衡
  • session复制
    • session复制是指在集群服务器之间同步session数据,以达到各个实例之间会话状态一致的做法。
    • 消耗数据带宽,还会占用大量的资源。
  • session共享
    • session 共享是指将 session 从服务器内存抽离出来,集中存储到独立的数据容器,并由各个服务器共享。
    • 独立的数据容器增加了网络交互,数据容器的读/写性能、稳定性以及网络I/O速度都成为性能的瓶颈。

五、整合Spring Session解决集群会话问题

session共享,本质上就是存储容器的变动 Spring Session 支持多种类型的存储容器

基于Redis整合

  • 为工程引入依赖
<!--spring session对接Redis必要依赖-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--spring boot对接Redis必要依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 之后就可以配置Spring Session了,主要是为Spring Security提供集群支持的会话注册表。
  • 修改配置文件
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/springSecurityDemo?useUnicode=true&&characterEncoding=utf8&&useSSL=false&&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: 127.0.0.1
    port: 6379
    database: 2
    timeout: 1000s
  session:
    store-type: redis
    timeout: 1800000
  • 重启项目登录
  • 通过Redis客户端查看

image-20201023145928025.png