在系统开发时,我们需要对接不同的第三方系统,我们可能需要给他们提供接口,但是他们要求的返回值包装类和我们系统中现有的包装类不一致。甚至还有各种各样的加密解决需求,我以前的解决方案是将加密解密,包装这些都写到代码中,抽取一个公共类即可,但是总觉得这种方式不够优雅,而且还要专门catch异常,然后将异常也封装成第三方系统需要的格式。
例如我们现有系统的包装类:
{
"timestamp": "2023-01-31 14:04:07",
"status": 500,
"error": "xx",
"exception": "xx",
"trace": "xx",
"message": "Token失效,请重新登录",
"path": "/xx"
}
而客户要求的包装类:
{
"Ret": 500,
"Msg": "xx",
"Data": null,
"Sig": "43D9C14B87BA728B9F33AA17962259FC"
}
客户要求Data参数要AES加密,同时还要附带Hmac签名的一个Sig。同时,客服传给我的值也会按照特定格式加密,需要进行解密。
对于返回给客户的数据需要加码,我们先编写两个注解
需要加密
/**
* 定义注解
* 功能:请求体/响应体解密/加密,请求体参数/响应需要进行解密/加密
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ThirdBodyDecrypt {
}
无需加密
/**
* 定义注解
* 功能:排除请求体/响应体解密/加密,请求体参数/响应不需要进行解密/加密
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcludeThridBodyDecrypt {
}
返回值包装类定义
@Data
@NoArgsConstructor
public class ThirdResultVO {
/**
* 返回编码
*/
@JsonProperty("Ret")
private Integer ret;
/**
* 有错误表示具体错误信息,无错误返回成功信息
*/
@JsonProperty("Msg")
private String msg;
/**
* 具体返回参数见各接口参数说明,所有数据采用UTF-8编码,JSON格式
*/
@JsonProperty("Data")
private String data;
/**
* 采用HMAC-MD5算法,采用MD5作为散列函数,通过SigSecret(签名密钥)对整个消息主体各参数的值拼接后进行加密
* 出参拼接顺序为:Ret(返回值)、Msg(返回信息)、Data(参数内容)
* 然后采用MD5信息摘要的方式形成新密文,参数签名必须大写
*/
@JsonProperty("Sig")
private String sig;
@JsonIgnore
public String getParamString() {
return this.ret + this.msg + this.data;
}
public static ThirdResultVO OK(String data) {
ThirdResultVO teldResultVO = new ThirdResultVO();
teldResultVO.setData(data);
teldResultVO.setRet(0);
teldResultVO.setMsg("success");
return teldResultVO;
}
public static ThirdResultVO error(int ret, String msg) {
ThirdResultVO r = new ThirdResultVO();
r.setRet(ret);
r.setMsg(msg);
return r;
}
public static ThirdResultVO error(int ret, String msg, String data) {
ThirdResultVO r = new ThirdResultVO();
r.setRet(ret);
r.setMsg(msg);
r.setData(data);
return r;
}
}
拦截请求值并解密
@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
public class ThirdRequestBodyHandler implements RequestBodyAdvice {
private final ObjectMapper objectMapper;
@Override
public boolean supports(@NotNull MethodParameter methodParameter, @NotNull Type targetType,
@NotNull Class<? extends HttpMessageConverter<?>> converterType) {
return SupportCheckUtil.getInstance().checkSupport(methodParameter);
}
@NotNull
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, @NotNull MethodParameter parameter, @NotNull Type targetType,
@NotNull Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
log.info("beforeBodyRead");
if (inputMessage.getBody().available() <= 0) {
return inputMessage;
}
byte[] requestDataByte = new byte[inputMessage.getBody().available()];
int read = inputMessage.getBody().read(requestDataByte);
log.debug("read: {}", read);
byte[] requestDataByteNew = null;
// 这里换成自己项目的统一入参即可
String data = objectMapper.readValue(requestDataByte, String.class);
try {
// 解密
requestDataByteNew = AesEncryptUtil.getInstance().decrypt(data);
} catch (Exception e) {
e.printStackTrace();
}
// 使用解密后的数据,构造新的读取流
assert requestDataByteNew != null;
InputStream rawInputStream = new ByteArrayInputStream(requestDataByteNew);
return new HttpInputMessage() {
@NotNull
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
@NotNull
@Override
public InputStream getBody() {
return rawInputStream;
}
};
}
@NotNull
@Override
public Object afterBodyRead(@NotNull Object body, @NotNull HttpInputMessage inputMessage,
@NotNull MethodParameter parameter, @NotNull Type targetType,
@NotNull Class<? extends HttpMessageConverter<?>> converterType) {
log.info("afterBodyRead");
log.info("请求值原文:{}", body);
return body;
}
@Override
public Object handleEmptyBody(Object body, @NotNull HttpInputMessage inputMessage,
@NotNull MethodParameter parameter, @NotNull Type targetType,
@NotNull Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
拦截返回值并加密
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class ThirdResponseBodyHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(@NotNull MethodParameter returnType, @NotNull Class converterType) {
//排除解密注解
return SupportCheckUtil.getInstance().checkSupport(returnType);
}
@Override
public Object beforeBodyWrite(Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType,
@NotNull Class selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) {
// 直接将Body转换成包装类,然后直接加密,加签即可
ThirdResultVO resultVO;
if (body instanceof ThirdResultVO) {
resultVO = ((ThirdResultVO) body);
if (StringUtils.isNotBlank(resultVO.getSig()))
return body;
} else {
String content = JSONUtil.toJsonStr(body);
log.info("返回值原文:{}", content);
String result = null;
try {
if (StringUtils.isNotBlank(content)) {
result = AesEncryptUtil.getInstance().encrypt(content);
}
} catch (Exception e) {
e.printStackTrace();
}
resultVO = ThirdResultVO.OK(result);
}
String sig = HmacUtil.getInstance().getHmacMD5(resultVO.getParamString());
resultVO.setSig(sig);
log.info("返回值包装:{}", resultVO);
return resultVO;
}
}
使用 将@TeldBodyDecrypt加到Controller上即可实现加密解决
@Slf4j
@RestController
@ThirdBodyDecrypt
@Api(tags = "第三方API")
@RequiredArgsConstructor
@RequestMapping("/third")
public class TeldApi1 extends TeldErrorHandler<TeldResultVO> {
@PostMapping("/query_token")
public String queryToken() {
return "我是没加密的参数";
}
}
我想到的是将原有异常拦截的上层,抽象出一个异常拦截的泛型类,定义一个抽象方法,每个继承他的方法都需要去实现转换返回值的抽象方法。
@Slf4j
public abstract class AllErrorHandler<T> {
/**
* 处理自定义异常
*/
@ExceptionHandler(ResultException.class)
public T handleRRException(ResultException e) {
return ERROR(e.getMessage());
}
@ExceptionHandler(NoHandlerFoundException.class)
public T handlerNoFoundException(Exception e) {
log.error(e.getMessage(), e);
return ERROR(404, "路径不存在,请检查路径是否正确");
}
// ....... 自行补充自己项目的异常一颗
/**
* 实现异常拦截后转换的核心方法,需要每个继承的类自定义去实现返回值
*
* @param code 错误码
* @param msg 错误消息
* @param data 返回的实体信息
* @return 转换后的错误类
*/
protected abstract T ERROR(int code, String msg, Object data);
protected T ERROR(int code, String msg) {
return ERROR(code, msg, null);
}
protected T ERROR(String msg) {
return ERROR(500, msg, null);
}
}
然后在统一异常拦截那里继承
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class BaseExceptionHandler extends AllErrorHandler<Result<?>> {
@Override
protected Result<?> ERROR(int code, String msg, Object data) {
return Result.error(code, msg, data);
}
}
这样我们原有项目就还是返回原来的包装类
然后在要改变返回包装类的方法处继承抽象异常类,并重写返回值方法即可。
@Slf4j
@RestController
@ThridBodyDecrypt
@Api(tags = "第三方API1")
@RequiredArgsConstructor
@RequestMapping("/third")
public class TeldApi1 extends AllErrorHandler<ThridResultVo> {
@PostMapping("/query_token1")
public String queryToken() {
return "我是没加密的参数";
}
@Override
protected ThridResultVo ERROR(int code, String msg, Object data) {
ThridResultVo thridResultVo;
if (data != null) {
try {
String dataString = objectMapper.writeValueAsString(data);
String encrypt = AesEncryptUtil.getInstance().encrypt(dataString);
thridResultVo = ThridResultVo.error(500, msg, encrypt);
} catch (Exception e) {
log.info("异常拦截错误:{}", e.getMessage());
thridResultVo = ThridResultVo.error(500, msg);
}
} else {
thridResultVo = ThridResultVo.error(500, msg);
}
String sig = HmacUtil.getInstance().getHmacMD5( thridResultVo.getParamString());
thridResultVo.setSig(sig);
return thridResultVo;
}
}
这样特定接口抛出的异常,就会是不同的包装类包装的返回值了!!
优雅!!真他娘的优雅!!哈哈哈哈!!