一、需求

只针对@RequestBody、 @ResponseBody两个注解起作用

期望在request请求进入controller前做是否解密验证,response在返回前做是否加密验证

二、设计

  • 添加自定义注解@Encrypt加解密注解(使用范围类与方法上)
  • 添加一个加解密注解的判定类。
  • 继承RequestBodyAdvice重写beforeBodyWrite方法结合判定类与外部配置确认调用是否需要加解密
  • ResponseBodyAdvice重写beforeBodyRead方法结合判定类与外部配置确认调用是否需要加解密

RequestBodyAdviceResponseBodyAdvice

spring boot jpa 实现数据的加解密 springboot加解密接口参数_spring

public interface RequestBodyAdvice {

	// 判断是否拦截(可以更精细化地进行判断是否拦截)
	boolean supports(MethodParameter methodParameter, Type targetType,
			Class<? extends HttpMessageConverter<?>> converterType);

	// 进行请求前的拦截处理
	HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

	// 进行请求后的拦截处理
	Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

	// 对空请求体的处理
	@Nullable
	Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}

spring boot jpa 实现数据的加解密 springboot加解密接口参数_spring boot_02

public interface ResponseBodyAdvice<T> {

	// 判断是否拦截(可以更精细化地进行判断是否拦截)
	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

	// 进行响应前的拦截处理
	@Nullable
	T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType,
			ServerHttpRequest request, ServerHttpResponse response);
			
}

三、实现

spring boot jpa 实现数据的加解密 springboot加解密接口参数_spring boot_03

1.自定义加解密的注解

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
 * @description: 参数加密设置
 * @Author: wts
 * @Date: 2022/11/7 11:10
 * @Version 1.0
 */
public @interface Encrypt {

    /**
     * 入参是否解密,默认解密
     */
    boolean in() default true;

    /**
     * 返回是否加密,默认加密
     */
    boolean out() default true;
}

2.实现RequestBodyAdvice、ResponseBodyAdvice 接口

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.wts.common.annotation.Encrypt;
import com.wts.common.exception.BizException;
import com.wts.utils.Sm4Util;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

/**
 * @description: post请求的加密参数进行解密,返回一个JSONObject对象
 * @Author: wts
 * @Date: 2022/11/7 11:10
 * @Version 1.0
 */
@ControllerAdvice(basePackages = "com.shensu.controller")
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {

    private Logger log = LoggerFactory.getLogger(DecryptRequestBodyAdvice.class);

    private final HttpSession session;

    public DecryptRequestBodyAdvice(HttpSession session) {
        this.session = session;
    }

    /**
     * 该方法用于判断当前请求,是否要执行beforeBodyRead方法
     * methodParameter: 方法的参数对象
     * type: 方法的参数类型
     * aClass: 将会使用到的Http消息转换器类类型
     * 注意:此判断方法,会在beforeBodyRead 和 afterBodyRead方法前都触发一次。
     *
     * @return 返回true则会执行beforeBodyRead
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    /**
     * 在Http消息转换器执转换,之前执行
     * inputMessage: 客户端的请求数据
     * methodParameter: 方法的参数对象
     * type: 方法的参数类型
     * aClass: 将会使用到的Http消息转换器类类型
     *
     * @return 返回 一个自定义的HttpInputMessage
     */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        try {
            boolean encode = false;
            // 判断该方法是否含有@Encrypt注解
            if (methodParameter.getMethod().isAnnotationPresent(Encrypt.class)) {
                //获取注解配置的包含和去除字段
                Encrypt serializedField = methodParameter.getMethodAnnotation(Encrypt.class);
                //入参是否需要解密
                encode = serializedField.in();
            }
            if (encode) {
                // 解密-使用解密后的数据,构造新的读取流
                return new MyHttpInputMessage(inputMessage, type);
            } else {
                return inputMessage;
            }
        } catch (Exception e) {
            log.error("请求参数错误:{}", e.getMessage(), e);
            throw new BizException("请求参数错误!");
        }
    }

    /**
     * 在Http消息转换器执转换,之后执行
     * o: 转换后的对象
     * httpInputMessage: 客户端的请求数据
     * methodParameter: handler方法的参数类型
     * type: handler方法的参数类型
     * aClass: 使用的Http消息转换器类类型
     *
     * @return 返回一个新的对象
     */
    @Override
    public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return o;
    }

    /**
     * 参数与afterBodyRead相同,不过这个方法处理的是,body为空的情况
     */
    @Override
    public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return o;
    }

    /**
     * 解密-使用解密后的数据,构造新的读取流
     */
    class MyHttpInputMessage implements HttpInputMessage {
        private HttpHeaders headers;

        private InputStream body;

        public MyHttpInputMessage(HttpInputMessage inputMessage, Type type) throws Exception {
            this.headers = inputMessage.getHeaders();
            String bodyStr = StringUtils.defaultString(IOUtils.toString(inputMessage.getBody(), "UTF-8"));
            try {
                // SM4解密
                String decodeParameters = Sm4Util.decryptEcb(bodyStr);
                // Feature.OrderedField:解析时增加参数不调整顺序
                JSONObject decodeParaJson = JSON.parseObject(decodeParameters, Feature.OrderedField);
                if (decodeParaJson != null) {
                    this.body = IOUtils.toInputStream(decodeParameters, "UTF-8");
                    return;
                }
                this.body = null;
            } catch (Exception e) {
                log.error("加密参数【{}】解密失败:{}", bodyStr, e.getMessage(), e);
                throw new BizException(e.getMessage());
            }
        }

        @Override
        public InputStream getBody() {
            return body;
        }

        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }

    }
}
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.wts.common.annotation.Encrypt;
import com.wts.utils.Sm4Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description 返回数据加密
 * 实现ResponseBodyAdvice接口,其实是对加了@RestController(也就是@Controller+@ResponseBody)注解的处理器将要返回的值进行增强处理。其实也就是采用了AOP的思想,对返回值进行一次修改。
 * @Author: wts
 * @Date: 2022/11/7 11:10
 * @Version 1.0
 */
