自定义spring-boot-starter(请求解密,响应加密)

2,149 阅读7分钟

自定义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、效果

加密解密请求响应-starter-1717382036076.gif

2、准备-自定义starter

2.1、目录

image-20240604164453629

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

image-20240604171516515

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、目录

image-20240604164532233

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();
    }

}

3.6、测试结果 - 静图

image-20240604165523548

image-20240604165550221