SpringBoot中,接口加解密

1、背景

在项目做等保,要求需要对接口数据进行加密,所做了调用方发送请求前,先对明文加密,然后发送密文,被调用方收到数据后,先进行解密,然后再进行处理。返回的结果同样也可以加密,被调用方将需要返回的数据进行加密,然后将密文返回给调用方,调用方收到后,再进行解密便得到明文。

2、解决方案

使用AES的方式对数据加密

AES介绍

  1. 对称加密
    加密与解密使用同一密钥,效率高但需安全分发密钥。
  2. 分组密码
    将明文分割为固定128位(16字节)的块独立处理。
  3. 密钥长度灵活
    支持128位、192位、256位三种长度,安全性依次递增(AES-128/192/256)

利用RequestBodyAdviceAdapter,继承这个类重写其beforeBodyRead方法,完成请求在进入方法前对参数解密

RequestBodyAdviceAdapter介绍

RequestBodyAdviceAdapter 是 Spring Framework 中用于拦截和处理 **@RequestBody 注解参数**的核心组件,属于 Spring MVC 的全局增强机制。它通过 AOP 思想实现对请求体的统一预处理,避免在 Controller 中重复编写与业务无关的逻辑(如解密、校验、日志记录等)

|---------------------|-------------------------------------|------------------------------|-------------------------|
| 方法 | 调用时机 | 用途 | 返回值 |
| supports() | 请求进入时优先判断 | 决定当前 Advice 是否生效(如按包路径、注解过滤) | true:生效; false:跳过 |
| beforeBodyRead() | 请求体被 HttpMessageConverter 读取 | 修改原始请求体(如解密、字符编码转换) | 自定义的 HttpInputMessage |
| afterBodyRead() | 请求体转换为 Java 对象 | 修改对象(如参数校验、字段注入) | 处理后的 Java 对象 |
| handleEmptyBody() | 请求体为空时 | 处理空请求体场景(如设置默认值) | 替代空体的对象 |

在数据返回时候需要加密,用到ResponseBodyAdvice

ResponseBodyAdvice 介绍

ResponseBodyAdvice 是 Spring MVC(4.1+)及 Spring Boot 中用于全局拦截并定制化处理响应体 的核心接口,通常与 @ControllerAdvice@RestControllerAdvice 配合使用。它允许开发者在控制器方法执行后、响应数据写入 HTTP 响应体之前,对返回的数据进行统一处理,适用于多种通用场景(如数据包装、脱敏、日志记录等)。

  1. 定位与触发时机
    • 作用阶段 :在控制器方法执行完毕且返回值被 HttpMessageConverter 序列化之前介入。
    • 触发条件 :仅对标注 @ResponseBody@RestController 的控制器方法生效。
  1. 核心方法

|---------------------|------------------------------------------------|------------------------------------------------|
| 方法 | 作用 | 参数说明 |
| supports() | 判断当前响应是否需被处理(返回 true 则触发 beforeBodyWrite ) | returnType:控制器返回类型; converterType:消息转换器类型。 |
| beforeBodyWrite() | 实际处理响应体,可修改或替换原始返回值 | body:原始返回值; request/response:HTTP 请求/响应对象; |

作用方法指定方案

1.用到了@RestControllerAdvice这个注解指定扫描包路径来实现,这样就需要supports()方法直接返回true

@ControllerAdvice(basePackages = "cn.shenzhihe.collection.controller")

2.如果想指定具体方法,可以自定义注解方式,在supports()方法下指定注解

java 复制代码
@Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
      return methodParameter.hasMethodAnnotation(Decrypt.class);
    
    }

3.代码实现

指定配置类讲AES注册到ioc

java 复制代码
@Configuration
@EnableConfigurationProperties(SecureProperties.class)
public class SecureConfiguration {
    @Autowired
    private SecureProperties secureProperties;

    @Bean
    public AES aes() {
        return SecureUtil.aes(this.secureProperties.getKey().getBytes(StandardCharsets.UTF_8));
    }
}

配置

java 复制代码
@Getter
@Setter
@ConfigurationProperties("secure")
public class SecureProperties {
    /**
     * 秘钥(长度只能是 128、192或256位,一个普通字符是8位)
     */
    private String key;
}

