使用@RequestBodyAdvice處理客戶端的加密請求體
阿新 • • 發佈:2020-07-22
業務場景:客戶端把json資料進行加密後,編碼成Base64字串,提交給伺服器。伺服器再進行解密。
使用 @RequestBodyAdvice
,可以在不修改任何Controller
程式碼的前提下,輕鬆完成。
之前寫過一篇帖子,使用@ResponseBodyAdvice統一對響應的資料進行處理。演示了,使用
ResponseBodyAdvice
統一對響應給客戶的json進行AES加密。
RequestBodyAdvice 介面
這個介面定義了一系列的方法,它可以在請求體資料被HttpMessageConverter
轉換前,後。執行一些邏輯程式碼。通常用來做解密。
import java.io.IOException; import java.lang.reflect.Type; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.lang.Nullable; public interface RequestBodyAdvice { /** * 該方法用於判斷當前請求,是否要執行beforeBodyRead方法 * @param handler方法的引數物件 * @param handler方法的引數型別 * @param 將會使用到的Http訊息轉換器類型別 * @return 返回true則會執行beforeBodyRead */ boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); /** * 在Http訊息轉換器執轉換,之前執行 * @param 客戶端的請求資料 * @param handler方法的引數物件 * @param handler方法的引數型別 * @param 將會使用到的Http訊息轉換器類型別 * @return 返回 一個自定義的HttpInputMessage */ HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException; /** * 在Http訊息轉換器執轉換,之後執行 * @param 轉換後的物件 * @param 客戶端的請求資料 * @param handler方法的引數型別 * @param handler方法的引數型別 * @param 使用的Http訊息轉換器類型別 * @return 返回一個新的物件 */ Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); /** * 同上,不過這個方法處理的是,body為空的情況 */ @Nullable Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); }
核心的方法就是 supports
,該方法返回的boolean值,決定了是要執行 beforeBodyRead
方法。而我們主要的邏輯就是在beforeBodyRead
方法中,對客戶端的請求體進行解密。
RequestBodyAdviceAdapter
實現 RequestBodyAdvice
介面的介面卡抽象類
import java.io.IOException; import java.lang.reflect.Type; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.lang.Nullable; public abstract class RequestBodyAdviceAdapter implements RequestBodyAdvice { @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { return inputMessage; } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return body; } @Override @Nullable public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return body; } }
演示:解密客戶端的AES加密請求體
客戶端使用AES演算法把json資料使用AES(128位金鑰)加密後,編碼為Base64字串作為請求體,請求伺服器。伺服器在 RequestBodyAdvice
中完成解密。
RequestBodyDecodeAdvice
import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import org.springframework.core.MethodParameter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter; @RestControllerAdvice public class RequestBodyDecodeAdvice extends RequestBodyAdviceAdapter { /** * 128位的AESkey */ private static final byte[] AES_KEY = "1111111111111111".getBytes(StandardCharsets.US_ASCII); @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { /** * 系統使用的是Gson作為json資料的Http訊息轉換器 */ return GsonHttpMessageConverter.class.isAssignableFrom(converterType); } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { // 讀取加密的請求體 byte[] body = new byte[inputMessage.getBody().available()]; inputMessage.getBody().read(body); try { // 使用AES解密 body = this.decrypt(Base64.getDecoder().decode(body), AES_KEY); } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) { e.printStackTrace(); throw new RuntimeException(e); } // 使用解密後的資料,構造新的讀取流 InputStream rawInputStream = new ByteArrayInputStream(body); return new HttpInputMessage() { @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } @Override public InputStream getBody() throws IOException { return rawInputStream; } }; } /** * AES解密 * * @param data * @param key * @return * @throws InvalidKeyException * @throws NoSuchAlgorithmException * @throws NoSuchPaddingException * @throws IllegalBlockSizeException * @throws BadPaddingException */ public byte[] decrypt(byte[] data, byte[] key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE); return cipher.doFinal(data); } private Cipher getCipher(byte[] key, int model) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(model, secretKeySpec); return cipher; } }
TestController
很簡單,幾乎沒有任何改動,只是列印客戶的的請求體
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 com.google.gson.JsonObject;
@RestController
@RequestMapping("/test")
public class TestController {
private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
@PostMapping
public Object test (@RequestBody JsonObject requestBody) {
LOGGER.info("requestBody={}", requestBody);
return requestBody;
}
}
客戶端
使用相同的AES金鑰加密原始資料
加密後的Base64編碼
Gg/hLfWllxXF7KkzeNTPELCZ3jjxgHL24tJWPhO+O7KS5vAS1ag9xmtjP94L8p8BY+HMggCL1mvVEHEL8+FwSSjWYLln8SZk4CuWil2x4sI=
使用Postman發起請求
伺服器響應的就是解密後的請求體
伺服器日誌
2020-07-22 13:39:31.870 INFO 2684 --- [ XNIO-1 task-1] TestController : requestBody={"name":"SpringBoot中文社群","site":"https://springboot.io"}
成功的解密了資料,並且完成了json物件的編碼。