1.背景
在某次项目的开发中,使用到了Spring Security权限框架进行后端权限开发的权限校验,底层集成Spring Session组件,非常方便的集成Redis进行分布式Session的会话集群部署。系统正式上线后,各个部署节点能够非常方便的进行集群部署,用户的Session会话信息全部保存在Redis中间件库中,开发者不用关心具体的实现,Spring Session组件已经全部集成好了。
但是在系统的用户管理模块中,提供了对系统用户账号的删除功能以及禁用功能,针对这两个功能,需求方给出的具体要求是:
- 删除:当管理员删除当前用户账号时,如果当前账号已经登录系统,则需要剔除下线,并且不可登录
- 禁用:当管理员对当前账号禁用操作时,如果当前账号已经登录系统,则需要剔除下线,并且登录时,提示当前账号已禁用
2.需求分析
从上面的需求来看,不管是删除还是禁用功能,都需要实现,如果当前账号已经登录系统,则需要剔除下线,而禁用操作只需要再登录时给出提示信息即可,这个在业务登录方法中就可以实现,不必从底层框架进行考虑。
因此,从底层技术测进行考虑时,我们需要探索如何在Spring Security权限框架中实现踢人下线的功能。
既然需求已经明确,从功能的实现可能性方面入手,我们则需要从几个方面进行考虑:
- 1)、在Spring Security框架中,用户登录的Session会话信息存储在哪里?
- 2)、在Spring Security框架中,Session会话如何存储,主要存储哪些信息?
- 3)、如何根据账号收集当前该账号登录的所有Session会话信息?
- 4)、如何在服务端主动销毁Session对象?
1)、在Spring Security框架中,用户登录的Session会话信息存储在哪里?
如果我们不考虑分布式Session会话的情况,单体Spring Boot项目中,服务端Session会话肯定存储在内存中,这样的弊端是如果当前应用需要做负载均衡进行部署时,用户请求服务端接口时,会存在Session会话丢失的情况,因为用户登录的会话信息都存在JVM内存中,没有进程之间的共享互通。
为了解决分布式应用Session会话不丢失的问题,Spring Session组件发布了,该组件提供了基于JDBC\Redis等中间件的方式,将用户端的Session会话存储在中间件中,这样分布式应用获取用户会话时,都会从中间件去获取会话Session,这样也保证了服务可以做负载部署以保证Session会话不丢失。本文主要讨论的也是这种情况,集成Redis中间件用来保存用户会话信息。
2)、在Spring Security框架中,Session会话如何存储,主要存储哪些信息?
由于我们使用了Redis中间件,所以,在Spring Security权限框架中产生的Session会话信息,肯定存储与Redis中,这点毫无疑问,那么存储了哪些信息呢?我会在接下来的源码分析中进行介绍
3)、如何根据账号收集当前该账号登录的所有Session会话信息?
我们从上面的需求分析中已经得知Session会话已经存储在Redis中,那么我们是否可以做这样的假设,我们只需要根据Spring Security中在Redis中存储的键值,找到和登录用户名相关的Redis缓存数据,就可以通过调用Security封装的方法进行获取,得到当前登录账号的会话信息呢?这个我们需要在源码中去找到答案
4)、如何在服务端主动销毁Session对象?
如果是单体的Spring Boot应用,Session信息肯定存储在JVM的内存中,服务端要主动销毁Session对象只需要找到Security权限框架如何存储的就可以进行删除。
在分布式的Spring Boot应用中,我们从上面已经得知Session会话信息以及存储在Redis中间件中,那么我们只需要得到当前登录的Session在Redis中的键值,就可以调用方法进行删除操作,从而主动在服务端销毁Session会话
3.源码分析
在上面的需求分析中,我们已经提出了假设,并且根据假设,做出来技术性的判断,接下来我们需要从Spring Security以及Spring Session组件的源码中,去寻找我们需要的答案。
首先,我们在源码分析前,我们需要找到入口,也就是我们在使用Spring Security框架,并且使用Spring Session组件时,我们如何使用的。
在pom.xml
文件中引入组件的依赖是必不可少的,如下:
<!--Spring Security组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Spring针对Redis操作组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--Spring Session集成Redis分布式Session会话-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
接下来,我们在Spring Boot项目中,需要添加@EnableRedisHttpSession
注解,以开启Redis组件对Session会话的支持,该注解我们需要制定Spring Session在Redis中存储的Redis命名空间,已经Session会话的时效性,示例代码如下:
@SpringBootApplication
@EnableRedisHttpSession(redisNamespace = "fish-admin:session",maxInactiveIntervalInSeconds = 7200)
public class FishAdminApplication {
static Logger logger= LoggerFactory.getLogger(FishAdminApplication.class);
public static void main(String[] args) throws UnknownHostException {
ConfigurableApplicationContext application=SpringApplication.run(FishAdminApplication.class, args);
Environment env = application.getEnvironment();
String host= InetAddress.getLocalHost().getHostAddress();
String port=env.getProperty("server.port");
logger.info("\n----------------------------------------------------------\n\t" +
"Application '{}' is running! Access URLs:\n\t" +
"Local: \t\thttp://localhost:{}\n\t" +
"External: \thttp://{}:{}\n\t"+
"Doc: \thttp://{}:{}/doc.html\n\t"+
"----------------------------------------------------------",
env.getProperty("spring.application.name"),
env.getProperty("server.port"),
host,port,
host,port);
}
在上面的代码中,我们指定Redis的命名空间是fish-admin:session
,默认最大失效7200
秒。
如果开发者默认不指定这两个属性的话,命名空间默认值是spring:session
,默认最大时效则是1800
秒
在上面我们已经说过了,既然是看源码,我们需要找到入口,这是看源码最好的方式,我们在使用Spring Session组件时,需要使用@EnableRedisHttpSession
注解,那么该注解就是我们需要重点关注的对象,我们需要搞清楚,该注解的作用是什么?
EnableRedisHttpSession.java
部分源码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
//more property..
}
在该注解中,我们可以看到,最关键的是在该注解之上,使用@Import
注解导入了RedisHttpSessionConfiguration.java
配置类,如果你经常翻看Spring Boot相关的源码,你会敏锐的察觉到,该配置类就是我们最终要找的类
先来看该类的UML图,如下:
该类实现了Spring框架中很多Aware
类型接口,Aware
类型的接口我们都知道,Spring容器在启动创建实体Bean后,会调用Aware
系列的set方法传参赋值
当然,最核心的,我们从源码中可以看到,是Spring Session组件会向Spring容器中注入两个实体Bean
,代码如下:
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository