这篇文章用到了SpringBoot AOP,如果还不熟悉的读者可以先看一下这篇文章,下面附上传送门:
https://blog.csdn.net/m0_74229735/article/details/134792386
什么是spring-boot-starter组件?
spring-boot-starter
是 Spring Boot 框架中的一种特殊的依赖项,它们旨在简化 Spring Boot 应用程序的构建和配置过程。Spring Boot 提供了许多预定义的 starter 组件,每个 starter 都包含了一组特定功能的依赖项。当你将某个 starter 添加到应用程序的依赖项中时,它会自动引入所需的库,并配置好默认值,从而为特定类型的应用程序功能提供了一种快速启动的方式。
例如,如果你想构建一个 web 应用程序,只需将 spring-boot-starter-web
starter 添加到项目的依赖项中,Spring Boot 就会自动配置并引入所有构建 web 应用程序所需的依赖项,包括嵌入式的 Tomcat 服务器、Spring MVC 等。这样,你就可以专注于编写应用程序的业务逻辑,而不必手动配置这些依赖项。
Spring Boot 提供了各种各样的 starter,涵盖了开发中常见的方方面面,比如数据库访问、安全认证、消息队列、测试等。每个 starter 都有一个明确的命名规则,以 spring-boot-starter-
作为前缀,后面跟着标识特定功能的名称,如 spring-boot-starter-data-jpa
用于数据访问、spring-boot-starter-security
用于安全认证等。
总之,spring-boot-starter
允许开发者通过简单地添加依赖项来快速启动和配置 Spring Boot 应用程序的各种功能,极大地简化了应用程序的构建和配置过程。
同时开发者也可以自行开发自己的 Spring Boot Starter。Spring Boot 提供了一套机制,允许开发者将一组特定功能所需的依赖项和自定义配置打包成一个 starter,并在其他项目中进行复用。
下面是开发者手动开发spring-boot-starter组件的一般结构
starter会把所有用到的依赖包都包含进来,避免开发者自己去引入依赖所带来的麻烦。
虽然不同的starter实现起来各有差异,但是他们基本上都会使用到两个相同的内容:ConfigurationProperties
和AutoConfiguration
。
什么是中间件的魅力?
重复逻辑的提炼、底层功能的封装、系统架构的升级,都是中间件所能触达到的场景。
在抛开 CRUD 推进到中间件的世界后,你会感受到用代码来写代码,用更底层的设计从根上解决更上层的问题。我们能深入到,动态代理、反射调用、Bean注册以及字节码插桩对方法增强,这些技术的运用既可以优化服务端开发过程,也可以在无声无息中采集和监控代码质量,还可以让低代码编程和可持续性交付上线得以实现。
终究,这些技术的使用都在为研发交付效率和交付质量做保障,而学会它们才能让自己的更有技术价值。
什么是黑白名单?
黑白名单是一种常见的访问控制机制,用于限制或允许特定的实体(如IP地址、域名、用户等)对某个资源或服务的访问。
黑名单(Blacklist):指明不允许访问的实体列表。当某个实体出现在黑名单中时,其访问请求将被拒绝或限制。黑名单通常用于阻止恶意实体、垃圾邮件发送者、尝试攻击的 IP 地址等。
白名单(Whitelist):指明允许访问的实体列表。只有出现在白名单中的实体才能够正常访问资源或服务,其他未列入白名单的实体将被拒绝访问。白名单通常用于限制对敏感数据、重要系统的访问,提高系统的安全性。
使用黑白名单的目的是控制对资源或服务的访问权限,以确保只有授权的实体能够进行访问,从而增强安全性、减少风险。黑白名单可以在各种场景中使用,例如网络安全、应用程序访问控制、防火墙配置等。
下面将主要对白名单控制接口的访问进行实现
在代码的接口层实现白名单控制是一种常见的安全机制,用于限制对系统接口的访问权限。通过白名单控制,只有在事先定义的白名单中列出的IP地址、用户或其他识别标识的请求才能被接受和处理,而其他未在白名单中的请求将被拒绝。
在互联网这种多数面向C端用户场景下的产品功能研发完成交付后,通常并不会直接发布上线。尤其是在一个原有服务功能已经沉淀了大量用户时,不断的迭代开发新增需求下,更不会贸然发布上线。
虽然在测试环境、预发环境都有了相应功能的验证,但在真实的用户场景下可能还会存在其他隐患问题。那么为了更好的控制系统风险,通常需要研发人员在代码的接口层,提供白名单控制。上线初期先提供可配置的白名单用户进行访问验证,控制整体的交付风险程度。
白名单确实可以解决接口功能或者服务入口的访问范围风险,那么这里有一个技术方案实现问题。就是如果研发人员在所有的接口上都加这样的白名单功能,那么就会非常耗费精力,同时在功能不再需要时可能还需要将代码删除。在这个大量添加和修改重复功能的代码过程中,也在一定程度上造成了研发成本和操作风险。所以站在整体的系统建设角度来说,我们需要有一个通用的白名单服务系统,减少研发在这方面的重复开发。
白名单控制还有许多应用场景,例如:
需要注意的是,白名单控制只是安全机制中的一部分,仅仅依靠白名单可能不足以提供完全的安全性。在实际应用中,还需要结合其他安全措施,如认证、授权、加密等来构建更可靠的系统安全。
这里就不详细讲了,后面会有文章详细说明
由于SpringBoot官方本身就提供了很多Starter,为了区别那些是官方的,哪些是第三方的,所以SpringBoot官方提出:
第三方提供的Starter统一用xxx-spring-boot-starter
而官方提供的Starter统一用spring-boot-starter-xxx。
白名单服务属于业务系统开发过程中可重复使用的通用功能,所以我们可以把这样的工具型功能单独提炼出来设计成技术组件,由各个需要的使用此功能的系统工程引入使用。整体的设计方案如图 白名单中间件框架设计。
- 使用自定义注解、切面技术和SpringBoot对于配置文件的处理方式,开发白名单中间件。
- 在中间件中通过提取指定字段的入参与配置文件白名单用户列表做比对确认是否允许访问。
- 最后把开发好的中间件引入到需要依赖白名单服务的系统,在SpringBoot启动时进行加载。
白名单控制中间件整个实现工程并不复杂,其核心点在于对切面的理解和运用,以及一些配置项需要按照 SpringBoot 中的实现方式进行开发。
那么,接下来我们就看一下每个类的实现过程和内容讲解。
package com.kjz.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author CSDN编程小猹
* @data 2023/12/06
* @description 切入点注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WhiteList {
//接口入参提取字段属性名称
String key() default "";
//拦截返回信息
String returnJson() default "";
}
代码详解
我们先来看一下注解@Retention的源码
然后再进入到RetentionPolicy.RUNTIME中查看源码
RetentionPolicy.RUNTIME 在它的注释中有这样一段描述:Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively. 其实说的就是加了这个注解,它的信息会被带到JVM运行时,当你在调用方法时可以通过反射拿到注解信息。除此之外,RetentionPolicy 还有两个属性 SOURCE、CLASS,其实这三个枚举正式对应了Java代码的加载和运行顺序,Java源码文件 -> .class文件 -> 内存字节码。并且后者范围大于前者,所以一般情况下只需要使用 RetentionPolicy.RUNTIME 即可。
查看一下@Target的源码
@Target 是元注解起到标记作用,它的注解名称就是它的含义,目标,也就是我们这个自定义注解 WhiteList 要放在类、接口还是方法上。@Target(ElementType.METHOD) 表示该注解可以用于修饰方法。在 JDK1.8 中 ElementType 一共提供了10中目标枚举。
常见的取值包括:
ElementType.TYPE:可以用于类、接口、枚举声明。
ElementType.FIELD:可以用于字段声明(包括枚举常量)。
ElementType.METHOD:可以用于方法声明。
ElementType.PARAMETER:可以用于参数声明。
ElementType.CONSTRUCTOR:可以用于构造函数声明。
ElementType.LOCAL_VARIABLE:可以用于局部变量声明。
可以参考自己的自定义注解作用域进行设置
自定义注解 @WhiteList 中有两个属性 key、returnJson。key 的作用是配置当前接口入参需要提取的属性,returnJson 的作用是在我们拦截到用户请求后需要给一个返回信息。
package com.kjz.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author CSDN编程小猹
* @data 2023/12/06
* @description 读取配置文件生成黑白名单配置类
*/
@ConfigurationProperties("kjz.whitelist")
public class WhiteListProperties {
private String users;
public String getUsers() {
return users;
}
public void setUsers(String users) {
this.users = users;
}
}
@ConfigurationProperties,用于创建指定前缀( prefix = "kjz.whitelist" )的自定义配置信息,这样就在 yml 或者 properties 中读取到我们自己设定的配置信息。
package com.kjz.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author CSDN编程小猹
* @data 2023/12/06
* @description 将黑白名单配置类注册为Bean
*/
@Configuration
@ConditionalOnClass(WhiteListProperties.class)
@EnableConfigurationProperties(WhiteListProperties.class)
public class WhiteListAutoConfigure {
@Bean("whiteListConfig")
@ConditionalOnMissingBean
public String whiteListConfig(WhiteListProperties properties) {
return properties.getUsers();
}
}
@Configuration,可以算作是一个组件注解,在 SpringBoot 启动时可以进行加载创建出 Bean 文件。因为 @Configuration 注解有一个 @Component 注解,@Configuration源码如下
@ConditionalOnClass(WhiteListProperties.class),Spring Boot 中的条件注解,@ConditionalOnClass(WhiteListProperties.class)
表示在指定类WhiteListProperties位于当前类路径上,才会实例化一个类。
除此之外还有其他属于此系列的常用的注解。
@ConditionalOnBean 仅仅在当前上下文中存在某个对象时,才会实例化一个 Bean
@ConditionalOnClass 某个 CLASS 位于类路径上,才会实例化一个 Bean
@ConditionalOnExpression 当表达式为 true 的时候,才会实例化一个 Bean
@ConditionalOnMissingBean 仅仅在当前上下文中不存在某个对象时,才会实例化一个 Bean
@ConditionalOnMissingClass 某个 CLASS 类路径上不存在的时候,才会实例化一个 Bean
@Bean,在 whiteListConfig 方法上我们添加了这个注解以及方法入参 WhiteListProperties properties。这里面包括如下几个内容:
properties 配置会被注入进来,当然你也可以选择使用 @Autowired 的方式配置注入在使用属性。
整个方法会在配置信息和Bean注册完成后,开始被实例化加载到 Spring 中。
@ConditionalOnMissingBean,现在就用到了这个方法上,代表只会实例化一个 Bean 对象。
创建spring.factories
package com.kjz.aspect;
import com.alibaba.fastjson.JSON;
import com.kjz.annotation.WhiteList;
import org.apache.commons.beanutils.BeanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
* @author CSDN编程小猹
* @data 2023/12/06
* @description 切面类
*/
@Aspect
@Component
public class JoinPoint {
private Logger logger = LoggerFactory.getLogger(JoinPoint.class);
@Resource
private String whiteListConfig;
//切入点表达式
@Pointcut("@annotation(com.kjz.annotation.WhiteList)")
public void aopPoint() {
}
@Around("aopPoint()")
public Object check(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//从连接点获取控制方法上的注解
Method method= getMethod(proceedingJoinPoint);
WhiteList annotation = method.getAnnotation(WhiteList.class);
//解析注解属性,获取入参信息
String keyValue= getFileValue(annotation.key(),proceedingJoinPoint.getArgs());
logger.info("WhiteList handler method: {} value: {}",method.getName(),keyValue);
//如果属性为空直接放行
if (null==keyValue||"".equals(keyValue)) return proceedingJoinPoint.proceed();
String[] split = whiteListConfig.split(",");
//白名单过滤
for (String s : split) {
if (keyValue.equals(s));
return proceedingJoinPoint.proceed();
}
//拦截
return returnJson(annotation,method);
}
// 返回对象
private Object returnJson(WhiteList annotation, Method method) throws InstantiationException, IllegalAccessException {
Class> returnType = method.getReturnType();
String returnJson = annotation.returnJson();
if ("".equals(returnJson)) {
return returnType.newInstance();
}
return JSON.parseObject(returnJson, returnType);
}
//获取属性值
private String getFileValue(String key, Object[] args) {
String filedValue = null;
for (Object arg : args) {
try {
if (null == filedValue || "".equals(filedValue)) {
filedValue = BeanUtils.getProperty(arg, key);
} else {
break;
}
} catch (Exception e) {
if (args.length == 1) {
return args[0].toString();
}
}
}
return filedValue;
}
//获取控制方法
private Method getMethod(ProceedingJoinPoint proceedingJoinPoint) throws NoSuchMethodException {
Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature= (MethodSignature)signature;
return proceedingJoinPoint.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
}
所以这部分代码比较多,但整体的逻辑实现并不复杂,主要包括如下内容:
- 使用注解 @Aspect,定义切面类。这是一个非常常用的切面定义方式。
- @Component 注解,将类生成为 Bean 对象。虽然其他的注解也可以注册 Bean 对象,但 @Component 具有组件含义,符合 Spring 设计的定义。如果你担心这个切面类在使用过程中有重名,那么还可以在 @Component 注解中指定 Bean 的名字
- @Pointcut("@annotation(com.kjz.annotation.WhiteList)"),定义切点。在 Pointcut 中提供了很多的切点寻找方式,有指定方法名称的、有范围筛选表达式的,也有我们现在通过自定义注解方式的。一般在中间件开发中,自定义注解方式使用的比较多,因为它可以更加灵活的运用到各个业务系统中。
- @Around("aopPoint()"),可以理解为是对方法增强的织入动作,有了这个注解的效果就是在你调用已经加了自定义注解 @DoWhiteList 的方法时,会先进入到此切点增强的方法。那么这个时候就你可以做一些对方法的操作动作了,比如我们实现的白名单用户拦截还是放行。
- 在 check 中拦截方法后,获取方法上的自定义注解。getMethod(jp),其实只要获取到方法,就可以通过方法在拿到注解信息,这部分可以参照源码内容。另外获取注解的手段还有其他方式,会在后文中展示出来
- 最后就是对当前拦截方法校验结果的操作,是拦截还是放行。其实拦截就是返回我们在自定义注解配置的 JSON 信息生成对象返回,放行则是调用proceedingJoinPoint.proceed(); 方法,让整个代码块向下继续执行。
运行mvn install
命令,将这个项目打成jar包部署到本地仓库,提供给另一个服务调用。
当另一个springboot项目要调用时只需要在poom.xml文件中引用
com.kjz
Whitelist-spring-boot-starter
1.0.0-SNAPSHOT
测试工程主要包括启动类 ApiTestApplication、Api接口类 UserController、配置信息 application.yml,这三面内容。
另外在工程 Maven 配置中会引入到我们开发好的白名单中间件服务,如下:
com.kjz
Whitelist-spring-boot-starter
1.0.0-SNAPSHOT
server:
port: 8081
# 白名单用户
kjz:
whitelist:
users: aaa,111,kjz
- 以上的这种配置方式只是基于本地的配置,并没有服务端注册中心那种方式。
- 通过这样的配置方式可以让学习中间件开发的研发人员更好的看到最核心的实现,也能让大家更加方便测试。如果你已经学会了,那么可以尝试在中间件中添加一种注册中心来配置白名单,这样就可以在不重启服务的情况下动态变化白名单列表了。
- users,白名单用户ID是逗号相隔的,这里的配置用户ID都可以正常访问系统。
/**
* 通过:http://localhost:8081/api/queryUserInfo?userId=aaa
* 拦截:http://localhost:8081/api/queryUserInfo?userId=123
*/
@WhiteList(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
logger.info("查询用户信息,userId:{}", userId);
return new UserInfo("匡匡:" + userId, 19, "厂里");
}
@DoWhiteList 自定义注解配置在方法上,两个参数信息一个配置 userId,另外一个配置方法拦截后的返回信息 {\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}
接口:http://localhost:8081/api/queryUserInfo?userId=aaa
{
"code": "0000",
"info": "success",
"name": "匡匡:aaa",
"age": 19,
"address": "厂里"
}
接口:http://localhost:8081/api/queryUserInfo?userId=123
{
"code": "1111",
"info": "非白名单可访问用户拦截!",
"name": null,
"age": null,
"address": null
}
服务端日志
2023-12-06 22:38:18.931 INFO 19653 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 7 ms
2023-12-06 22:38:18.984 INFO 19653 --- [nio-8081-exec-1] c.b.whitelist.DoJoinPoint : whitelist handler method:queryUserInfo value:123
2023-12-06 22:38:29.555 INFO 19653 --- [nio-8081-exec-2] c.b.whitelist.DoJoinPoint : whitelist handler method:queryUserInfo value:aaa
2023-12-06 22:38:29.563 INFO 19653 --- [nio-8081-exec-2] c.b.m.w.test.interfaces.UserController : 查询用户信息,userId:aaa
2023-12-06 22:52:24.510 INFO 19653 --- [nio-8081-exec-4] c.b.whitelist.JoinPoint : middleware whitelist handler method:queryUserInfo value:123
2023-12-06 22:54:25.852 INFO 19653 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
通过测试结果我们可以看到,白名单中间件已经起到了拦截作用。通过不同用户ID的访问,返回是否拦截的结果信息。