一、 应用场景
当和第三方应用对接系统的时候, 可能别人的参数加密方式和我们的不相同,那就需要和对方沟通好他们的接口参数是如何加密的,达成一致后才方便后续的工作开展。
二、示例说明
采用Springboot 项目开发,先在component 中封装好切面,然后在具体的服务中依赖此项目。为啥要采用切面,主要是因为使用注解 @ControllerAdvice
比较方便实现
三、 SpringMVC 请求和响应相关组件结构
1、component 具体结构与实现
1.1 第三方应用配置信息
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.Serializable;
import java.security.MessageDigest;
/**
* 第三方应用配置
*/
@Component
public class ThirdPartyAppConfig implements Serializable {
@Value("${third.party.app.config.secret}")
private String appSecret;
@Value("${third.party.app.config.id}")
private String appId;
/**
具体的加密方式:
1. 将 dict 进行 json 序列号,之后使用 AES/CBC/PKCS5PADDING 进行对称加密,加密的 为 app_secret
的 sha256 的 digest 哈希值的前 16 字节, 加密的块长度为 16
2. 加密之后,生成的是标准的 base64 编码,为了 url 传输安全, 做两部转换:
a) 将 base64 结果中的+/分别用-和_替代,例:a+/b ===> a-_b
b) 将 base64 尾部用于 padding 的=去除,例: abc= 变成 abc 其中的时间戳为 UTC 时间
*/
/**
* 加密
* @param data 需要加密的json 数据
* @return
* @throws Exception
*/
public String encrypt(String data) throws Exception {
if (StringUtils.isEmpty(data)) {
return null;
}
MessageDigest sha = MessageDigest.getInstance("SHA-256");
sha.update(appSecret.getBytes());
byte[] raw = sha.digest();
byte[] raw_half = new byte[16];
System.arraycopy(raw, 0, raw_half, 0, 16);
MessageDigest iv_init = MessageDigest.getInstance("MD5");
iv_init.update(data.getBytes("UTF-8"));
byte[] iv_raw = iv_init.digest();
SecretKeySpec skeySpec = new SecretKeySpec(raw_half, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");//"算法/模式/补码方式"
IvParameterSpec iv = new IvParameterSpec(iv_raw);//使用CBC模式,需要一个向量iv,可增加加密算法的强度
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8"));
byte[] encrypted_sum = new byte[encrypted.length + 16];
System.arraycopy(iv_raw, 0, encrypted_sum, 0, 16);
System.arraycopy(encrypted, 0, encrypted_sum, 16, encrypted.length);
String result = new BASE64Encoder().encode(encrypted_sum).toString().trim().replace("+", "-")
.replace("/", "_").replace("\r", "").replace("\n", "");//此处使用BASE64做转码功能,同时能起到2次加密的作用。
while (result.endsWith("=")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
/**
* 解密
*
* @param decodeData 需要解密的数据
* @return
*/
public String decrypt(String decodeData) {
try {
int trim_number = (4 - decodeData.length() % 4) % 4;
String trim_str = "";
for (int i = 0; i < trim_number; i++) {
trim_str += "=";
}
decodeData = (decodeData + trim_str).replace("_", "/").replace("-", "+");
MessageDigest sha = MessageDigest.getInstance("SHA-256");
sha.update(appSecret.getBytes());
byte[] raw = sha.digest();
byte[] raw_half = new byte[16];
System.arraycopy(raw, 0, raw_half, 0, 16);
byte[] encrypted_init = new BASE64Decoder().decodeBuffer(decodeData);//先用base64解密
byte[] iv_raw = new byte[16];
System.arraycopy(encrypted_init, 0, iv_raw, 0, 16);
byte[] encrypted = new byte[encrypted_init.length - 16];
System.arraycopy(encrypted_init, 16, encrypted, 0, encrypted_init.length - 16);
SecretKeySpec skeySpec = new SecretKeySpec(raw_half, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(iv_raw);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
byte[] original = cipher.doFinal(encrypted);
return new String(original, "UTF-8");
} catch (Exception ex) {
System.out.println(ex);
return null;
}
}
}
1.2 将配置类作为自动配置的类
在 resources/META-INF/spring.factories
中配置,这样就将系统配置文件的值: third.party.app.config.secret
和 third.party.app.config.id
自动设置到 ThirdPartyAppConfig
属性中
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.thirdparty.ThirdPartyAppConfig
2、 对于请求体入参的RequestBody 进行解密
2.1 定义解密注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 解密请求
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptRequestBody {
}
2.2 解密的请求体Controller 切面
import com.thirdparty.ThirdPartyAppConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
@ControllerAdvice
@Component
@Aspect
@Slf4j
@Order(1)
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
@Resource
private ThirdPartyAppConfig thirdPartyAppConfig;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(DecryptRequestBody.class) || methodParameter.hasParameterAnnotation(DecryptRequestBody.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
//先判断有没有使用该注解
boolean isAnnotationPresent = parameter.getMethod().isAnnotationPresent(DecryptRequestBody.class);
if (isAnnotationPresent) {
return new DecryptHttpInputMessage(inputMessage, "UTF-8");
}
return inputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return null;
}
public class DecryptHttpInputMessage implements HttpInputMessage {
private HttpInputMessage httpInputMessage;
private String charset;
@Override
public HttpHeaders getHeaders() {
return httpInputMessage.getHeaders();
}
public DecryptHttpInputMessage(HttpInputMessage httpInputMessage, String charset) {
this.httpInputMessage = httpInputMessage;
this.charset = charset;
}
@Override
public InputStream getBody() throws IOException {
//读取body的数据
StringWriter writer = new StringWriter();
IOUtils.copy(httpInputMessage.getBody(), writer, StandardCharsets.UTF_8.name());
String decrypt = writer.toString();
log.info("前端传进来的数据:{}", decrypt);
//把数据解密,
decrypt = thirdPartyAppConfig.decrypt(decrypt);
log.info("解密后的数据为:{}", decrypt);
return IOUtils.toInputStream(decrypt, charset);
}
}
}
3、 对于响应体入参的ResponseBody 进行加密
3.1 定义加密注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 加密响应体
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptResponseBody {
}
3.2 加密的响应体Controller 切面
import com.alibaba.fastjson.JSONObject;
import com.component.common.base.BaseResult;
import com.thirdparty.ThirdPartyAppConfig;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
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.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
@ControllerAdvice
@Component
@Aspect
@Slf4j
@Order(1)
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<BaseResult> {
@Resource
private ThirdPartyAppConfig thirdPartyAppConfig;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return returnType.hasMethodAnnotation(EncryptResponseBody.class);
}
@Override
public BaseResult beforeBodyWrite(BaseResult body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
try {
Object content = body.getContent();
if (content != null) {
String jsonString = JSONObject.toJSONString(content);
body.setContent(thirdPartyAppConfig.encrypt(jsonString));
}
} catch (Exception e) {
e.printStackTrace();
}
return body;
}
}
4、实际使用
@PostMapping("/testParam")
@DecryptRequest
public BaseResult<?> testParam(@RequestBody String requestParam){
//这里是解密后的数据
log.info(requestParam);
return BaseResult.buildSuccess("转换后的参数为:",requestParam);
}