SpringBoot项目实战 - SpringBoot实现请求参数与响应参数加解密

一、需求

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

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

二、设计

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

RequestBodyAdviceResponseBodyAdvice
在这里插入图片描述

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);
}

在这里插入图片描述

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);
			
}

三、实现

在这里插入图片描述

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 接口

DecryptRequestBodyAdvice类

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.wts.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;
        }

    }
}

EncryptResponseBodyAdvice类

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.wts.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.加解密工具类

Sm4Util类

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--&gt;byte[]
        byte[] keyData = ByteUtils.fromHexString(S_KEY);
        // hexString--&gt;byte[]
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
        // byte[]--&gt;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进制字符串--&gt;byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // String--&gt;byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 加密后的数组
        byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
        // byte[]--&gt;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);
    }
}

SM2Util类

import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.asymmetric.SM2;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SM2Util {
    private static final byte [] interfaceId= HexUtil.decodeHex("6C415A32626679784574694655734E3970414D63");
    private static final byte [] appId=HexUtil.decodeHex("686665303376646965365574574266386B647457");

    private static final ThreadLocal<SM2> THREAD_INTERFACE = ThreadLocal.withInitial(() -> {
        SM2 interfaceSm2=new SM2("B0FEE4FADB1E599598B23EB3CEC1FA60A420401E98FBE426C0A35AB5F4474ABA", null, null);
        interfaceSm2.usePlainEncoding();
        return interfaceSm2;
    });
    private static final ThreadLocal<SM2> THREAD_APP = ThreadLocal.withInitial(() -> {
        SM2 appSm2=new SM2("875C855B8873E38D16E1E39774F0AC20034CD68A0BB34D334B105163B01FAD21","6014DED6EA507518C971C0B593DE886EFA34A52BFD47094DAB8006890FAABA4F", "02E608B3A330E376671050CFD4B9E22CF24BB2CFE239D941AC8CFE5257180C76");
        appSm2.usePlainEncoding();
        return appSm2;
    });

    public static String sign(String data){
        byte [] sign=THREAD_INTERFACE.get().sign(data.getBytes(CharsetUtil.CHARSET_UTF_8),interfaceId);
        return HexUtil.encodeHexStr(sign);
    }

    public static boolean verify(JSONObject data){
        String sign=data.getString("sign");
        if(StringUtils.isBlank(sign)){
            return false;
        }
        return THREAD_APP.get().verify(getSignCheckContent(data).getBytes(CharsetUtil.CHARSET_UTF_8),HexUtil.decodeHex(sign),appId);
    }

    public static void signAppData(JSONObject data){
        byte [] sign= THREAD_APP.get().sign(getSignCheckContent(data).getBytes(CharsetUtil.CHARSET_UTF_8),appId);
        data.put("sign",HexUtil.encodeHexStr(sign));
    }

    public static String getSignCheckContent(JSONObject params) {
        if (params == null) {
            return null;
        } else {
            params.remove("sign");
            StringBuilder content = new StringBuilder();
            List<String> keys = new ArrayList(params.keySet());
            Collections.sort(keys);

            for(int i = 0; i < keys.size(); ++i) {
                String key = keys.get(i);
                String value = String.valueOf(params.get(key));
                content.append((i == 0 ? "" : "&") + key + "=" + value);
            }
            return content.toString();
        }
    }
}

MD5Util类

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {

    public static final String MD5_KEY = "2~s^}0app1[md5s7";

    public final static String encode(String s) throws NoSuchAlgorithmException {
        char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        byte[] btInput = s.getBytes();
        MessageDigest mdInst = MessageDigest.getInstance("MD5");// 获得MD5摘要算法的 MessageDigest 对象
        mdInst.update(btInput);// 使用指定的字节更新摘要
        byte[] md = mdInst.digest();// 获得密文
        int j = md.length;// 把密文转换成十六进制的字符串形式
        char str[] = new char[j * 2];
        int k = 0;
        for (int i = 0; i < j; i++) {
            byte byte0 = md[i];
            str[k++] = hexDigits[byte0 >>> 4 & 0xf];
            str[k++] = hexDigits[byte0 & 0xf];
        }
        return new String(str).toLowerCase();
    }

    public final static String encode(String s, String charset) throws Exception {
        char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        byte[] btInput = s.getBytes(charset);
        MessageDigest mdInst = MessageDigest.getInstance("MD5");// 获得MD5摘要算法的 MessageDigest 对象
        mdInst.update(btInput);// 使用指定的字节更新摘要
        byte[] md = mdInst.digest();// 获得密文
        int j = md.length;// 把密文转换成十六进制的字符串形式
        char str[] = new char[j * 2];
        int k = 0;
        for (int i = 0; i < j; i++) {
            byte byte0 = md[i];
            str[k++] = hexDigits[byte0 >>> 4 & 0xf];
            str[k++] = hexDigits[byte0 & 0xf];
        }
        return new String(str).toLowerCase();
    }

    public static void main(String[] args) throws NoSuchAlgorithmException {
        String secret001 = MD5Util.encode("secret001");
        System.out.println(secret001);
        System.out.println("2a705a8a91df2f6ee75aff7e2ae85567");
    }

}

4. 解决在ResponseBodyAdvice.beforeBodyWrite()方法种进行加密多了一层引号问题

