springboot修改接口入参出参实现入参加密出参解密

一、背景

针对项目已经开发完的接口,都需要加上传输数据加密的功能,对接口入参进行AES解密,对接口出参进行加密。考虑到尽量改动少点,使用自定义注解结合springmvc里的RequestBodyAdvice和ResponseBodyAdvice两个类进行实现。

RequestBodyAdvice允许针对接口请求体被读取之前进行修改,ResponseBodyAdvice允许接口出参在被返回之前进行修改。

二、实现

1、新建两个自定义注解类,用来标记哪些接口需要进行加密解密。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Documented
public @interface Decrypt {
}

注意:@Decrypt配置的作用域是方法和参数上,@Encrypt则是只在方法上。

2、新建自定义DecryptRequestAdvice类继承RequestBodyAdviceAdapter,进行入参解密

 

import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.client.utils.JSONUtils;
import com.google.common.base.Throwables;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
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.RequestBodyAdviceAdapter;

import java.lang.reflect.Type;

/**
 * @Author: 夏威夷8080
 * @Date: 2000/7/8 20:28
 */
@ControllerAdvice
@Slf4j
public class DecryptRequestAdvice extends RequestBodyAdviceAdapter {

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        try {
            String requestBody = JSONUtils.serializeObject(body);
            if (JSONObject.isValid(requestBody)) {
                JSONObject jsonObject = JSONObject.parseObject(requestBody);
                String encryptData = jsonObject.getString("encryptData");
                if (StringUtils.isBlank(encryptData)) {
                    throw new IllegalArgumentException("缺少加密内容!");
                }
                log.info("接口解密入参数据");
                String decryptData = new AesUtil().decryptByHex(encryptData);
//            String decryptData = Base64.decodeStr(encryptData);
//                log.info("接口解密后的入参:{}", decryptData);
                body = JSONObject.parseObject(decryptData, targetType);
            } else {
//                log.error("获取到的入参不是合法的json格式!");
                throw new IllegalArgumentException("获取到的入参不是合法的json格式!");
            }
        } catch (Exception e) {
            log.error("接口入参解密出错:{}", Throwables.getStackTraceAsString(e));
        }
        return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
    }
}

 

 

3、新建自定义EncryptResponseAdvice类继承ResponseBodyAdvice,进行出参加密

这里的R是自定义的接口返回封装类

 

import com.alibaba.nacos.client.utils.JSONUtils;
import com.google.common.base.Throwables;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
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;

/**
 * @Author: 夏威夷8080
 * @Date: 2000/7/8 19:55
 */
@ControllerAdvice
@Slf4j
public class EncryptResponseAdvice implements ResponseBodyAdvice<R> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.hasMethodAnnotation(Encrypt.class);
    }

    @Override
    public R beforeBodyWrite(R body, MethodParameter returnType, MediaType selectedContentType,
                             Class<? extends HttpMessageConverter<?>> selectedConverterType,
                             ServerHttpRequest request, ServerHttpResponse response) {

        try {
            if (body.getData() != null) {
                log.info("接口加密返回的数据");
//                log.info("接口加密前返回的数据:{}", JSONUtils.serializeObject(body.getData()));
                String encStr = new AesUtil().encryptByHex(JSONUtils.serializeObject(body.getData()));
//                String encStr = Base64.encode(JSONUtil.toJsonStr(body.getData()));
//                log.info("接口加密后返回的数据:{}", encStr);
                body.setData(encStr);
            }
        } catch (Exception e) {
            log.error("接口返回数据加密出错:{}", Throwables.getStackTraceAsString(e));
        }
        return body;
    }
}

  

4、controller接口

@PostMapping("/test")
    @ApiOperation(value = "测试接口加密解密")
    @Encrypt
    public R<UserInfoDTO> test(@Decrypt @RequestBody @Valid QueryVO vo) {
        UserInfoDTO convert = Convert.convert(UserInfoDTO.class, vo);
return new R(convert); }

5、AES加解密工具类

 

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.HexUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.util.Arrays;

/**
 */
@Component
public class AesUtil {
    /**
     * @author ngh
     * AES128 算法
     * <p>
     * CBC 模式
     * <p>
     * PKCS7Padding 填充模式
     * <p>
     * CBC模式需要添加一个参数iv
     * <p>
     * 介于java 不支持PKCS7Padding,只支持PKCS5Padding 但是PKCS7Padding 和 PKCS5Padding 没有什么区别
     * 要实现在java端用PKCS7Padding填充,需要用到bouncycastle组件来实现
     */
    private Key key;
    private Cipher cipher;
    boolean isInited = false;

    String aesKey = "0325mlm2022";
    byte[] iv = "0103021405060878".getBytes();
    byte[] keyBytes = aesKey.getBytes();

    public void init(byte[] keyBytes) {
        // 如果密钥不足16位,那么就补足.  这个if 中的内容很重要
        int base = 16;
        if (keyBytes.length % base != 0) {
            int groups = keyBytes.length / base + (keyBytes.length % base != 0 ? 1 : 0);
            byte[] temp = new byte[groups * base];
            Arrays.fill(temp, (byte) 0);
            System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length);
            keyBytes = temp;
        }
        // 初始化
        Security.addProvider(new BouncyCastleProvider());
        // 转化成JAVA的密钥格式
        key = new SecretKeySpec(keyBytes, CipherType.AES_ALGORITHM);
        try {
            // 初始化cipher
            cipher = Cipher.getInstance(CipherType.AES_CBC_PKC7PADDING, "BC");
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchProviderException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }


