自定义spring-boot-starter(请求解密,响应加密)
0、什么是starter
Spring Starter是一种依赖注入框架,它可以帮助我们更快地启动和构建Spring应用程序。它包含了一个预定义的Spring配置,通常包含常用的bean定义,例如数据源、日志、安全认证等。他的优点包括:
1.1 快速集成:Spring Boot Starter 让开发者在创建新的应用程序时可以快速集成 Spring 和其他常用框架组件,这样就不必花费大量时间去研究如何集成。
1.2 简化配置:Spring Boot Starter 内置了许多常用的配置,可以帮助开发人员快速构建 Spring 应用程序,同时也让配置变得更加简单易懂。
1.3 良好的默认设置:Spring Boot Starter 遵循“约定优于配置”的原则,它有很多良好的默认设置和推荐实践,这些设置可以大大减少开发者的工作量。
1.4 模块化设计:Spring Boot Starter 是基于模块化设计的,每个 Starter 都是独立的、可以重用的组件,通过简单的配置就可以将这些组件集成到应用程序中。
1.5 提高生产效率:通过使用 Spring Boot Starter,开发者可以快速创建和部署 Spring 应用程序,从而提高生产效率。在较短的时间内创建具有可扩展性和可维护性的应用程序。
总之,Spring Boot Starter 的优点在于能够快速集成、简化配置、良好的默认设置、模块化设计以及提高生产效率等。这些优点使得Spring Boot在现代软件开发中变得越来越受欢迎。
日常开发中,经常会有独立于业务之外的配置模块,可能多个项目公用该配置,例如内容审核,短信登录等,如果在每个项目中多次引用太过麻烦。因此我们可以将配置模块封装为starter,需要时在模块pom中添加该依赖,方便复用。
1、效果
2、准备-自定义starter
2.1、目录
2.2、引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wangzhan</groupId>
<artifactId>encrypt-decrypt-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>encrypt-decrypt-spring-boot-starter</name>
<description>encrypt-decrypt-spring-boot-starter</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 自定义starter,实现自动化配置的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.wangzhan.EncryptDecryptSpringBootStarterApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2.3、加解密注解
package com.wangzhan.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.PARAMETER})
public @interface Decrypt {
}
package com.wangzhan.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
}
2.4、请求响应拦截处理
package com.wangzhan.advice;
import com.wangzhan.annotation.Decrypt;
import com.wangzhan.config.KeyService;
import com.wangzhan.util.AESUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
@ConditionalOnClass(KeyService.class)
@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {
@Autowired
private KeyService keyService;
/**
* 该方法用于判断当前请求,是否要执行beforeBodyRead方法
*
* @param methodParameter handler方法的参数对象
* @param targetType handler方法的参数类型
* @param converterType 将会使用到的Http消息转换器类类型
* @return 返回true则会执行beforeBodyRead
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
}
/**
* 在Http消息转换器执转换,之前执行
* @param inputMessage
* @param parameter
* @param targetType
* @param converterType
* @return 返回 一个自定义的HttpInputMessage
* @throws IOException
*/
@Override
public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
byte[] body = new byte[inputMessage.getBody().available()];
inputMessage.getBody().read(body);
try {
byte[] keyBytes = keyService.getMd5Key().getBytes();
byte[] decrypt = AESUtils.decrypt(body, keyBytes);
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decrypt);
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return byteArrayInputStream;
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
} catch (Exception e) {
e.printStackTrace();
}
return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
}
}
package com.wangzhan.advice;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wangzhan.annotation.Encrypt;
import com.wangzhan.config.KeyService;
import com.wangzhan.util.AESUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ConditionalOnClass(KeyService.class)
@ControllerAdvice
public class EncryptResponse implements ResponseBodyAdvice<Object> {
private ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private KeyService keyService;
/**
* 该方法用于判断当前请求的返回值,是否要执行beforeBodyWrite方法
*
* @param methodParameter handler方法的参数对象
* @param converterType 将会使用到的Http消息转换器类类型
* @return 返回true则会执行beforeBodyWrite
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Encrypt.class);
}
/**
* 在Http消息转换器执转换,之前执行
* @param body
* @param returnType
* @param selectedContentType
* @param selectedConverterType
* @param request
* @param response
* @return 返回 一个自定义的HttpInputMessage,可以为null,表示没有任何响应
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
byte[] keyBytes = keyService.getMd5Key().getBytes();
try {
return AESUtils.encrypt(objectMapper.writeValueAsBytes(body), keyBytes);
} catch (Exception e) {
e.printStackTrace();
}
return body;
}
}
2.5、设置可配置的参数
package com.wangzhan.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.encrypt")
public class KeyProperties {
private final static String DEFAULT_KEY = "1234567890abcdef";
private String key = DEFAULT_KEY;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
2.6、针对密钥对其进行加密
package com.wangzhan.config;
/**
* @author wangzhan
* @version 1.0
* @description 密钥的加密类
* @date 2024/5/31 10:14:12
*/
public class KeyService {
private final String md5Key;
public KeyService(String md5Key) {
this.md5Key = md5Key;
}
public String getMd5Key() {
return md5Key;
}
}
package com.wangzhan.config;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.DigestUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author wangzhan
* @version 1.0
* @description key自动配置类
* @date 2024/5/31 10:18:26
*/
// 当存在某个类时,此自动配置类才会生效
@ConditionalOnClass(KeyProperties.class)
// 导入我们自定义的配置类,供当前类使用(相当于把bean放到IOC容器,供全局DI)
@EnableConfigurationProperties(KeyProperties.class)
@Configuration
public class KeyAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(KeyAutoConfiguration.class);
@Autowired
private KeyProperties keyProperties;
@Bean
public KeyService keyService() {
String str = DigestUtil.md5Hex(keyProperties.getKey());
logger.info("加密前key:{},加密后的key:{}", keyProperties.getKey(), str);
// md5加密密钥
return new KeyService(str);
}
}
2.7、AES加解密工具类
package com.wangzhan.util;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AESUtils {
private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* 获取 cipher
* @param key
* @param model
* @return
* @throws Exception
*/
private static Cipher getCipher(byte[] key, int model) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(model, secretKeySpec);
return cipher;
}
/**
* AES加密
* @param data
* @param key
* @return
* @throws Exception
*/
public static String encrypt(byte[] data, byte[] key) throws Exception {
Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
return Base64.getUrlEncoder().withoutPadding().encodeToString(cipher.doFinal(data));
}
/**
* AES解密
* @param data
* @param key
* @return
* @throws Exception
*/
public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
return cipher.doFinal(Base64.getUrlDecoder().decode(data));
}
}
2.8、配置 /META-INF/spring.factories 自动加载
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.wangzhan.config.KeyAutoConfiguration
2.9、配置 /META-INF/spring-configuration-metadata.json 为配置添加提示内容
{
"groups": [],
"hints": [],
"properties": [
{
"sourceType": "com.wangzhan.config.KeyProperties",
"name": "spring.encrypt.key",
"type": "java.lang.String",
"description": "密钥"
}
]
}
2.10、生成jar
2.11、扩展
@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean。
属性映射注解
- @ConfigurationProperties :配置文件属性值和实体类的映射
- @EnableConfigurationProperties:和@ConfigurationProperties配合使用,把@ConfigurationProperties修饰的类加入ioc容器。
配置bean注解
- @Configuration :标识该类为配置类,并把该类注入ioc容器
- @Bean :一般在方法上使用,声明一个bean,bean名称默认是方法名称,类型为返回值。
条件注解
- prefix :配置属性名称的前缀
- value :数组,获取对应property名称的值,与name不可同时使用
- name :数组,可与prefix组合使用,组成完整的配置属性名称,与value不可同时使用
- havingValue :比较获取到的属性值与havingValue给定的值是否相同,相同才加载配置
- matchIfMissing :缺少该配置属性时是否可以加载。如果为true,没有该配置属性时也会正常加载;反之则不会生效
- @Conditional:是根据条件类创建特定的Bean,条件类需要实现Condition接口,并重写matches接口来构造判断条件。
- @ConditionalOnBean :容器中存在指定bean,才会实例化一个Bean
- @ConditionalOnMissingBean:容器中不存在指定bean,才会实例化一个Bean
- @ConditionalOnClass:系统中有指定类,才会实例化一个Bean
- @ConditionalOnMissingClass:系统中没有指定类,才会实例化一个Bean
- @ConditionalOnExpression:当SpEl表达式为true的时候,才会实例化一个Bean
- @AutoConfigureAfter :在某个bean完成自动配置后实例化这个bean
- @AutoConfigureBefore :在某个bean完成自动配置前实例化这个bean
- @ConditionalOnJava :系统中版本是否符合要求
- @ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化
- @ConditionalOnResource:类路径下是否存在指定资源文件
- @ConditionalOnWebApplication:是web应用
- @ConditionalOnNotWebApplication:不是web应用
- @ConditionalOnJndi:JNDI指定存在项
- @ConditionalOnProperty:配置Configuration的加载规则
3、使用-自定义starter
3.1、目录
3.2、引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 引入密文解密starter -->
<dependency>
<groupId>com.wangzhan</groupId>
<artifactId>encrypt-decrypt-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
3.3、配置
server.port=8022
# 加密配置
spring.encrypt.key=wangzhan
3.4、实体类
package com.wangzhan.domain;
public class User {
private String name;
private Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
3.5、controller层
package com.wangzhan.controller;
import com.wangzhan.annotation.Decrypt;
import com.wangzhan.annotation.Encrypt;
import com.wangzhan.domain.User;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/starter")
public class BasicController {
/***
* @description 测试加密
* @param user
* @return java.lang.Object
* @author wangzhan
*/
@PostMapping("/user")
@Encrypt
public Object user(@RequestBody User user) {
return user;
}
/***
* @description 测试解密
* @param user
* @return java.lang.Object
* @author wangzhan
*/
@RequestMapping("/saveUser")
public Object saveUser(@RequestBody @Decrypt User user) {
return user.toString();
}
}