客戶端、服務端網路通訊,為了安全,會對報文資料進行加解密操作。
在SpringBoot專案中,最好使用參考AOP思想,加解密與Controller業務邏輯解耦,互不影響。
以解密為例:需要在request請求到達Controller之前進行攔截,獲取請求body中的密文並對其進行解密,然後把解密後的明文重新設定到request的body上。
攔截器、過濾器、Controller之間的關係
如圖所示,在Request物件進入Controller之前,可使用Filter過濾器和Interceptor攔截器進行攔截。
過濾器、攔截器主要差異:
1、過濾器來自於 Servlet;而攔截器來自於 Spring 框架;
2、觸發時機不同:請求的執行順序是:請求進入容器 > 進入過濾器 > 進入 Servlet > 進入攔截器 > 執行控制器(Controller)
3、過濾器是基於方法回撥實現的;攔截器是基於動態代理(底層是反射)實現的;
4、過濾器是 Servlet 規範中定義的,所以過濾器要依賴 Servlet 容器,它只能用在 Web 專案中;攔截器是 Spring 中的一個元件,因此攔截器既可以用在 Web 專案中,同時還可以用在 Application 或 Swing 程式中;
5、過濾器通常是用來實現通用功能過濾的,比如:敏感詞過濾、字符集編碼設定、響應資料壓縮等功能;攔截器更接近業務系統,所以攔截器主要用來實現專案中的業務判斷的,比如:登入判斷、許可權判斷、日誌記錄等業務;
對於我們當前應用場景來說,區別就是過濾器更適用於修改request body。
具體實現分析
修改請求,會有兩個問題:
1、請求體的輸入流被讀取,它就不能再被其他元件讀取,因為輸入流只能被標記、重置,並且在讀取後會被消耗。
2、HttpServletRequest物件的body資料只能get,不能set,不能再次賦值。而咱們的需求是需要給HttpServletRequest body解密並重新賦值。
基於以上兩個問題,咱們需要定義一個HttpServletRequest實現類,增加賦值方法,來滿足我們的需求。
CustomHttpServletRequestWrapper.java
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
/**
* 自定義HttpServletRequestWrapper
* qxc
* 20240622
*/
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private String body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
} finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getBody() {
return this.body;
}
public void setBody(String body) {
this.body = body;
}
}
接下來,繼續寫Filter類,用於攔截請求,解析body, 解密報文,替換請求body資料。
RequestWrapperFilter.java
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Objects;
/**
* 自定義Filter
* qxc
* 20240622
*/
@Slf4j
public class RequestWrapperFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
CustomHttpServletRequestWrapper customHttpServletRequestWrapper = null;
try {
HttpServletRequest req = (HttpServletRequest) request;
customHttpServletRequestWrapper = new CustomHttpServletRequestWrapper(req);
preHandle(customHttpServletRequestWrapper);
} catch (Exception e) {
log.warn("customHttpServletRequestWrapper Error:", e);
}
chain.doFilter((Objects.isNull(customHttpServletRequestWrapper) ? request : customHttpServletRequestWrapper), response);
}
public void preHandle(CustomHttpServletRequestWrapper request) throws Exception {
//僅當請求方法為POST時修改請求體
if (!request.getMethod().equalsIgnoreCase("POST")) {
return;
}
//讀取原始請求體
StringBuilder originalBody = new StringBuilder();
String line;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
while ((line = reader.readLine()) != null) {
originalBody.append(line);
}
}
String bodyText = originalBody.toString();
//json字串轉成map集合
Map<String, String> map = getMap(bodyText);
//獲取解密引數,解密資料
if (map != null && map.containsKey("time") && map.containsKey("data")) {
String time = map.get("time");
String key = "基於時間戳等引數生成金鑰、此處請換成自己的金鑰";
String data = map.get("data");
//解密資料
String decryptedData = Cipher.decrypt(key, data);
//為請求物件重新設定body
request.setBody(decryptedData);
}
}
private Map<String, String> getMap(String text) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 將JSON字串轉換為Map
Map<String, String> map = objectMapper.readValue(text, Map.class);
return map;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
AES加解密演算法封裝
Cipher.java
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import javax.crypto.spec.SecretKeySpec;
/**
* 自定義AES加解密演算法類
* qxc
* 20240622
*/
public class Cipher {
public static String encrypt(String key, String text){
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES");
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encryptedBytes = cipher.doFinal(text.getBytes("UTF-8"));
String cipherText = base64Encode(encryptedBytes);
return cipherText;
}catch (Exception ex){
ex.printStackTrace();
return "";
}
}
public static String decrypt(String key, String cipherText) {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES");
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decryptedBytes = cipher.doFinal(base64Decode(cipherText));
return new String(decryptedBytes, StandardCharsets.UTF_8);
}catch (Exception ex){
ex.printStackTrace();
return "";
}
}
public static String getMd5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(input.getBytes());
byte[] digest = md.digest();
BigInteger bigInt = new BigInteger(1, digest);
String md5Hex = bigInt.toString(16);
while (md5Hex.length() < 32) {
md5Hex = "0" + md5Hex;
}
return md5Hex;
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
public static String base64Encode(byte[] bytes) {
if (bytes != null && bytes.length > 0) {
return Base64.getEncoder().encodeToString(bytes);
}
return "";
}
public static byte[] base64Decode(String base64Str) {
if (base64Str != null && base64Str.length() > 0) {
return Base64.getDecoder().decode(base64Str);
}
return new byte[]{};
}
}
最後,需要在WebMvcConfigurer中配置並使用RequestWrapperFilter
import com.qxc.server.encryption.RequestWrapperFilter;
import com.qxc.server.jwt.JwtInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootApplication
public class ServerApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
@Bean
public FilterRegistrationBean servletRegistrationBean() {
RequestWrapperFilter userInfoFilter = new RequestWrapperFilter();
FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(userInfoFilter);
bean.setName("requestFilter");
bean.addUrlPatterns("/*");
bean.setOrder(Ordered.LOWEST_PRECEDENCE);
return bean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
}
}
這樣,就實現了請求報文的解密、資料替換,而且對Controller邏輯沒有影響。