The client encodes or encrypts the request body (e.g. AES encryption, Base64 encoding), and the server decodes the request body after receiving it. This is a very common requirement. Thanks to the RequestBodyAdvice interface provided by spring mvc. We can do this very easily and without modifying any code in the Controller.

Practice

The client’s request body is encoded using Base64 and the server automatically decodes it via RequestBodyAdvice. This is all transparent to the Controller and no code changes are required.

Create a project

spring boot demo app

Base64Encoded

If the parameters of a Handler Method are annotated with @Base64Encoded. then it means that the client request body is encoded using Base64. It needs to be decoded first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package io.springcloud.demo.annotation;

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

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

@Retention(RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Base64Encoded {

}

DecodeHttpInputMessage

DecodeHttpInputMessage is an implementation class of the HttpInputMessage interface. This class represents a complete Http request, including the body and header.

 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
package io.springcloud.demo.advice;

import java.io.IOException;
import java.io.InputStream;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;

public class DecodeHttpInputMessage implements HttpInputMessage {

    private HttpHeaders headers;

    private InputStream body;

    public DecodeHttpInputMessage(HttpHeaders headers, InputStream body) {
        super();
        this.headers = headers;
        this.body = body;
    }

    @Override
    public HttpHeaders getHeaders() {
        return this.headers;
    }

    @Override
    public InputStream getBody() throws IOException {
        return this.body;
    }
}

Base64DecodeBodyAdvice

Custom RequestBodyAdvice implementation class. This interface is so simple that the main points are in the code comments.

 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
package io.springcloud.demo.advice;

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

import org.springframework.core.MethodParameter;
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 io.springcloud.demo.annotation.Base64Encoded;
import lombok.extern.slf4j.Slf4j;

@RestControllerAdvice // Don't forget the @RestControllerAdvice annotation. It will take effect for all RestControllers.
@Slf4j
public class Base64DecodeBodyAdvice extends RequestBodyAdviceAdapter {

    /**
        * If this method returns false, the `beforeBodyRead` method will not be executed. 
        */
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // If the parameter is annotated with `@Base64Encoded` then it needs to be decoded.
        return methodParameter.hasParameterAnnotation(Base64Encoded.class);
    }

    /**
        * This method will be executed before spring mvc reads the request body. We can do some pre-processing of the request body here.
        */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) throws IOException {

        try (InputStream inputStream = inputMessage.getBody()) {

            // Read request body
            byte[] body = StreamUtils.copyToByteArray(inputStream);
            
            log.info("raw: {}", new String(body));

            // Base64 Decode
            byte[] decodedBody = Base64.getDecoder().decode(body);
            
            log.info("decode: {}", new String(decodedBody, StandardCharsets.UTF_8));

            // Return the decoded body
            return new DecodeHttpInputMessage(inputMessage.getHeaders(), new ByteArrayInputStream(decodedBody));
        }
    }
}

TestController

A very simple Controller used to verify that the request body has been successfully decoded.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package io.springcloud.demo.controller;

import java.util.Map;

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 io.springcloud.demo.annotation.Base64Encoded;

@RestController
@RequestMapping("/api/test")
public class TestController {

    @PostMapping(consumes = "text/plain", produces = "application/json; charset=utf-8")
    public Map<String, Object> test(@RequestBody @Base64Encoded String content) {
        return Map.of("success", true, "cotent", content);
    }
}

Testing

Client-side encrypted request body

Suppose our request body is the string: “你好 Spring”, and its Base64 encoding is: 5L2g5aW9IFNwcmluZw==

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package io.springcloud.demo.test;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Main {
    public static void main(String[] args) {
        String content = Base64.getEncoder().encodeToString("你好 Spring".getBytes(StandardCharsets.UTF_8));
        System.out.println(content);
        // 5L2g5aW9IFNwcmluZw==
    }
}

Client Request Log

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
POST /api/test HTTP/1.1
Content-Type: text/plain
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: fbdd10ee-8975-4765-8f3d-2c5a25a9316e
Host: localhost
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 20
 
5L2g5aW9IFNwcmluZw==
 
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: application/json;charset=utf-8
Date: Tue, 15 Mar 2022 07:45:42 GMT
 
{"success":true,"cotent":"你好 Spring"}

Server console output log

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
2022-03-15 15:45:42.845 DEBUG 14552 --- [  XNIO-1 task-1] io.undertow.request.security             : Attempting to authenticate /api/test, authentication required: false
2022-03-15 15:45:42.845 DEBUG 14552 --- [  XNIO-1 task-1] io.undertow.request.security             : Authentication outcome was NOT_ATTEMPTED with method io.undertow.security.impl.CachedAuthenticatedSessionMechanism@75e9e4f1 for /api/test
2022-03-15 15:45:42.845 DEBUG 14552 --- [  XNIO-1 task-1] io.undertow.request.security             : Authentication result was ATTEMPTED for /api/test
2022-03-15 15:45:42.846 DEBUG 14552 --- [  XNIO-1 task-1] o.s.web.servlet.DispatcherServlet        : POST "/api/test", parameters={}
2022-03-15 15:45:42.846 DEBUG 14552 --- [  XNIO-1 task-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to io.springcloud.demo.controller.TestController#test(String)
2022-03-15 15:45:42.847  INFO 14552 --- [  XNIO-1 task-1] i.s.demo.advice.Base64DecodeBodyAdvice   : raw: 5L2g5aW9IFNwcmluZw==
2022-03-15 15:45:42.847  INFO 14552 --- [  XNIO-1 task-1] i.s.demo.advice.Base64DecodeBodyAdvice   : decode: 你好 Spring
2022-03-15 15:45:42.848 DEBUG 14552 --- [  XNIO-1 task-1] m.m.a.RequestResponseBodyMethodProcessor : Read "text/plain;charset=UTF-8" to ["你好 Spring"]
2022-03-15 15:45:42.848 DEBUG 14552 --- [  XNIO-1 task-1] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json;charset=utf-8', given [*/*] and supported [application/json;charset=utf-8]
2022-03-15 15:45:42.849 DEBUG 14552 --- [  XNIO-1 task-1] m.m.a.RequestResponseBodyMethodProcessor : Writing [{success=true, cotent=你好 Spring}]
2022-03-15 15:45:42.853 DEBUG 14552 --- [  XNIO-1 task-1] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

Everything is normal.