程序员子龙(Java面试 + Java学习) 程序员子龙(Java面试 + Java学习)
首页
学习指南
工具
开源项目
技术书籍

程序员子龙

Java 开发从业者
首页
学习指南
工具
开源项目
技术书籍
  • 基础

  • JVM

  • Spring

  • 并发编程

  • Mybatis

  • 网络编程

  • 数据库

  • 缓存

  • 设计模式

  • 分布式

  • 高并发

  • SpringBoot

    • SpringBoot 整合redis
    • SpringBoot 线程池
    • springboot下整合mybatis
    • spring boot 配置文件的加载顺序
    • springboot启动不加载bootstrap.yml文件的问题解决
    • SpringBoot设置动态定时任务
    • springboot整合hibernate
    • ApplicationRunner、InitializingBean、@PostConstruct使用详解
    • Spring Boot 优雅的参数校验方案
    • ELK处理 SpringBoot 日志,太优雅了!
    • SpringBoot配置数据源
    • Spring Boot 默认数据库连接池 —— HikariCP
    • 数据库连接池Hikari监控
    • Spring Boot中使用AOP统一处理Web请求日志
    • SpringBoot 三大开发工具,你都用过么?
    • Spring Boot 3.2 + CRaC = 王炸!
    • springboot启动的时候排除加载某些bean
    • spring boot中集成swagger
    • springboot项目引入这个包以后把原来的json报文改为了xml格式返回
    • SpringBoot中new对象不能自动注入对象和属性的问题
    • 使用 Spring Boot Actuator 监控应用
    • 记录一次springboot自动任务线上突然不执行问题排查过程
    • SpringBoot定时任务@Scheduled源码解析
    • Spring Boot + Lua = 王炸!
    • Spring Boot 实现定时任务动态管理
    • SpringBoot的@Async注解有什么坑?
    • druid 参数配置详解
    • Spring Boot HandlerMethodArgumentResolver 使用和场景
    • SpringBoot数据加解密
    • 解决controller层注入的service为null
    • 在 Spring Boot 中通过 RequestBodyAdvice 统一解码请求体
      • RequestBodyAdvice 接口
      • 定义 @DecodeBody 注解
      • Controller
      • RequestBodyDecodeAdvice
      • 测试
      • 总结
    • SpringBoot之使用Redisson实现分布式锁(含完整例子)
  • SpringCloudAlibaba

  • Nginx

  • 面试

  • 生产问题

  • 系统设计

  • 消息中间件

  • Java
  • SpringBoot
程序员子龙
2024-08-15
目录

在 Spring Boot 中通过 RequestBodyAdvice 统一解码请求体

在一些数据比较敏感或者对安全要求比较高的应用中,客户端提交给服务器的数据需要进行加密,服务器需要解密后才能获取到原始的请求数据。

在 Spring Boot 中,可以通过 RequestBodyAdvice 对请求体进行统一的解密处理,这对 Controller 来说是完全透明的,极大地提高了应用的可维护性。

# RequestBodyAdvice 接口

这是由 spring mvc 提供的一个增强接口,用于在 HttpMessageConverter 读取请求体并把它转换为 Java 对象之前进行一些操作。

public interface RequestBodyAdvice {

    boolean supports(MethodParameter methodParameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType);

    HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

    Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

