目录
一、理解会话
二、防御会话固定攻击
三、会话过期
四、会话并发控制
五、集群会话的缺陷
六、集群会话的解决方案
七、整合Spring Session解决集群会话问题
只需在两个浏览器中用同一个账号登录就会发现,系统并没有任何会话并发限制。一个账号可以多处同时登陆并不是一个好的策略。Spring Security已经为我们提供了完善的会话管理功能,包括会话固定攻击、会话超时检测以及会话并发控制。
会话(session)就是无状态的HTTP实现用户状态可维持的一种解决方案。HTTP本身的无状态使得用户在与服务器的交互过程中,每个请求之间都没有关联性。这就意味着用户的访问没有身份记录,站点也无法为用户提供个性化的服务。session的诞生解决了这个难题。
服务器通过与用户约定每个请求都携带一个id类的信息,从而让不同的请求之间有了关联。当用户首次访问系统时,系统会为该用户生成一个sessionId,并添加到cookie中。在该用户的会话期内,每个请求都自动携带该cookie,因此系统可以很轻易识别出是来自哪个用户的请求。
会话固定攻击:
黑客只需访问一次系统,将系统生成的sessionId提取并拼凑在URL上,然后将该URL发给一些取得信任的用户。只要用户在session有效期内通过此URL进行登录,该sessionId就会绑定用户的身份,黑客便可以轻松享有同样的会话状态,完全不需要用户名和密码。这就是会话固定攻击。
防御会话固定攻击的方法非常简单,只需在用户登录之前重新生成新的session即可,在继承WebSecurityConfigureAdapter 时,Spring Security 已经启用了该配置。
sessionManagement是一个会话管理的配置器,其中,防御会话固定攻击的策略有四种:
默认启用的是migrateSession策略,可以自己根据需要修改,如:
除防御会话固定攻击外,还可以通过Spring Security配置一些会话过期策略,例如过期时跳转到某个URL:
也可以完全自定义过期策略:
public class MyInvalidSessionStrategy implements InvalidSessionStrategy {
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("session无效,已过期!");
}
}
设置单个用户允许同时在线的最大会话数,如果没有额外的配置,新登录的会话会踢掉旧的会话。
查看效果:
在另一个浏览器登录之后,之前的浏览器会提示出这条信息。改成 maximumSession(2) 时候,两个浏览器可以同时登录。
大多数集群部署会采用类似下图的网络结构:
在这种网络结构下,用户的请求首先会到LB(负载均衡)服务器上,LB服务器再根据负载均衡策略将这些请求转发至后面的服务,以达到请求分散的目的。正常情况下,在集群中,同个用户的请求可能会分发到不同的服务器上,加入登录操作是在Server1上完成的,Server1缓存了用户的登录状态,但Server2和Server0并不知情,如果用户的后续操作被分配到了Server2或Server0上,这时就会要求该用户重新登录,这就是典型的会话状态集群不同步问题。
集群会话的常见方案有三种:
(1)添加依赖
org.springframework.session
spring-session-core
org.springframework.session
spring-session-data-redis
2.4.2
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
3.6.0
(2)配置Spring Session
为Spring Security提供集群支持的会话注册表
@Configuration
@EnableRedisHttpSession
public class HttpSessionConfig {
// 提供redis连接,localhost:6379
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new JedisConnectionFactory();
}
@Bean
public SpringSessionBackedSessionRegistry springSessionBackedSessionRegistry(
FindByIndexNameSessionRepository sessionRepository) {
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
在该类中,使用了@Autowired注解将RedisConnectionFactory对象注入进来。RedisConnectionFactory对象用于创建一个RedisTemplate对象,用于操作Redis存储session的数据。
@Bean注解被用于定义了三个Bean:
sessionRedisOperations方法返回一个RedisTemplate对象,用于操作Redis存储session的数据。
sessionRepository方法返回一个FindByIndexNameSessionRepository对象,这是Spring Session提供的一个实现类,用于管理HttpSession。
sessionRepository方法还返回了一个MapSessionRepository对象,这个对象用于在没有Redis环境的情况下,将Session存储在内存中。
在这个类中,配置了Redis存储session和使用内存存储session的两种实现,可以根据需要选择相应的实现。
需要注意的是,如果想使用Redis存储session,就必须保证已经正确配置了Redis环境,包括Redis服务器的地址和端口等信息。
(3)将新的会话注册表提供给Spring Security
(4)在不同的端口上启动服务测试
默认的服务是启动在8080端口的,这里我们在8081端口上再启动一个:
两个端口都启动后,先访问8080端口,再访问8081端口:
这时候再刷新8080端口的页面,可以看到已经被8081上的用户给挤掉了:
在redis中也可以看到存储的session信息: