背景:
在实际项目中,我们常常需要在Controller请求前后进行一些操作,比如:参数解密/返回结果加密,打印请求参数和返回结果的日志等。
方式一:统一包装返回值
- 通常返回结果包含code、message、data,结构如下
- 缺点:每个Controller都要修改
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult<T> {
private int code;
private String message;
private T data;
public ResponseResult(T data) {
this.data = data;
this.code = 0;
this.message = "success";
}
}
方式二:使用RequestBodyAdvice和ResponseBodyAdvice对请求前后进行处理
基于ControllerAdvice和HttpMessageConverter实现
注解1:标记不加密处理ExcludeHttpBodyDecrypt
import java.lang.annotation.*;
/**
* @author yin
* @create 2022-6-1 14:52
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcludeHttpBodyDecrypt {
}
注解1:标记加密处理ExcludeHttpBodyDecrypt
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* RequestBodyHandler
*
* @Author yin
* @create 2022-6-1 14:52
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HttpBodyDecrypt {
}
测试Controller接口
@RestController
@RequestMapping("/UniversityManagement")
public class BsBuUniversityInfoController {
@HttpBodyDecrypt
@PostMapping(value = "/BsBuUniversityInfo/testNoLogin", name = "测试接口")
public Object test(@RequestBody BsBuUniversityInfoModel model) {
BsBuUniversityInfo buUniversityInfo = new BsBuUniversityInfo();
buUniversityInfo.setUniversityCode("00000");
return buUniversityInfo;
}
}
通过RequestBodyAdvice,拦截Controller对request解密,签名验证处理
/**
* @author yin
* @create 2022-6-1 10:09
*/
import cn.gewut.business.common.Annotation.ExcludeHttpBodyDecrypt;
import cn.gewut.business.common.Annotation.HttpBodyDecrypt;
import cn.gewut.business.common.Util.AESUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
@ControllerAdvice
public class RequestAdvisor implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
System.out.println("supports");
//排除解密注解--包含@ExcludeHttpBodyDecrypt 返回false不执行
boolean methodHasExcludeHttpBodyDecrypt = methodParameter.hasMethodAnnotation(ExcludeHttpBodyDecrypt.class);
if (methodHasExcludeHttpBodyDecrypt) {
return false;
}
//解密注解--包含@HttpBodyDecrypt 返回true 需要执行
boolean methodHasHttpBodyDecrypt = methodParameter.hasMethodAnnotation(HttpBodyDecrypt.class);
if (methodHasHttpBodyDecrypt) {
return true;
}
//排除解密注解--声明类是否包含@ExcludeHttpBodyDecrypt 返回false不执行
boolean classHasExcludeHttpBodyDecrypt = methodParameter.getDeclaringClass().getAnnotation(ExcludeHttpBodyDecrypt.class) != null;
if (classHasExcludeHttpBodyDecrypt) {
return false;
}
boolean classHasHttpBodyDecrypt = methodParameter.getDeclaringClass().getAnnotation(HttpBodyDecrypt.class) != null;
if (classHasHttpBodyDecrypt) {
return true;
}
return false;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
System.out.println("beforeBodyRead");
if (inputMessage.getBody().available() <= 0) {
return inputMessage;
}
byte[] requestDataByte = new byte[inputMessage.getBody().available()];
inputMessage.getBody().read(requestDataByte);
byte[] requestDataByteNew = null;
try {
// 解密
requestDataByteNew = AESUtils.decryptHandler(requestDataByte);
} catch (Exception e) {
e.printStackTrace();
}
// 使用解密后的数据,构造新的读取流
InputStream rawInputStream = new ByteArrayInputStream(requestDataByteNew);
return new HttpInputMessage() {
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
@Override
public InputStream getBody() throws IOException {
return rawInputStream;
}
};
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
System.out.println("afterBodyRead");
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
System.out.println("handleEmptyBody");
return null;
}
}
通过ResponseBodyAdvice,拦截Controller对Response封装,统一返回对象
/**
* @author yin
* @create 2022-6-1 10:09
*/
import cn.gewut.business.common.Util.ResponseResult;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class ResponseAdvisor implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
System.out.println(methodParameter.toString());
return o;
// if (o instanceof ResponseResult) {
// return o;
// }
// return new ResponseResult<>(o);
}
}
用到的 AES加密,签名工具类
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* @author yin
* @create 2022-6-1 10:09
*/
public class AESUtils {
public static final String AESKey = "key1234567890key";
public static final String signKey = "123456";
// 加密
public static String Encrypt(String sSrc, String sKey) throws Exception {
if (sKey == null) {
System.out.print("Key为空null");
return null;
}
// 判断Key是否为16位
if (sKey.length() != 16) {
System.out.print("Key长度不是16位");
return null;
}
byte[] raw = sKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
return Base64.getEncoder().encodeToString(encrypted);//此处使用BASE64做转码功能,同时能起到2次加密的作用。
}
// 解密
public static String Decrypt(String sSrc, String sKey) throws Exception {
try {
// 判断Key是否正确
if (sKey == null) {
System.out.print("Key为空null");
return null;
}
// 判断Key是否为16位
if (sKey.length() != 16) {
System.out.print("Key长度不是16位");
return null;
}
byte[] raw = sKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] encrypted1 = Base64.getDecoder().decode(sSrc);//先用base64解密
try {
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original, "utf-8");
return originalString;
} catch (Exception e) {
System.out.println(e.toString());
return null;
}
} catch (Exception ex) {
System.out.println(ex.toString());
return null;
}
}
/**
* AES解密 加MD5签名验证。
*
* @param requestBytes
* @return
*/
public static byte[] decryptHandler(byte[] requestBytes) throws Exception {
if (requestBytes.length <= 0) {
return new byte[0];
}
String requestData = new String(requestBytes, StandardCharsets.UTF_8);
JSONObject jsonobj = JSON.parseObject(requestData);
System.out.println("收到的加密数据:" + jsonobj);
String encrypt = jsonobj.get("encrypt") == null ? "" : jsonobj.get("encrypt").toString();
String decrypt = "";
if (encrypt.length() > 0) {
//解密参数
decrypt = AESUtils.Decrypt(encrypt, AESKey);
if (decrypt == null) {
//解密失败,返回异常
throw new IllegalArgumentException("解密失败");
}
}
System.out.println("解密后的数据:" + decrypt);
//获取并验证签名。
String sign = jsonobj.get("sign") == null ? "" : jsonobj.get("sign").toString();
String md5Data = Md5Utils.md5Digest(decrypt + signKey);
if (!sign.equals(md5Data)) {
//验签失败
throw new IllegalArgumentException("验证签名失败");
}
//验证通过,返回解密后的参数
return decrypt.getBytes(StandardCharsets.UTF_8);
}
/**
* AES加密 加MD5签名验证。
*
* @param requestBytes
* @return
*/
public byte[] encryptHandler(byte[] requestBytes) throws Exception {
if (requestBytes.length <= 0) {
return new byte[0];
}
String requestData = new String(requestBytes, StandardCharsets.UTF_8);
String encrypt = "";
if (requestData.length() > 0) {
//加密密文
encrypt = AESUtils.Encrypt(requestData, AESKey);
if (encrypt == null) {
//加密失败,返回异常
throw new IllegalArgumentException("加密失败");
}
}
JSONObject jsonObj = new JSONObject();
jsonObj.put("encrypt", encrypt);//加密数据
jsonObj.put("sign", Md5Utils.md5Digest(requestData + signKey));//签名
System.out.println("加密签名后数据:" + jsonObj);
//验证通过,返回解密后的参数
return jsonObj.toJSONString().getBytes(StandardCharsets.UTF_8);
}
//测试AES加密
public static void main(String[] args) throws Exception {
/*
* 此处使用AES-128-ECB加密模式,key需要为16位。
*/
String cKey = "key1234567890key";
// 需要加密的字串
String cSrc = "www.yin.com";
System.out.println(cSrc);
// 加密
String enString = Encrypt(cSrc, cKey);
System.out.println("加密后的字串是:" + enString);
// 解密
String DeString = Decrypt(enString, cKey);
System.out.println("解密后的字串是:" + DeString);
System.out.println("*******************************************");
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "小米");
jsonObject.put("universityCode", "00000");
System.out.println("发送的接口数据:" + jsonObject);
AESUtils aesUtils = new AESUtils();
//加密
byte[] bytes = aesUtils.encryptHandler(jsonObject.toJSONString().getBytes());
//解密
byte[] bytes1 = aesUtils.decryptHandler(bytes);
}
}
原理
RequestResponseBodyAdviceChain
1. 启动执行RequestResponseBodyAdviceChain()初始化
把RequestBodyAdvice,ResponseBodyAdvice实现类都加到RequestResponseBodyAdviceChain;
2. 每次请求执行beforeBodyRead(),
源码可以看出循环所有实现类,然后先执行advice.supports(parameter, targetType, converterType) 为true后,执行request = advice.beforeBodyRead(request, parameter, targetType, converterType);
3. 再执行afterBodyRead()
源码可以看出循环所有实现类,然后先执行advice.supports(parameter, targetType, converterType) 为true后,执行body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> {
private final List<Object> requestBodyAdvice = new ArrayList<>(4);
private final List<Object> responseBodyAdvice = new ArrayList<>(4);
/**
* Create an instance from a list of objects that are either of type
* {@code ControllerAdviceBean} or {@code RequestBodyAdvice}.
*/
public RequestResponseBodyAdviceChain(@Nullable List<Object> requestResponseBodyAdvice) {
this.requestBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, RequestBodyAdvice.class));
this.responseBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, ResponseBodyAdvice.class));
}
@SuppressWarnings("unchecked")
static <T> List<T> getAdviceByType(@Nullable List<Object> requestResponseBodyAdvice, Class<T> adviceType) {
if (requestResponseBodyAdvice != null) {
List<T> result = new ArrayList<>();
for (Object advice : requestResponseBodyAdvice) {
Class<?> beanType = (advice instanceof ControllerAdviceBean ?
((ControllerAdviceBean) advice).getBeanType() : advice.getClass());
if (beanType != null && adviceType.isAssignableFrom(beanType)) {
result.add((T) advice);
}
}
return result;
}
return Collections.emptyList();
}
@Override
public boolean supports(MethodParameter param, Type type, Class<? extends HttpMessageConverter<?>> converterType) {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
if (advice.supports(parameter, targetType, converterType)) {
request = advice.beforeBodyRead(request, parameter, targetType, converterType);
}
}
return request;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
if (advice.supports(parameter, targetType, converterType)) {
body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
}
return body;
}
@Override
@Nullable
public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
return processBody(body, returnType, contentType, converterType, request, response);
}
@Override
@Nullable
public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
if (advice.supports(parameter, targetType, converterType)) {
body = advice.handleEmptyBody(body, inputMessage, parameter, targetType, converterType);
}
}
return body;
}
@SuppressWarnings("unchecked")
@Nullable
private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
if (advice.supports(returnType, converterType)) {
body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
contentType, converterType, request, response);
}
}
return body;
}
@SuppressWarnings("unchecked")
private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
List<Object> availableAdvice = getAdvice(adviceType);
if (CollectionUtils.isEmpty(availableAdvice)) {
return Collections.emptyList();
}
List<A> result = new ArrayList<>(availableAdvice.size());
for (Object advice : availableAdvice) {
if (advice instanceof ControllerAdviceBean) {
ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
continue;
}
advice = adviceBean.resolveBean();
}
if (adviceType.isAssignableFrom(advice.getClass())) {
result.add((A) advice);
}
}
return result;
}
private List<Object> getAdvice(Class<?> adviceType) {
if (RequestBodyAdvice.class == adviceType) {
return this.requestBodyAdvice;
}
else if (ResponseBodyAdvice.class == adviceType) {
return this.responseBodyAdvice;
}
else {
throw new IllegalArgumentException("Unexpected adviceType: " + adviceType);
}
}
}