在处理字符串时,特别是涉及到编码和传输的情况下,将其转换为字节数组是一种常见的做法,有以下几个原因:

  1. 字符编码一致性:将字符串转换为字节数组时,可以明确指定字符编码,确保数据按照指定的编码进行处理。这样可以避免在不同环境中出现编码不一致导致的问题,比如乱吗或格式错误。
  2. 数据传输:在网络传输或文件存储时,数据通常是以字节流的形式进行传输的。将字符串转换为字节数组后,可以更方便地通过输出流写入到网络连接或文件中,确保数据的完整传输。
  3. 数据处理效率:直接操作字节数组通常比操作字符串更高效。在处理大量数据时,直接操作字节数组可以提高效率,减少不必要的内存开销。
  4. 数据安全性:对敏感数据进行加密后,通常会将加密后的数据以字节流的形式进行处理和传输,而不是直接以字符串形式暴露在外。这样可以增加数据的安全性,避免数据在传输过程中被窃取或篡改。

综上所述,将加密后的字符串转换为字节数组,并通过字节流写入输出流,可以更好地保证数据的完整性、一致性和安全性,同时确保在写入过程中不会引入额外的引号或其他格式问题。这种做法是一种通用的最佳实践,特别适用于处理敏感数据或涉及编码和传输的场景。希望这些解释能帮助你理解为什么要将字符串转换为字节数组再写入输出流。如果有任何疑问,请随时提出。

@Slf4j
@ControllerAdvice
public class ApiResponseWrapper implements ResponseBodyAdvice<Object> {

    @Resource
    private ConfigValue configValue;

    /**
     * 确定是否支持给定的返回类型和转换器类型
     *
     * @param returnType     返回类型
     * @param converterType  转换器类型
     * @return 如果支持该返回类型和转换器类型,则返回true;否则返回false
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

     /**
     * 对返回结果进行处理,加密后再写入输出流
     *
     * @param body                  待处理的返回结果对象
     * @param returnType            返回类型
     * @param selectedContentType   选择的内容类型
     * @param selectedConverterType 选定的转换器类型
     * @param request               HTTP请求
     * @param response              HTTP响应
     * @return 处理后的返回结果对象,如果需要加密返回则返回null
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<?
            extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
                                  ServerHttpResponse response) {
        if (Boolean.TRUE.equals(configValue.getRespEncrypt())) {
            // 加密返回
            try (OutputStream stream = response.getBody()) {
                // 将加密后的字符串写入输出流之前,先将其转换为字节数组,然后再写入输出流。这样可以确保不会在写入过程中引入额外的引号或其他格式问题。
                stream.write(AESUtil.encrypt(JSONObject.toJSONString(body)).getBytes("utf-8"));
                stream.flush();
            } catch (IOException e) {
                log.error("beforeBodyWrite stream write error", e);
            }
            return null;
        } else {
            // 不加密返回
            return body;
        }
    }
}

AES加密工具类

import com.wts.common.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
@Slf4j

public class AESUtil {

    private static final String ALGORITHM_AES128 = "AES";

    private static String key = "wts2020@ssabc";

    /**
     * 加密算法
     */
    public static String encrypt(String source) {
        return encrypt(source, key);
    }

    /**
     * 解密算法
     */
    public static String decrypt(String sSrc) {
        return decrypt(sSrc, key);
    }

    /**
     * 加密算法
     */
    public static String encrypt(String sSrc, String sKey) {
        try {
            byte[] raw = sKey.getBytes(StandardCharsets.UTF_8);
            SecretKeySpec sKeySpec = new SecretKeySpec(raw, "AES");
            Cipher cipher = Cipher.getInstance(ALGORITHM_AES128);
            cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
            byte[] encrypted = cipher.doFinal(sSrc.getBytes(StandardCharsets.UTF_8));
            return new BASE64Encoder().encode(encrypted);
        } catch (Exception e) {
            log.error("加密数据异常:{}", e.getMessage(), e);
            throw new BizException("加密数据异常");
        }

    }

    /**
     * 解密算法
     */
    public static String decrypt(String sSrc, String sKey) {
        if (sSrc == null) {
            return null;
        }
        try {
            byte[] raw = sKey.getBytes(StandardCharsets.UTF_8);
            SecretKeySpec sKeySpec = new SecretKeySpec(raw, "AES");
            Cipher cipher = Cipher.getInstance(ALGORITHM_AES128);
            cipher.init(Cipher.DECRYPT_MODE, sKeySpec);

            BASE64Decoder decoder = new BASE64Decoder();
            byte[] encrypted1 = decoder.decodeBuffer(sSrc);
            byte[] original = cipher.doFinal(encrypted1);
            return new String(original, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("解密数据异常:{}", e.getMessage(), e);
            throw new BizException("解密数据异常");
        }
    }
}

统一业务异常类

public class BizException extends RuntimeException {

    protected Integer errorCode;

    protected String errorMsg;

    public BizException() {
        super();
    }

    public BizException(String errorMsg) {
        super(errorMsg);
        this.errorMsg = errorMsg;
    }

    public BizException(Integer errorCode, String errorMsg) {
        super(errorCode + ":" + errorMsg);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public BizException(Integer errorCode, String errorMsg, Throwable cause) {
        super(errorMsg, cause);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(Integer errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
}
  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值