说实话,我一开始真没把“自定义 SpringBoot Starter”当回事,以为不过是整一套自动配置嘛,Spring 本身就挺善于封装的。但真到实际落地的时候,才发现其中的门道远比想象中多。尤其当你要做一个全局的加解密组件,不光是“能用”,还得“好用”、“能复用”、“能扩展”,这就开始考验功底了。
为啥要搞个 Starter?
场景很典型,早年我在对接一个金融系统的项目,接口评审的时候人家一句话把我们打回来了:“数据传输必须加密,而且要支持 SM2。”
行,那就加密呗,咱们写个工具类搞定。第一个接口搞完,领导夸了;第二个接口上线,PM也很满意;第三个……等等,怎么每次都要复制那一堆工具代码?改一处出 bug 的概率高得惊人不说,团队协作的时候每个人写法还不一样,审代码都快审瞎了。
说实话,那个时候我脑子里冒出的第一个想法就是:要是能一行配置,像整合 Redis、JWT 那样,直接用就好了。
然后,我就开始了“自定义 Starter”这条路,一发不可收拾。
一步一步做个 Starter 出来
Starter 的底层原理其实不复杂,说白了就是自动装配 + 组件注册 + 配置解耦,SpringBoot 自己的核心机制就支持这一套。那我就按这个节奏来搞:
首先,项目结构要清晰,遵循 SpringBoot 的习惯:
encryAdecry-spring-boot-starter
└── java
└── com.xbhog
├── advice
├── annotation
├── handler
├── holder
├── GlobalConfig.java
├── GlobalProperties.java
└── resources
└── META-INF
└── spring.factories
这里面的关键点是spring.factories,这玩意是 SpringBoot Starter 的“身份证”,告诉 SpringBoot:“我这儿有个自动配置类,记得加载我。”
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xbhog.GlobalConfig
然后我们来看看这个配置类里干了啥:
@Configuration
@ComponentScan("com.xbhog")
@EnableConfigurationProperties(GlobalProperties.class)
public class GlobalConfig {
@Bean
public SecurityHandler encryAdecryImpl(GlobalProperties properties) {
return new EncryAdecryImpl(properties);
}
}
这么一来,整个包下的组件就能被自动扫描和注入了,同时配合@ConfigurationProperties把外部配置文件参数也读进来了,比如:
encryption:
type: SM2
key: 123456
有了这些,组件的灵活性就大了。
拦截点在哪儿?加解密是怎么接入进来的?
这部分我当时也卡了挺久,因为加解密不能只靠控制器显式调用工具类,太烦了,而且不优雅。最优解是什么?就是你写接口像平时那样写,Starter 来负责“偷天换日”。
我选的是RequestBodyAdvice和ResponseBodyAdvice,这两个接口可以在 Spring 处理请求和响应体之前“插个手”进去,把数据“拦”下来做一波操作。
拦截请求体进行解密:
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
SecuritySupport securitySupport = parameter.getMethodAnnotation(SecuritySupport.class);
ContextHolder.setCryptHolder(securitySupport.securityHandler());
String original = IOUtils.toString(inputMessage.getBody(), Charset.defaultCharset());
String handler = securitySupport.securityHandler();
SecurityHandler securityHandler = SpringContextHolder.getBean(handler, SecurityHandler.class);
String plainText = securityHandler.decrypt(original);
return new MappingJacksonInputMessage(IOUtils.toInputStream(plainText, Charset.defaultCharset()), inputMessage.getHeaders());
}
拦截响应体进行加密:
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
String cryptHandler = ContextHolder.getCryptHandler();
SecurityHandler securityHandler = SpringContextHolder.getBean(cryptHandler, SecurityHandler.class);
return securityHandler.encrypt(body.toString());
}
你看,这逻辑是不是就很清楚了:你只管写@PostMapping的业务代码,数据交给它加解密,不改动 Controller,业务零侵入。
不过,有个前提:你必须是 POST 请求,并且参数在 body 中,GET 请求是不管用的——这是这套机制的一个小缺点,但一般传敏感数据本来就不应该用 GET,对吧。
如何实现“扩展能力”?加密方式不想被写死咋办?
一开始我用的是 Hutool 提供的SM2加密工具,确实方便,但后来看到了它的 bug(每次初始化生成新的密钥对),我就意识到这个组件必须支持自定义扩展。
于是,我定义了一个接口:
public interface SecurityHandler {
String encrypt(String original);
String decrypt(String original);
default void init() {}
}
然后,默认实现类是:
@Component("encryAdecryImpl")
publicclass EncryAdecryImpl implements SecurityHandler {
privatestaticvolatile SM2 sm2;
@Resource
private GlobalProperties globalProperties;
@PostConstruct
public void init() {
KeyPair pair = SecureUtil.generateKeyPair(globalProperties.getAlgorithmType());
byte[] privateKey = pair.getPrivate().getEncoded();
byte[] publicKey = pair.getPublic().getEncoded();
sm2= SmUtil.sm2(privateKey, publicKey);
}
@Override
public String encrypt(String original) {
return sm2.encryptBase64(original, KeyType.PublicKey);
}
@Override
public String decrypt(String original) {
return StrUtil.utf8Str(sm2.decryptStr(original, KeyType.PrivateKey));
}
}
如果你不想用 SM2,可以自己实现SecurityHandler接口,然后在注解里这么写:
@SecuritySupport(securityHandler = "yourCustomHandler")
一行搞定,是不是很丝滑?
怎么测试这个玩意到底好不好使?
整合测试的时候,我是这么搞的:
@Slf4j
@RestController
publicclass BasicController {
@SecuritySupport
@PostMapping("/hello")
public String hello(@RequestBody String name) {
return"Hello " + name;
}
@GetMapping("/configTest")
public String configTest(@RequestParam("name") String name) {
return encryAdecry.encrypt(name);
}
}
启动项目以后,在@PostConstruct里生成一次密钥对,这个时候控制台就能看到类似这样的输出:
生成的公钥:[B@4f3f5b24
生成的私钥:[B@62f59913
然后你直接调用/hello接口,POST 一个加密字符串进去,Starter 会帮你解密;你返回的响应,它也会自动帮你加密。
对业务代码来说,丝毫无感;但对安全传输来说,这一层加密就好比自动上锁上保险,还是军工级别的。
为什么这个 Starter 值得一试?
总结几点我的真实感受吧:
少写重复代码:真正做到“封装一次,到处用”,不再 copy 工具类;
对业务零侵入:不影响业务逻辑,所有处理都在外围进行;
高度可扩展:想换加密算法?自己写个实现就行;
集成超丝滑:一个注解,一个依赖,就能上线。
这个东西说复杂不复杂,说简单也不简单。要做到“让别人用了觉得舒服”,这才是技术的高级玩法。
最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek
领取专属 10元无门槛券
私享最新 技术干货