背景
最近需要实现一个对于系统的授权检测功能,即当SpringBoot应用被启动时,需要当前设备是否具有有效的的授权许可信息,若无则直接退出应用。具体的实现方案请继续看下文。
环境
Ruoyi-Vue SpringBoot3 RuoYi-Vue: 🎉 基于SpringBoot,Spring Security,JWT,Vue & Element 的前后端分离权限管理系统,同时提供了 Vue3 的版本 - Gitee.comhttps://gitee.com/y_project/RuoYi-Vue/tree/springboot3/
初始实现
基于文章开头所提到需求,很容易想到可以在主启动类中编写相关代码来实现,由于我们需要的是在类加载初期校验,并且我们可能需要从配置文件中获取部分信息,因此需要使用如下方法来编写代码。
首先在当前主启动类所在的包下新建utils包,用于校验代码的编写,
新建校验工具类InitCheckUtil,代码如下
@Component
public class InitCheckUtil {
@Value("${init.key}")
private String key;
@PostConstruct
public void check() {
if (StringUtils.isBlank(key) || !"ruoyi".equals(key)) {
System.out.println("系统未授权,请联系管理员");
System.exit(0);
}else{
System.out.println("current key is: " + key + "system is authorized");
}
}
}
不难看出,这个校验工具的逻辑为,首先从配置文件(application.yml)中加载了init.key的值,配文件信息如下:
之后通过check方法来判断获取的key是否为 ruoyi ,若不满足条件则直接退出。执行代码后结果如下:
可以看到已经打印了当前key值,并表明系统已经进行了了授权。需要注意的是,这里的@PostConstruct注解确保了Spring容器启动时就会执行这个方法。
目前来看似乎已经完成了我们的需求,但是如果现在想要在项目启动前就执行呢,而不是等到容器加载时在校验,可以看到输出当前key信息之前已经有其它的日志信息,更为复杂的项目在启动时往往伴随着更多的前置初始化的东西,那么,如何在SpringBoot类启动前就进行校验呢?
ApplicationContextInitializer
上一小节我们已经实现了一个初步版本,而为了能够满足新的启动之前的即校验需求,我们需要做进一步的改动。首先可以想到的便是在主启动类中进行加载,直接在启动方法之前,使用创建InitCheckUtil对象的方式,然后调用其check方法。
如果此时直接启动项目,将会出现如下效果:
看起来似乎实现了启动前的校验,但是我们并没有更改配置文件中的key值,正常情况下服务应该是启动成功的状态。但目前状况的原因是由于我们现在的代码先于Spring容器执行,因此@Value注解将无法正常读取到key值,进而校验失败,因此InitCheckUtil类的代码变为:
public class InitCheckUtil {
private String key;
public InitCheckUtil(){}
public InitCheckUtil(String key){
this.key = key;
}
public void check() {
if (StringUtils.isBlank(key) || !"ruoyi".equals(key)) {
System.out.println("系统未授权,请联系管理员");
System.exit(0);
}else{
System.out.println("current key is: " + key + ", system is authorized");
}
}
}
但由于没有了Spring容器的能力的加持,此时对于配置文件读取就无法直接使用@Value注解来获取了,因此我们现在使用使用一种新的方式来实现对配置文件的读取,即通过ApplicationContextInitializer 接口,它的作用在于当容器启动之前就可以读取配置文件中的值,从而满足我们当前的需求,具体实现如下,新建CheckConfigInitlnitializer类并实现 ApplicationContextInitializer 接口的 initialize 方法,在该方法中通过applicationContext获取ConfigurableEnvironment 对象,进而获取配置文件中的信息后传入校验类中实现校验:
public class CheckConfigInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
String initKey = environment.getProperty("init.key");
InitCheckUtil checkUtil = new InitCheckUtil(initKey);
checkUtil.check();
}
}
由于我们更改了配置文件的获取方式,因此主启动类的中内容也需要做相关的更改,如下所示增加了对于初始化器的注册,从而可以确保可以在整个容器启动之前完成校验。
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class RuoYiApplication
{
public static void main(String[] args)
{
SpringApplication application = new SpringApplication(RuoYiApplication.class);
application.addInitializers(new CheckConfigInitializer());
application.run( args);
System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
}
}
此时系统可正常启动,并输出相应的校验信息如下
自动装配
在上一步中我们已经基本实现了对于文章开头提出需求问题的解决,但是若后续存在类似需求时需要我们不断往主主启动类中添加代码。这种方法看起来极为的不优雅,整个启动类会变得愈发臃肿。因此我们可以通过SpringBoot中的自动装配机制对上面的代码进行进一步的调整优化。
其实目前优化的方式严格意义上时上一步的另一种实现方式,这里将通过使用spring.factories文件来代替在主启动类中显式的注册初始化类。通过在项目当前模块的resources目录下新建META-INF 文件夹(不存在则新建),并新增 spring.factories 文件,该文件的内容将在Spring容器启动前进行扫描并加载,若为了实现通用性可将其迁移至common模块中,此处仅做展示用,方便起见未进行迁移。
在spring.factories 文件中添加如下内容,其中 key 为 org.springframework.context.ApplicationContextInitialize 固定值,value 为我们自定义的实现的全包名。
org.springframework.context.ApplicationContextInitializer=com.ruoyi.utils.CheckConfigInitializer
这个时候就可以将启动类中的代码还原为原始的状态,再次启动程序后发现系统可以正常运行,其原理为,当SpringApplication初始化时通过SpringFactoriesLoader获取到配置在 META-INF/spring.factories 文件中的 ApplicationContextInitializer 的所有实现类,进而加载到容器中,而在这个过程中将实现对了系统启动的初步校验。