Spring Security已经为我们提供了完善的会话管理功能,包括会话固定攻击会话超时检测以及会话并发控制。会话(session)就是无状态的HTTP实现用户状态可维持的一种解决方案。当用户首次访问系统时,系统会该用户生成一个sessionId,并添加到cookie中。在该用户的会话期内,每个请求都自动携带该cookie,因此系统可以很轻易地识别出这个来自那个用户的请求。

一:会话固定攻击

尽管cookie非常好用,但有时用户会在浏览器禁用它。为了解决这个问题,有些服务器还支持用URL重写的方式来实现类似的体验,例如:http://cyh.com/index.jsp;jsessionid=1pjztz08i2u4i,URL重写原本是为了兼容禁用cookie的浏览器而设计的,但却也容易被黑客利用,进而导致会话固定攻击。

会话固定攻击(session fixation attack)是利用应用系统在服务器的会话ID固定不变机制,借助他人用相同的会话ID获取认证和授权,然后利用该会话ID劫持他人的会话以成功冒充他人,造成会话固定攻击。

springsecurity清除session springsecurity session管理_java

整个攻击流程是:
1、攻击者Attacker能正常访问该应用网站;
2、应用网站服务器返回一个会话ID给他;
3、攻击者Attacker用该会话ID构造一个该网站链接发给受害者Victim;
4-5、受害者Victim点击该链接,携带攻击者的会话ID和用户名密码正常登录了该网站,会话成功建立;
6、攻击者Attacker用该会话ID成功冒充并劫持了受害者Victim的会话。

但在Spring Security的HTTP防火墙会帮助我们拦截不合法的URL,当我们试图访问带Session的URL时,服务器会返回异常给用户。
也可以在Spring Security中配置会话管理来防御会话固定攻击,如果不配置Spring Security默认是使用migrateSession,栗子如下

http.authorizeRequests()
                .antMatchers("/api/**").permitAll()
                .antMatchers("/controller/**").authenticated()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login.html")
                .and()
            .sessionManagement()
                .sessionFixation().none();
sessionManagement是一个会话管理的配置器,其中,防御会话固定攻击的策略有四种,
none: 不做任何变动,登录过后仍然使用旧的session
newSession:登录之后创建一个新的session
migrateSession: 登录之后创建一个session,并将旧的session的数据复制到新的session里面 ---->默认
changeSessionId:不创建新的会话,而是使用由servlet容器提供的会话固定保护

二:会话过期
默认情况下,会话的过期时间是30分钟,即在30分钟内没有任何操作便会失效,在Spring Security中,当会话过期,我们可以设置他跳转到某一个地址,配置如下:

.sessionManagement()
.invalidSessionUrl("/login")

或者自定义过期策略,只要实现InvalidSessionStrategy接口,重写onInvalidSessionDetected方法即可,还需要配如下配置:

.sessionManagement()
.invalidSessionStrategy(你重写的实现类)

当然我们可以修改会话的过期时间。在spring boot中,会话过期时间最少为1分钟,即便设置小于60s也会被修正为1分钟。

server.servlet.session.timeout=60

二:会话并发控制
在Spring Security中,会话管理最完善的是会话并发控制,但会话并发控制存在一些用法的陷阱,这个等下说。

配置如下:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    DataSource dataSource;
    @Autowired
    MyUserDetailsServiceImpl myUserDetailsService;
    
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher(){
        return new HttpSessionEventPublisher();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        http.authorizeRequests()
                .antMatchers("/admin/api/**").hasAuthority("admin")
                .antMatchers("/app/api/**").permitAll()
                .antMatchers("/user/api/**").hasAuthority("user")
                .anyRequest().authenticated()
                .and().formLogin()
                .loginPage("/myLogin.html")
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .and()
                .sessionManagement()
                .maximumSessions(1) //最大会话数设置为1
                .maxSessionsPreventsLogin(true); //阻止新会话登录,默认为false

        http.csrf().disable();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

maximumSessions用于设置单个用户允许同时在线的最大会话数,如果没有额外设置,那么新登录的会话会踢掉旧的会话。如果需要在在会话数达到最大数时,阻止新会话建立,而不是踢掉旧的会话,则可以设置maxSessionsPreventsLogin为true。

我们通过将已登录的旧会话注销,访问/logout路径,然后尝试再次登录,发现登录不了了。这是因为Spring security是通过监听session的摧毁时间来触发会话信息表相关清理工作的,现在的问题点是缺乏事件源。但在spring Security中触发事件的类(HttpSessionEventPublisher)是有的,只是没有暴露出来,只需要把HttpSessionEventPublisher注册给IOC容器就可以,如下:

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

上面我们是通过构建UserDetails来实现Spring Security所需的UserDetails,如果我们是通过自定义数据库结构,实现UserDetetails来是实现的话,就会出现问题,发现虽然我们做了会话的限制,但好像不起作用,不管多少个客户端都可以登录。

```php
public class User implements UserDetails {

    private Long id;

    private String username;

    private String password;

    private String roles;

    private boolean enable;

    private List<GrantedAuthority> authorities;

    public String getRoles() {
        return roles;
    }

    public void setRoles(String roles) {
        this.roles = roles;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public boolean isEnable() {
        return enable;
    }

    public void setEnable(boolean enable) {
        this.enable = enable;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enable;
    }

    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

Spring Security为了实现会话并发控制,采用会话信息表来管理用户的会话状态,具体实现如下:

public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {

   protected final Log logger = LogFactory.getLog(SessionRegistryImpl.class);

   // 存放用户以及其对应的所有sessionId的map
   private final ConcurrentMap<Object, Set<String>> principals;

   // 存放sessionId以及其对应的SessionInformation
   private final Map<String, SessionInformation> sessionIds;
   .........................
   ..................
}

principals中:
Object类似UserDetails的实现类
Set里面装的就是sessionId的集合
sessionIds中:
String就是sessionId
SessionInformation

通过查看源码发现,当有登录的时候,会触发spring的事件,然后onApplicationEvent会监听到,principals的compute方法是如果 key 对应的 value 不存在,则返回该 null,如果存在,则返回通过 remappingFunction 重新计算后的值。问题就出在在,因为hashMap如果存储对象的话,使用对象的引用地址,因为每个用户的地址都不一样,所以虽然我们做了会话限制,但还是可以登录多个用户。

springsecurity清除session springsecurity session管理_安全_02

此时,我们只要重写了User 的hashcode的值就可以把问题解决

@Override
public boolean equals(Object obj) {
    return obj instanceof User ? this.username.equals(((User) obj).username) : false;
}

@Override
public int hashCode() {
    return this.username.hashCode();
}