    @Nullable
    Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • supports:是否要执行此接口,如果返回 false,则该 RequestBodyAdvice 会跳过。在这个方法中,可以获取到 Controller 方法中参数及其类型的信息,以及要使用的 HttpMessageConverter 信息。
  • beforeBodyRead:在请求体被读取前执行,在这个方法中,可以获取到完整的请求体,请求头以进行修改。最后,需要返回修改后的 HttpInputMessage。
  • afterBodyRead:在请求体读取后执行。
  • handleEmptyBody:如果读取到的请求体是空,则执行。

接下来,我们通过一个示例来演示 RequestBodyAdvice 接口的用法。

假设,客户端 POST 给服务器的所有请求数据都是通过 Base64 进行编码的,我们需要通过 RequestBodyAdvice 进行统一的解码。

# 定义 @DecodeBody 注解

package cn.springdoc.demo.annotations;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * 
 * 需要对请求体进行解码
 * 
 */
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface DecodeBody {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

该注解用于 Controller 参数,只有注解了 @DecodeBody 的请求体(@RequestBody)才需要解码。

# Controller

package cn.springdoc.demo.web.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.springdoc.demo.annotations.DecodeBody;


@RestController
@RequestMapping
public class DemoController {

    @PostMapping("/demo")
    public ResponseEntity<String> demo (@RequestBody @DecodeBody String payload) {
        return ResponseEntity.ok(payload);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

一个很简单的 Controller,接收客户端 POST 的字符串,并且原样返回。

使用 @DecodeBody 注解了参数,表示需要对客户端的请求体进行解码。

# RequestBodyDecodeAdvice

核心的 RequestBodyAdvice 实现如下。

package cn.springdoc.demo.web.advice;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Base64;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import cn.springdoc.demo.annotations.DecodeBody;

@RestControllerAdvice
public class RequestBodyDecodeAdvice extends RequestBodyAdviceAdapter{

    static final Logger log = LoggerFactory.getLogger(RequestBodyDecodeAdvice.class);

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasParameterAnnotation(DecodeBody.class);
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        
        // 读取完整的客户端请求体,也就是加密/编码后的数据
        byte[] payload = StreamUtils.copyToByteArray(inputMessage.getBody());
        
        log.info("加密 Payload:{}", new String(payload));
        
        // 解码为原始数据
        byte[] rawPayload = Base64.getDecoder().decode(payload);
        
        log.info("原始 Payload:{}", new String(rawPayload));
        
        // 返回 HttpInputMessage 匿名对象
        return new HttpInputMessage() {
            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }
            @Override
            public InputStream getBody() throws IOException {
                // 使用原始数据构建为 ByteArrayInputStream
                return new ByteArrayInputStream(rawPayload);
            }
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

这里我们直接继承了 RequestBodyAdviceAdapter 适配器类,它实现了 RequestBodyAdvice,并且提供了默认实现,所以我们只需要覆写自己感兴趣的方法即可。

通过 @RestControllerAdvice 注解,自动注册到 RequestMappingHandlerAdapter 中。

@RestControllerAdvice 还可以通过其 basePackages 和 basePackageClasses 属性更细粒度地控制要拦截的 Controller,具体可以参阅 官方文档 (opens new window)。

首先,在 supports 方法中判断 Controller 的注解是否有 @DecodeBody 注解,如果没有的话,表示不需要进行解码,返回 false。

然后,beforeBodyRead 被调用,使用 StreamUtils 工具类读取完整的请求体,也就是客户端 Base64 编码后的数据。接着对其进行解码,得到原文。

最后,返回 HttpInputMessage 对象,请求头不做任何修改,但是请求体使用的是 解码后的原文。

# 测试

启动服务器,在控制台使用 cURL 发起请求:

$ curl -H "Content-Type: text/plain; charset=UTF-8" -X POST -d "SGVsbG8gc3ByaW5nZG9jLmNu" "http://localhost:8080/demo"
Hello springdoc.cn
1
2

如上,POST 到服务器的字符串 SGVsbG8gc3ByaW5nZG9jLmNu 是 Hello springdoc.cn 字符串的 Base64 编码。得到的响应和原文相匹配,说明在 Controller 中得到的请求体,已经是正确解码后的数据了。

后端输出的日志如下:

[nio-8080-exec-8] c.s.d.w.advice.RequestBodyDecodeAdvice   : 加密 Payload:SGVsbG8gc3ByaW5nZG9jLmNu
[nio-8080-exec-8] c.s.d.w.advice.RequestBodyDecodeAdvice   : 原始 Payload:Hello springdoc.cn
1
2

# 总结

通过实现 RequestBodyAdvice 接口,并注册为 Spring 的 Bean,可以在请求到达 Controller 之前或之后对请求体进行定制化的处理。这样可以实现一些常见的需求,例如请求体解密、数据验签、日志记录等。

上次更新: 2024/09/26, 14:54:48
解决controller层注入的service为null
SpringBoot之使用Redisson实现分布式锁(含完整例子)

← 解决controller层注入的service为null SpringBoot之使用Redisson实现分布式锁(含完整例子)→

最近更新
01
一个注解,优雅的实现接口幂等性
11-17
02
MySQL事务(超详细!!!)
10-14
03
阿里二面:Kafka中如何保证消息的顺序性?这周被问到两次了
10-09
更多文章>
Theme by Vdoing | Copyright © 2024-2024

    辽ICP备2023001503号-2

  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式