1、前言
- 请求解密: 前端在请求后端之前,将
body
参数进行AES
对称加密,并且将加密的key
写进request的header中,后端获取请求参数以及header中的key,进行解密操作; - **响应加密:**后端在往前端返回数据前,先对参数进行加密,并且进行
AES
对称加密key
写进respose的header中,前端获取响应参数以及header中的key,进行解密数据; - 实现类: 通过实现
RequestBodyAdvice
接口,对前端请求的参数进行解密并且重新,让真实结构的数据进入到Controller中;通过实现ResponseBodyAdvice
接口,将响应的参数进行AES加密,返回到前端; - 自定义注解: 对于一个请求是否需要继续解密、返回的参数是否需要加密,通过自定义注解
DecryptRequest
与EncryptResponse
实现,注解可以作用在类或者方法上,如果类和方法都存在以方法上的注解为准,注解中的value默认为true,如果对某个方法不想进行加密或者解密那么value就设置为false,比如:@EncryptResponse(value = false)
;
2、工具类
EncryptUtil
实现对请求的解密、响应数据的加密处理
EncryptUtil:
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* @Author: LiHuaZhi
* @Date: 2022/5/26 22:31
* @Description:
**/
public class EncryptUtil {
public final static String AES = "AES";
/**
* 设置为CBC加密,默认情况下ECB比CBC更高效
*/
private final static String CBC = "/CBC/PKCS5Padding";
public static void main(String[] args) {
/**
* 偏移量,AES 128位数据块对应偏移量为16位,任意值即可
*/
String offset = "5LiN6KaB56C06Kej";
String input = "123";
String encryptAES = encryptBySymmetry(input, offset, EncryptUtil.AES);
System.out.println("AES加密:" + encryptAES);
String aes = decryptBySymmetry(encryptAES, offset, EncryptUtil.AES);
System.out.println("AES解密:" + aes);
}
/**
* 对称加密
*
* @param input : 密文
* @param offset : 偏移量
* @param algorithm : 类型:DES、AES
* @return
*/
public static String encryptBySymmetry(String input, String offset, String algorithm) {
try {
// 根据加密类型判断key字节数
checkAlgorithmAndKey(offset, algorithm);
// CBC模式
String transformation = algorithm + CBC;
// 获取加密对象
Cipher cipher = Cipher.getInstance(transformation);
// 创建加密规则
// 第一个参数key的字节
// 第二个参数表示加密算法
SecretKeySpec sks = new SecretKeySpec(offset.getBytes(), algorithm);
// 使用CBC模式
IvParameterSpec iv = new IvParameterSpec(offset.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, sks, iv);
// 加密
byte[] bytes = cipher.doFinal(input.getBytes());
// 输出加密后的数据
return Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("加密失败!");
}
}
/**
* 对称解密
*
* @param input : 密文
* @param offset : 偏移量
* @param algorithm : 类型:DES、AES
* @return
*/
public static String decryptBySymmetry(String input, String offset, String algorithm) {
try {
// 根据加密类型判断key字节数
checkAlgorithmAndKey(offset, algorithm);
// CBC模式
String transformation = algorithm + CBC;
// 1,获取Cipher对象
Cipher cipher = Cipher.getInstance(transformation);
// 指定密钥规则
SecretKeySpec sks = new SecretKeySpec(offset.getBytes(), algorithm);
// 默认采用ECB加密:同样的原文生成同样的密文
// CBC加密:同样的原文生成的密文不一样
// 使用CBC模式
IvParameterSpec iv = new IvParameterSpec(offset.getBytes());
cipher.init(Cipher.DECRYPT_MODE, sks, iv);
// 3. 解密,上面使用的base64编码,下面直接用密文
byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(input));
// 因为是明文,所以直接返回
return new String(bytes);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("解密失败!");
}
}
private static void checkAlgorithmAndKey(String key, String algorithm) {
// 根据加密类型判断key字节数
int length = key.getBytes().length;
boolean typeEnable = false;
if (AES.equals(algorithm)) {
typeEnable = length == 16;
} else {
throw new RuntimeException("加密类型不存在");
}
if (!typeEnable) {
throw new RuntimeException("加密Key错误");
}
}
}
UUIDUtils:
import java.text.SimpleDateFormat;
import java.util.UUID;
/**
* @Author: LiHuaZhi
* @Version: 1.0
**/
public class UUIDUtils {
public UUIDUtils() {
}
public static String getUuId() {
return UUID.randomUUID().toString().replace("-", "");
}
public static String getNumberId() {
return NumberId.getNumberId();
}
public static void main(String[] args) {
System.out.println(getUuId());
System.out.println(getNumberId());
}
static class NumberId {
private static int Guid = 100;
public NumberId() {
}
public void main(String[] args) {
System.out.println(getNumberId());
System.out.println(getNumberId());
System.out.println(getNumberId());
}
private static String getNumberId() {
++Guid;
long now = System.currentTimeMillis();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy");
String time = dateFormat.format(now);
String info = now + "";
int ran = 0;
if (Guid > 999) {
Guid = 100;
}
ran = Guid;
return time + info.substring(2, info.length()) + ran;
}
}
}
3、自定义注解
DecryptRequest:
import java.lang.annotation.*;
/**
* @author LiGezZ
* @date: 加了此注解的接口(true)将进行数据解密操作(post的body) 可
* @Description:
*/
@Target({ElementType.METHOD , ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DecryptRequest {
/**
* 是否对body进行解密
*/
boolean value() default true;
}
EncryptResponse:
import java.lang.annotation.*;
/**
* @author LiGezZ
* @Description: 是否对响应的结果进行加密
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptResponse {
/**
* 是否对结果加密
*/
boolean value() default true;
}
4、加密处理器
EncryptionHandler:
import com.encryption.interceptor.annotation.DecryptRequest;
import com.encryption.interceptor.annotation.EncryptResponse;
import org.springframework.core.MethodParameter;
/**
* @Author: LiHuaZhi
* @Date: 2022/5/26 23:17
* @Description:
**/
public class EncryptionHandler {
public static String IGNORE_HEADER_NAME = "ignoreParam";
public static String ENCRYPT_KEY = "encrypt";
/**
* 判断是否对响应参数进行加密
*
* @param returnType
* @return
*/
public static boolean checkEncrypt(MethodParameter returnType) {
boolean flag = false;
EncryptResponse classPresent = returnType.getContainingClass().getAnnotation(EncryptResponse.class);
if (classPresent != null) {
// 类上标注的是否需要解密
flag = classPresent.value();
}
EncryptResponse methodPresent = returnType.getMethod().getAnnotation(EncryptResponse.class);
if (methodPresent != null) {
// 方法上标注的是否需要解密
flag = methodPresent.value();
}
return flag;
}
/**
* 判断是否对请求参数解密
*
* @param returnType
* @return
*/
public static boolean checkDecrypt(MethodParameter returnType) {
try {
boolean flag = false;
DecryptRequest classPresent = returnType.getContainingClass().getAnnotation(DecryptRequest.class);
if (classPresent != null) {
//类上标注的是否需要解密
flag = classPresent.value();
}
// 判断方法上是否有加密注解
DecryptRequest methodPresent = returnType.getMethod().getAnnotation(DecryptRequest.class);
if (methodPresent != null) {
//方法上标注的是否需要解密
flag = methodPresent.value();
}
return flag;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
5、请求解密
DecryptHttpInputMessage:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.encryption.utils.EncryptUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import lombok.extern.slf4j.Slf4j;
/**
* @author:LiGezZ
*/
@Slf4j
public class DecryptHttpInputMessage implements HttpInputMessage {
private HttpInputMessage inputMessage;
private String charset = "UTF-8";
private String secretKey;
public DecryptHttpInputMessage(HttpInputMessage inputMessage, String secretKey) {
this.inputMessage = inputMessage;
this.secretKey = secretKey;
}
@Override
public InputStream getBody() {
try {
InputStream body = inputMessage.getBody();
String content = getStringByInputStream(body);
// content转map,获取密文
Map<String, String> map = JSON.parseObject(content, Map.class);
String cipherText = map.get(EncryptionHandler.ENCRYPT_KEY);
// 解密密文
// 获取偏移量
String offset = secretKey.substring(10, 26);
String decryptBody = EncryptUtil.decryptBySymmetry(cipherText, offset, EncryptUtil.AES);
return new ByteArrayInputStream(decryptBody.getBytes(charset));
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException("请求数据处理失败!");
}
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
public static String getStringByInputStream(InputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
byte[] b = new byte[10240];
int n;
while ((n = inputStream.read(b)) != -1) {
outputStream.write(b, 0, n);
}
} catch (Exception e) {
inputStream.close();
outputStream.close();
}
return outputStream.toString();
}
}
DecryptRequestBodyAdvice:
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
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;
/**
* @author LiGezZ
*/
@ControllerAdvice
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return EncryptionHandler.checkDecrypt(methodParameter);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
HttpHeaders headers = inputMessage.getHeaders();
// 对特定请求的header不进行进行解密(一般为swagger、feign请求)
List<String> str = headers.get(EncryptionHandler.IGNORE_HEADER_NAME);
if (str != null && str.size() > 0) {
return inputMessage;
}
// 进行解密处理,返回解密后的参数
// 获取请求加密key
String key = headers.getFirst("key");
return new DecryptHttpInputMessage(inputMessage, key);
}
@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 body;
}
}
6、响应加密
EncryptResponseBodyAdvice:
import java.math.BigInteger;
import java.util.List;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializeConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.serializer.ToStringSerializer;
import com.encryption.utils.EncryptUtil;
import com.encryption.utils.UUIDUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
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;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @author LiGezZ
*/
@Slf4j
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return EncryptionHandler.checkEncrypt(returnType);
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
try {
HttpHeaders headers = request.getHeaders();
// 对特定请求的header不进行加密(一般为swagger、feign请求)
List<String> str = headers.get(EncryptionHandler.IGNORE_HEADER_NAME);
if (str != null && str.size() > 0) {
return body;
}
String result = null;
Class<?> dataClass = body.getClass();
if (dataClass.isPrimitive() || (body instanceof String)) {
result = String.valueOf(body);
} else {
SerializeConfig serializeConfig = SerializeConfig.globalInstance;
serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
serializeConfig.put(Long.class, ToStringSerializer.instance);
result = JSON.toJSONString(body, serializeConfig, SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteDateUseDateFormat, SerializerFeature.WriteMapNullValue);
}
String key = UUIDUtils.getUuId().toUpperCase();
// 设置响应的加密key,用于前端解密
response.getHeaders().add(EncryptionHandler.ENCRYPT_KEY, key);
// 对数据加密返回
String offset = key.substring(10, 26);
return EncryptUtil.encryptBySymmetry(result, offset, EncryptUtil.AES);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("数据响应错误!");
}
}
}
6、代码实现
- EncryptionController: 模拟前端的接口;
@Api(tags = "模拟加密-解密")
@RestController
@RequestMapping("/encrypt")
@Slf4j
public class EncryptionController {
/**
* 将明文数据加密为密文,并且返回加密时的key
* 其实就是模拟前端请求时的加密数据
*
* @param text 明文数据
* @return
*/
@ApiOperation(value = "模拟前端加密数据", notes = "模拟前端加密数据")
@ApiOperationSupport(order = 5)
@GetMapping("/jia")
public String jia(String text) {
String key = UUIDUtils.getUuId().toUpperCase();
String offset = key.substring(10, 26);
String encrypt = EncryptUtil.encryptBySymmetry(text, offset, EncryptUtil.AES);
// 写入header
Map<String, String> map = new HashMap<>(2);
// 模拟请求时的body,key为encrypt,value为实际请求参数的加密结果
map.put("encrypt", encrypt);
String body = JSON.toJSONString(map);
Map<String, String> map2 = new HashMap<>(4);
map2.put("key", key);
map2.put("body", body);
return JSON.toJSONString(map2);
}
/**
* 通过key和加密后的密文,解密为明文
* 前端请求时,发送的数据已经被加密了;并且加密的偏移量为key(32字符)的10-26个字符共16个字符
* 前端请求时,发送的数据已经被加密了;并且加密的偏移量为key(32字符)的10-26个字符共16个字符
*
* @param text
* @return
*/
@ApiOperation(value = "模拟前端解密数据", notes = "模拟前端解密数据")
@ApiOperationSupport(order = 5)
@GetMapping("/jie")
public String jie(String key, String text) {
String offset = key.substring(10, 26);
return EncryptUtil.decryptBySymmetry(text, offset, EncryptUtil.AES);
}
}
- TestController: 测试加密解密的接口;
@Api(tags = "测试接口")
@RestController
@RequestMapping("/test")
@Slf4j
@EncryptResponse
@DecryptRequest
public class TestController {
@PostMapping
public String jia(@RequestBody User user) {
System.out.println(user);
return "success";
}
}
7、测试
此处测试没有结合前端,通过knife4j(swagger)
进行测试,需要通过接口的来模拟请求的请求加密和模拟前端的对响应的解密操作,结合接口请求的流程测试如下:
第一步:
模拟前端对数据加密,调用EncryptionController
的jia
方法,如下:
第二步:
对body进行加密请求:
在文档中,可以看到正式需要的请求示例应该是加密前的数据,但是我们需要传入加密后的参数:
加密后请求参数:
响应参数:
解密响应参数:
响应给前端解密的key,不会直接在参数中返回,而是写入到Response Headers
的heder
中;
8、总结
1、在系统中通过在header中设置IGNORE_HEADER_NAME
的变量,表示前端不用进行加密,后端也不会解密(即使有加密注解),这样的目的是为了避免我们在开发阶段使用knife4j(swagger)
进行调试时还需要带上模拟加密过程,以及在微服务下的feign
调用也不用进行加密/解密处理;
2、并不是需要对所有的数据都进行加密处理,需要看情况而定,并且只对post、put、delete的body请求参数进行加密,如果是上传文件、或者下载文件流应该取消请求或者响应加密;
3、RequestBodyAdvice、ResponseBodyAdvice、Intercepter、AOP的先后顺序:Intercepter
、RequestBodyAdvice
对参数进行解密、AOP前置通知
、AOP后置通知
、ResponseBodyAdvice
对响应参数加密