@ControllerAdvice(basePackages = "com.shensu.controller")
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice {

    private Logger log = LoggerFactory.getLogger(EncryptResponseBodyAdvice.class);

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }

    /**
     * 原controller要返回的内容
     *
     * @param body
     * @param methodParameter
     * @param mediaType
     * @param aClass
     * @param serverHttpRequest
     * @param serverHttpResponse
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        Method method = methodParameter.getMethod();
        if (method.isAnnotationPresent(Encrypt.class)) {
            //获取注解配置的包含和去除字段
            Encrypt serializedField = methodParameter.getMethodAnnotation(Encrypt.class);
            //出参是否需要加密
            if (serializedField != null && !serializedField.out()) {
                return body;
            }
        }
        try {
            Map<String, String> resultMap = new HashMap<>();
            String result = JSON.toJSONString(body, SerializerFeature.DisableCircularReferenceDetect);
            // 判断使用哪种加密方式进行加密
            HttpServletRequest req = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
            Object encryptType = req.getAttribute("encryptType");
            if (encryptType != null && "aes".equals(encryptType.toString())) {
                //对返回json数据进行AES加密
                String returnStr = AesUtil.encrypt(result);
               	String sign = MD5Util.encode((returnStr + MD5Util.MD5_KEY));
                resultMap.put("sign", sign);
                resultMap.put("result", returnstr);
            } else {
                // 对返回json数据进行SM4加密
                String sign = MD5Util.encode((returnStr + MD5Util.MD5_KEY));
                String returnStr = Sm4Util.encryptEcb(result);
                resultMap.put("sign", sign);
                resultMap.put("result", returnStr);
            }
            return resultMap;
        } catch (Exception e) {
            log.error("对方法method:{}返回数据进行解密出现异常:{}", methodParameter.getMethod().getName(), e.getMessage(), e);
        }
        return body;
    }
}

3.加解密工具类

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class Sm4Util {
    private static final Logger log = LoggerFactory.getLogger(Sm4Util.class);

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 密钥
     */
    private static final String S_KEY = "6d476239336e624a6966383135346441";
    /**
     * 编码格式
     */
    private static final String ENCODING = "UTF-8";
    /**
     * 算法名称
     */
    public static final String ALGORITHM_NAME = "SM4";
    /**
     * 定义分组加密模式使用:PKCS5Padding
     */
    public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
    /**
     * 128-32位16进制;256-64位16进制
     */
    public static final int DEFAULT_KEY_SIZE = 128;

    /**
     * 获取加密参数解密之后的数据
     *
     * @param params
     * @return
     */
    public static Map<String, Object> getParams(String params) {
        Map info = new HashMap<String, Object>();
        try {
            if (StringUtils.isNotBlank(params)) {
                String strValue = Sm4Util.decryptEcb(params);
                info = JSONObject.parseObject(strValue, Map.class);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return info;
    }

    //密钥****************************************

    /**
     * 系统产生秘钥
     *
     * @param keySize
     * @return
     * @throws Exception
     * @explain
     */
    public static byte[] generateKey(int keySize) throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }

    /**
     * 自动生成密钥
     *
     * @return
     * @explain
     */
    public static byte[] generateKey() throws Exception {
        return generateKey(DEFAULT_KEY_SIZE);
    }

    /**
     * 返回String类型密钥
     *
     * @return
     * @throws Exception
     */
    public static String generateKeyString() throws Exception {
        return ByteUtils.toHexString(generateKey());
    }


    //SM4解密=================================================

    /**
     * sm4解密
     *
     * @param cipherText 16进制的加密字符串(忽略大小写)
     * @return 解密后的字符串
     * @explain 解密模式:采用ECB
     */
    public static String decryptEcb(String cipherText) throws Exception {
        // 用于接收解密后的字符串
        String decryptStr = "";
        // 用于接收解密后的字符串
        // hexString-->byte[]
        byte[] keyData = ByteUtils.fromHexString(S_KEY);
        // hexString-->byte[]
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
        // byte[]-->String
        decryptStr = new String(srcData, ENCODING);
        return decryptStr;
    }

    /**
     * sm4解密
     *
     * @param key        16进制密钥
     * @param cipherText 16进制的加密字符串(忽略大小写)
     * @return 解密后的字符串
     * @explain 解密模式:采用ECB
     */
    public static String decryptEcb(String key, String cipherText) throws Exception {
        // 用于接收解密后的字符串
        String decryptStr = "";
        byte[] keyData = ByteUtils.fromHexString(key);
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
        decryptStr = new String(srcData, ENCODING);
        return decryptStr;
    }

    /**
     * 解密
     *
     * @param key
     * @param cipherText
     * @return
     * @throws Exception
     * @explain
     */
    public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(cipherText);
    }

    /**
     * 生成ECB暗号
     *
     * @param algorithmName 算法名称
     * @param mode          模式
     * @param key
     * @return
     * @throws Exception
     * @explain ECB模式(电子密码本模式:Electronic codebook)
     */
    private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
        Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
        cipher.init(mode, sm4Key);
        return cipher;
    }

    //SM4加密=================================================

    /**
     * sm4加密
     *
     * @param paramsStr 待加密字符串
     * @return 返回16进制的加密字符串
     * @throws Exception
     * @explain 加密模式:ECB 密文长度不固定,会随着被加密字符串长度的变化而变化
     */
    public static String encryptEcb(String paramsStr) throws Exception {
        return encryptEcb(S_KEY, paramsStr);
    }

    /**
     * sm4加密
     *
     * @param hexKey   16进制密钥(忽略大小写)
     * @param paramStr 待加密字符串
     * @return 返回16进制的加密字符串
     * @throws Exception
     * @explain 加密模式:ECB 密文长度不固定,会随着被加密字符串长度的变化而变化
     */
    public static String encryptEcb(String hexKey, String paramStr) throws Exception {
        String cipherText = "";
        // 16进制字符串-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // String-->byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 加密后的数组
        byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
        // byte[]-->hexString
        cipherText = ByteUtils.toHexString(cipherArray);
        return cipherText;
    }

    /**
     * 加密模式之Ecb
     *
     * @param key
     * @param data
     * @return
     * @throws Exception
     */
    public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(data);
    }

    //校验****************************************
    /**
     * 校验加密前后的字符串是否为同一数据
     * @explain
     * @param hexKey 16进制密钥(忽略大小写)
     * @param cipherText 16进制加密后的字符串
     * @param paramStr 加密前的字符串
     * @return 是否为同一数据
     * @throws Exception
     */
    public static boolean verifyEcb(String hexKey, String cipherText, String paramStr) throws Exception {
        // 用于接收校验结果
        boolean flag = false;
        // hexString-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // 将16进制字符串转换成数组
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] decryptData = decrypt_Ecb_Padding(keyData, cipherData);
        // 将原字符串转换成byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 判断2个数组是否一致
        flag = Arrays.equals(decryptData, srcData);
        return flag;
    }

    public static void main(String[] args) throws Exception {
        // 获取参数
        Map<String, Object> data = Sm4Util.getParams("3503be9157de9a12d23a028311c44221f32a014d61a4148b1ddfc04a02d425cf622e06e24388eb4ec01089a2e461fa2a");
        System.out.println("获取加密参数解密之后的数据:" + data);
        // 自动生成密钥
        String secretKey = generateKeyString();
        System.out.println("自动生成密钥:" + secretKey);
        // 加密
        String encryptEcb = Sm4Util.encryptEcb("{\n" +
                "  \"id\": 11\n" +
                "}");
        System.out.println("加密结果:" + encryptEcb);
        // 解密
        String decrypt = Sm4Util.decryptEcb("3503be9157de9a12d23a028311c44221f32a014d61a4148b1ddfc04a02d425cf622e06e24388eb4ec01089a2e461fa2a");
        System.out.println("解密结果:" + decrypt);
    }
}