前言
特别是在做一下政府项目时,有时候需要对所有接口数据进行加密。
加密传输结构体定义
- 请求头
localhost:8080/user/info
header:
Content-Type:application/json
signature:55efb04a83ca083dd1e6003cde127c45
timestamp:1648308048
salt:123456
clientType:ANDORID
- body体
// 原始请求体
{
"page": 1,
"size": 10
}
// 加密后的请求体
{
"data": "1ZBecdnDuMocxAiW9UtBrJzlvVbueP9K0MsIxQccmU3OPG92oRinVm0GxBwdlXXJ"
}
// 加密响应体:
{
"data": "fxHYvnIE54eAXDbErdrDryEsIYNvsOOkyEKYB1iBcre/QU1wMowHE2BNX/je6OP3NlsCtAeDqcp7J1N332el8q2FokixLvdxAPyW5Un9JiT0LQ3MB8p+nN23pTSIvh9VS92lCA8KULWg2nViSFL5X1VwKrF0K/dcVVZnpw5h227UywP6ezSHjHdA+Q0eKZFGTEv3IzNXWqq/otx5fl1gKQ==",
"code": 200,
"signature": "aa61f19da0eb5d99f13c145a40a7746b",
"msg": "",
"timestamp": 1648480034,
"salt": 632648
}
// 解密后的响应体:
{
"code": 200,
"data": [{
"id": 1,
"name": "boyka",
"registerTime": "2022-03-27T00:19:43.699",
"userType": "COMMON"
}],
"msg": "用户列表查询成功",
"salt": 0
}
AES加密工具类
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Decoder;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
/**
*/
public class EncryptUtils {
private EncryptUtils() {
}
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
public static final String IV = "2233445566778899";
/**
* base 64 encode
*
* @param bytes 待编码的byte[]
* @return 编码后的base 64 code
*/
public static String base64Encode(byte[] bytes) {
String data = Base64.encodeBase64String(bytes);
if (!StringUtils.isEmpty(data)) {
// 处理一行超过76个字符换行问题
return data.replaceAll("[\\s*\t\n\r]", "");
} else {
return data;
}
}
/**
* base 64 decode
*
* @param base64Code 待解码的base 64 code
* @return 解码后的byte[]
* @throws Exception
*/
public static byte[] base64Decode(String base64Code) throws Exception {
return StringUtils.isEmpty(base64Code) ? null : new BASE64Decoder().decodeBuffer(base64Code);
}
/**
* AES加密
*
* @param content 待加密的内容
* @param encryptKey 加密密钥
* @return 加密后的byte[]
* @throws Exception
*/
public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
byte[] raw = encryptKey.getBytes();
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);//"算法/模式/补码方式"
IvParameterSpec iv = new IvParameterSpec(IV.getBytes());//使用CBC模式,需要一个向量iv,可增加加密算法的强度
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
return cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
}
/**
* AES解密
*
* @param encryptBytes 待解密的byte[]
* @param decryptKey 解密密钥
* @return 解密后的String
* @throws Exception
*/
public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {
byte[] raw = decryptKey.getBytes();
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);//"算法/模式/补码方式"
IvParameterSpec iv = new IvParameterSpec(IV.getBytes());//使用CBC模式,需要一个向量iv,可增加加密算法的强度
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv);
byte[] decryptBytes = cipher.doFinal(encryptBytes);
return new String(decryptBytes);
}
/**
* AES加密为base 64 code
*
* @param content 待加密的内容
* @param encryptKey 加密密钥
* @return 加密后的base 64 code
* @throws Exception
*/
public static String aesEncrypt(String content, String encryptKey) throws Exception {
return base64Encode(aesEncryptToBytes(content, encryptKey));
}
/**
* 将base 64 code AES解密
*
* @param encryptStr 待解密的base 64 code
* @param decryptKey 解密密钥
* @return 解密后的string
* @throws Exception
*/
public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);
}
public static void main(String[] args) throws Exception {
String timestamp = "1648308048";
String salt = "123456";
String data = "{\n" +
"\t\"page\": 1,\n" +
"\t\"size\": 10\n" +
"}";
String privateKey = "8VBkO5amPcPrXV3m";
String encryptData = aesEncrypt(data, privateKey);
System.out.println(encryptData);
encryptData = "fxHYvnIE54eAXDbErdrDryEsIYNvsOOkyEKYB1iBcre/QU1wMowHE2BNX/je6OP3NlsCtAeDqcp7J1N332el8r4JPvZmKlXKUUReD8ayUckrFQkSATLwASJmMTArIeit28+LbLFz9e9K0vdaRi/splThh1+7u5CDg/ixDWwVThOlZknRgP3RKar6KN7BJGFyCJYNqpYPkuapsa2B2B6DHQ==";
String decryptData = aesDecrypt(encryptData, privateKey);
System.out.println(decryptData);
String signature = Md5Utils.genSignature(timestamp + salt + encryptData + privateKey);
System.out.println(signature);
}
public static byte[] generateDesKey(int length) throws Exception {
//实例化
KeyGenerator kgen = KeyGenerator.getInstance("AES");
//设置密钥长度
kgen.init(length);
//生成密钥
SecretKey skey = kgen.generateKey();
//返回密钥的二进制编码
return skey.getEncoded();
}
public static int genSalt() {
int salt = 0;
while (true) {
salt = (int) (Math.random() * 1000000);
if (salt < 1000000 && salt > 99999)
break;
}
return salt;
}
}
Md5Utils.java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Utils {
/**
* 生成签名
*
* @param value 数据
* @return 结果
*/
public static String genSignature(String value) {
try {
// 得到一个信息摘要器
MessageDigest digest = MessageDigest.getInstance("md5");
byte[] result = digest.digest(value.getBytes());
StringBuilder buffer = new StringBuilder();
// 把每个byte 做一个与运算 0xff;
for (byte b : result) {
// 与运算
int number = b & 0xff;// 加盐
String str = Integer.toHexString(number);
if (str.length() == 1) {
buffer.append("0");
}
buffer.append(str);
}
// 标准的md5加密后的结果
return buffer.toString();
} catch (NoSuchAlgorithmException e) {
return "";
}
}
}
加密拦截器与过滤
SecretFilter
import org.springframework.beans.factory.annotation.Value;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* @description: 过滤器
*/
public class SecretFilter implements Filter {
public static ThreadLocal<Boolean> secretThreadLocal = new ThreadLocal<>();
@Value("${secret.privateKey.h5}")
private String h5Key;
@Value("${secret.privateKey.default}")
private String defaultKey;
public static ThreadLocal<String> clientPrivateKeyThreadLocal = new ThreadLocal<>();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 免加密
if (!request.getRequestURI().startsWith(SecretConstant.PREFIX)) {
secretThreadLocal.set(Boolean.FALSE);
filterChain.doFilter(request, response);
} else {
// 加密,先只考虑POST情况
secretThreadLocal.set(Boolean.TRUE);
// 简单获取对应加密的私钥
String privateKey = ("H5".equalsIgnoreCase(Objects.requireNonNull(request.getHeader("clientType")))) ? h5Key : defaultKey;
clientPrivateKeyThreadLocal.set(privateKey);
filterChain.doFilter(request, response);
}
}
}
- 请求参数处理类
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
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.util.CollectionUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Objects;
import static cn.boykaff.encrypt.common.base.ResponseCode.SECRET_API_ERROR;
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class SecretRequestAdvice extends RequestBodyAdviceAdapter {
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
//如果支持加密消息,进行消息解密。
String httpBody;
if (Boolean.TRUE.equals(SecretFilter.secretThreadLocal.get())) {
httpBody = decryptBody(inputMessage);
} else {
httpBody = StreamUtils.copyToString(inputMessage.getBody(), Charset.defaultCharset());
}
//返回处理后的消息体给messageConvert
return new SecretHttpMessage(new ByteArrayInputStream(httpBody.getBytes()), inputMessage.getHeaders());
}
/**
* 解密消息体
*
* @param inputMessage 消息体
* @return 明文
*/
private String decryptBody(HttpInputMessage inputMessage) throws IOException {
InputStream encryptStream = inputMessage.getBody();
String requestBody = StreamUtils.copyToString(encryptStream, Charset.defaultCharset());
// 验签过程
HttpHeaders headers = inputMessage.getHeaders();
if (CollectionUtils.isEmpty(headers.get("clientType"))
|| CollectionUtils.isEmpty(headers.get("timestamp"))
|| CollectionUtils.isEmpty(headers.get("salt"))
|| CollectionUtils.isEmpty(headers.get("signature"))) {
throw new ResultException(SECRET_API_ERROR, "请求解密参数错误,clientType、timestamp、salt、signature等参数传递是否正确传递");
}
String timestamp = String.valueOf(Objects.requireNonNull(headers.get("timestamp")).get(0));
String salt = String.valueOf(Objects.requireNonNull(headers.get("salt")).get(0));
String signature = String.valueOf(Objects.requireNonNull(headers.get("signature")).get(0));
String privateKey = SecretFilter.clientPrivateKeyThreadLocal.get();
ReqSecret reqSecret = JSON.parseObject(requestBody, ReqSecret.class);
String data = reqSecret.getData();
String newSignature = "";
if (!StringUtils.isEmpty(privateKey)) {
newSignature = Md5Utils.genSignature(timestamp + salt + data + privateKey);
}
if (!newSignature.equals(signature)) {
// 验签失败
throw new ResultException(SECRET_API_ERROR, "验签失败,请确认加密方式是否正确");
}
try {
String decrypt = EncryptUtils.aesDecrypt(data, privateKey);
if (StringUtils.isEmpty(decrypt)) {
decrypt = "{}";
}
return decrypt;
} catch (Exception e) {
log.error("error: ", e);
}
throw new ResultException(SECRET_API_ERROR, "解密失败");
}
}
- 相应参数处理类
SecretResponseAdvice
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
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.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import static cn.boykaff.encrypt.common.base.ResponseCode.SECRET_API_ERROR;
/**
* @description:
* @author: boykaff
* @date: 2022-03-25-0025
*/
@ControllerAdvice
public class SecretResponseAdvice implements ResponseBodyAdvice {
private Logger logger = LoggerFactory.getLogger(SecretResponseAdvice.class);
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
// 判断是否需要加密
Boolean respSecret = SecretFilter.secretThreadLocal.get();
String secretKey = SecretFilter.clientPrivateKeyThreadLocal.get();
// 清理本地缓存
SecretFilter.secretThreadLocal.remove();
SecretFilter.clientPrivateKeyThreadLocal.remove();
if (null != respSecret && respSecret) {
if (o instanceof ResponseBasic) {
// 外层加密级异常
if (SECRET_API_ERROR == ((ResponseBasic) o).getCode()) {
return SecretResponseBasic.fail(((ResponseBasic) o).getCode(), ((ResponseBasic) o).getData(), ((ResponseBasic) o).getMsg());
}
// 业务逻辑
try {
// 使用FastJson序列号会导致和之前的接口响应参数不一致,后面会重点讲到
String data = EncryptUtils.aesEncrypt(objectMapper.writeValueAsString(o), secretKey);
// 增加签名
long timestamp = System.currentTimeMillis() / 1000;
int salt = EncryptUtils.genSalt();
String dataNew = timestamp + "" + salt + "" + data + secretKey;
String newSignature = Md5Utils.genSignature(dataNew);
return SecretResponseBasic.success(data, timestamp, salt, newSignature);
} catch (Exception e) {
logger.error("beforeBodyWrite error:", e);
return SecretResponseBasic.fail(SECRET_API_ERROR, "", "服务端处理结果数据异常");
}
}
}
return o;
}
}
过滤器配置
@Configuration
public class WebConfig {
@Bean
public Filter secretFilter() {
return new SecretFilter();
}
@Bean
public FilterRegistrationBean filterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("secretFilter"));
registration.setName("secretFilter");
registration.addUrlPatterns(PREFIX + "/*");
registration.setOrder(1);
return registration;
}
}
总结
好了到这里大的骨架就已经有了,这样就完成了加解密的过程。
客户端请求加密->发起请求->服务端解密->业务处理->服务端响应加密->客户端解密展示