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
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
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.