    public String encrypt(String content) {
        return Base64.encode(encrypt(content.getBytes(), keyBytes));
    }

    public String encrypt(String content, String keyBytes) {
        return Base64.encode(encrypt(content.getBytes(), keyBytes.getBytes()));
    }

    public String encryptByHex(String content, String keyBytes) {
        return HexUtil.encodeHexStr(encrypt(content.getBytes(), keyBytes.getBytes()));
    }

    public String encryptByHex(String content) {
        return HexUtil.encodeHexStr(encrypt(content.getBytes(), keyBytes));
    }

    /**
     * 加密方法
     *
     * @param content  要加密的字符串
     * @param keyBytes 加密密钥
     * @return
     */
    public byte[] encrypt(byte[] content, byte[] keyBytes) {
        byte[] encryptedText = null;
        keyBytes = new String(keyBytes).getBytes();
        init(keyBytes);
        try {
            cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
            encryptedText = cipher.doFinal(content);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return encryptedText;
    }

    public String decrypt(String encryptedData) {
        return new String(decrypt(Base64.decode(encryptedData), keyBytes));
    }

    public String decrypt(String encryptedData, String keyData) {
        return new String(decrypt(Base64.decode(encryptedData), keyData.getBytes()));
    }

    public String decryptByHex(String encryptedData, String keyData) {
        return new String(decrypt(HexUtil.decodeHex(encryptedData), keyData.getBytes()));
    }

    public String decryptByHex(String encryptedData) {
        return new String(decrypt(HexUtil.decodeHex(encryptedData), keyBytes));
    }

    /**
     * 解密方法
     *
     * @param encryptedData 要解密的字符串
     * @param keyBytes      解密密钥
     * @return
     */
    public byte[] decrypt(byte[] encryptedData, byte[] keyBytes) {
        byte[] encryptedText = null;
        init(keyBytes);
        try {
            cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
            encryptedText = cipher.doFinal(encryptedData);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return encryptedText;
    }


    public static void main(String[] args) throws IOException {

        AesUtil aes = new AesUtil();
        //加密字符串
        String content = "{\n" +
                "\"start\":\"2022-07-20 08:00:35\",\n" +
                "\"end\":\"2022-07-20 16:00:37\",\n" +
                "  \"page\": 1,\n" +
                "  \"pageSize\": 10\n" +
                "}";

        System.out.println("加密前的:" + content);
        // 加密方法
        String encStr = aes.encryptByHex(content);
        System.out.println("加密后的内容:" + encStr);
        // 解密方法
        String decStr = aes.decryptByHex("7807418b6840a6");
        System.out.println("解密后的内容:" + decStr);


    }
}

 

6、常量类 

/**
 * @Version: 1.0
 */
public class CipherType {
    //MD5
    public final static String MD5 = "MD5";

    //sha
    public final static String SHA_1 = "SHA-1";
    public final static String SHA_256 = "SHA-256";

    //HMAC
    public final static String HMAC_SHA_1 = "HmacSHA1";
    public final static String HMAC_SHA_256 = "HmacSHA256";

    //AES
    public final static String AES_ALGORITHM = "AES";
    public final static String AES_CBC_PKC5PADDING = "AES/CBC/PKCS5Padding";
    public final static String AES_CBC_PKC7PADDING = "AES/CBC/PKCS7Padding";
    public final static String AES_ECB_PKC7PADDING = "AES/ECB/PKCS7Padding";
    public final static String AES_CBC_NODDING = "AES/CBC/NoPadding"; //NoPadding非填充,明文必须是16的整数倍

    public final static String AES_ECB_PKC5PADDING = "AES/ECB/PKCS5Padding"; //ECB模式,IV不要填
    public final static String AES_ECB_NODDING = "AES/ECB/NoPadding";

    //RSA
    public final static String RSA = "RSA";

    //RSA加密算法
    public final static String RSA_ECB_PSCS1PADDING = "RSA/ECB/PKCS1Padding";
//
//    public final static String RSA_ECB_PSCS1PADDING = "RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING";

    //签名算法
    public final static String SHA256_RSA = "SHA256withRSA";


}

  

 

三、原理说明

上面那两个advice类型,都要使用@ControllerAdvice注解进行修饰,它其实是一个实现特殊功能的@Component,只是针对controller进行拦截,本质还是aop,我们平常使用的全局异常处理类@ExceptionHandler,GlobalExceptionHandler,也是配合该注解使用。

另外这边入参拦截修改,只针对@RequestBody修饰的body进行处理,同时返回一样,要被@ResponseBody修饰,如果你使用的是@RestController,那就不需要再加了。

四、问题解决

参见《SpringMvc里的RequestBodyAdviceAdapter使用问题

posted @ 2022-07-10 10:22  夏威夷8080  阅读(3290)  评论(0编辑  收藏  举报