yml

java 复制代码
secure:
  key: 1234567890123456

入参加密

java 复制代码
/**
 * 请求到达controller中的方法之前,会拦截标注有 @Decrypt 注解或者指定包下的方法,负责将原始请求中的密文转换为明文
 */
@ControllerAdvice(basePackages = "cn.shenzhihe.collection.controller")
public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter {

    @Autowired
    private AES aes;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
       // 注解方式
        //return methodParameter.hasMethodAnnotation(Decrypt.class);
        //路径方式直接返回true
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        String encoding = "UTF-8";
        try {
            //①:获取http请求中原始的body
            String body = IOUtils.toString(inputMessage.getBody(), encoding);
            //②:解密body,使用AES算法解密,得到明文
            String decryptBody = aes.decryptStr(body);
            //将解密之后的body数据重新封装为HttpInputMessage作为当前方法的返回值
            InputStream inputStream = new ByteArrayInputStream(decryptBody.getBytes(encoding));
            return new HttpInputMessage() {
                @Override
                public InputStream getBody() throws IOException {
                    return inputStream;
                }
                @Override
                public HttpHeaders getHeaders() {
                    return inputMessage.getHeaders();
                }
            };
        } catch (Exception e) {
            // 如果解密失败,返回原始消息
            return inputMessage;
        }
    }
}
java 复制代码
/**
 * 在结果返回给调用者之前,会拦截标注有@Encrypt 注解或者包下方法的方法,对接口的返回值进行处理,将其转换为密文
 */
@ControllerAdvice(basePackages = "cn.shenzhihe.collection.controller")
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private AES aes;

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
       // 注解方式
        //return methodParameter.hasMethodAnnotation(Decrypt.class);
        //路径方式直接返回true
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body == null) {
            return body;
        }
        String result;
        if (body instanceof String) {
            result = (String) body;
        } else {
            //如果是对象,则转换为json字符串
            try {
                result = objectMapper.writeValueAsString(body);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        //加密返回
        return this.aes.encryptHex(result);
    }
}

两个注解一起使用,入参解密,返回加密

注解

java 复制代码
/**
 * 接口方法上添加该注解,这表示这个接口的参数是被加密的,进入方法之前,参数会自动被解密
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Decrypt {
}
java 复制代码
/**
 * 接口方法上添加该注解,则返回的结果,会自动加密
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypt {
}

4.测试

java 复制代码
@RestController
@RequestMapping("/secure")
@Slf4j
public class SecureController {

    /**
     * 参数加密测试,需要在方法上标注 @Decrypt 注解
     *
     * @param body
     * @return
     */
   // @Decrypt
    @PostMapping("/decryptTest")
    public List<String> decryptTest(@RequestBody List<String> body) {
        log.info("参数加密测试,请求参数:{}", body);
        return body;
    }

   // @Encrypt
    @GetMapping("/encryptTest")
    public List<String> encryptTest() {
        List<String> encyptList = new ArrayList<>();
        encyptList.add("1");
        encyptList.add("2");
        return encyptList;
    }
}

返回密文

解析密文

相关推荐
用户685453759776932 分钟前
同步成本换并行度:多线程、协程、分片、MapReduce 怎么选才不踩坑
后端
javaTodo39 分钟前
Claude Code 记忆机制详解:从 CLAUDE.md 到 Auto Memory,六层体系全拆解
后端
LSTM971 小时前
使用 C# 和 Spire.PDF 从 HTML 模板生成 PDF 的实用指南
后端
JaguarJack1 小时前
为什么 PHP 闭包要加 static?
后端·php·服务端
BingoGo1 小时前
为什么 PHP 闭包要加 static?
后端
是糖糖啊2 小时前
OpenClaw 从零到一实战指南(飞书接入)
前端·人工智能·后端
百度Geek说2 小时前
基于Spark的配置化离线反作弊系统
后端
后端AI实验室2 小时前
用AI写代码,我差点把漏洞发上线:血泪总结的10个教训
java·ai
Java编程爱好者2 小时前
虚拟线程深度解析:轻量并发编程的未来趋势
后端