AOP实现接口入参、出参加解密功能

借助Spring的AOP和注解,实现对业务接口请求参数和响应参数的加密、解密功能

一、后端部分

1、注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Secret
{
    // 参数类(用来传递加密数据,只有方法参数中有此类或此类的子类才会执行加解密)
    Class value();

    // 参数类中传递加密数据的属性名,默认encryptStr
    String encryptStrName() default "encryptStr";
}

2、切面类

import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.annotation.Secret;
import com.ruoyi.common.core.domain.AjaxResult;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Type;

/**
 * 接口入参、回参加解密
 *
 * @author 逆行
 * @date 2024/02/20 15:22
 **/
@Aspect
@Component
public class SecretAspect
{

    // 是否进行加密解密,通过配置文件注入(不配置默认为true)
    @Value("${isSecret:true}")
    boolean isSecret;

    // 定义切点,使用了@Secret注解的类 或 使用了@Secret注解的方法
    @Pointcut("@within(com.ruoyi.common.annotation.Secret) || @annotation(com.ruoyi.common.annotation.Secret)")
    public void pointcut()
    {
    }

    // 环绕切面
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point)
    {
        AjaxResult result = null;
        // 获取被代理方法参数
        Object[] args = point.getArgs();
        // 获取被代理对象
        Object target = point.getTarget();
        // 获取通知签名
        MethodSignature signature = (MethodSignature) point.getSignature();

        try
        {
            // 获取被代理方法
            Method pointMethod = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
            // 获取被代理方法上面的注解@Secret
            Secret secret = pointMethod.getAnnotation(Secret.class);
            // 被代理方法上没有,则说明@Secret注解在被代理类上
            if (secret == null)
            {
                secret = target.getClass().getAnnotation(Secret.class);
            }

            if (secret != null)
            {
                // 获取注解上声明的加解密类
                Class clazz = secret.value();
                // 获取注解上声明的加密参数名
                String encryptStrName = secret.encryptStrName();

                for (int i = 0; i < args.length; i++)
                {
                    // 如果是clazz类型则说明使用了加密字符串encryptStr传递的加密参数
                    if (clazz.isInstance(args[i]))
                    {
                        Object cast = clazz.cast(args[i]);      //将args[i]转换为clazz表示的类对象
                        // 通过反射,执行getEncryptParam()方法,获取加密数据
                        Method method = clazz.getMethod(getMethodName(encryptStrName));
                        // 执行方法,获取加密数据
                        String encryptStr = (String) method.invoke(cast);
                        // 加密字符串是否为空
                        if (StringUtils.isNotBlank(encryptStr))
                        {
                            // 解密
                            String json = DesUtils.decrypt(encryptStr);
                            // 转换vo
                            args[i] = JSON.parseObject(json, (Type) args[i].getClass());
                        }
                    }
                    // 其他类型,比如基本数据类型、包装类型就不使用加密解密了
                }
            }

            // 执行请求
            result = (AjaxResult) point.proceed(args);

            // 判断配置是否需要返回加密
            if (isSecret)
            {
                // 获取返回值json字符串
                String jsonString = JSON.toJSONString(result.get("data"));
                // 加密
                String s = DesUtils.encrypt(jsonString);
                result.put("data", s);
            }

        } catch (Throwable e)
        {
            throw new RuntimeException(e);
        }
        return result;
    }

    /**
     * 转化方法名
     *
     * @param name
     * @return
     */
    private String getMethodName(String name)
    {
        String first = name.substring(0, 1);
        String last = name.substring(1);
        first = StringUtils.upperCase(first);
        return "get" + first + last;
    }

}

3、加解密工具类

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * 加解密工具类
 *
 * @Author: 逆行
 * @DATE: 2024/2/27
 */
public class DesUtils
{
    private final static String ALGORITHM = "ALGORITHM";// 算法
    private final static String CHARSET = "UTF-8";// 字符集编码
    private final static String DEFAULT_KEY = "www.gzjxlcit.com";// 秘钥


    /**
     * @Title 使用 默认key 加密
     * @Author 逆行
     * @Date 2020/5/21 17:02
     * @versioin V1.0
     **/
    public static String encrypt(String data) throws Exception
    {
        byte[] bt = encrypt(data.getBytes(CHARSET), DEFAULT_KEY.getBytes(CHARSET));
        return Base64.getEncoder().encodeToString(bt);
    }

    /**
     * @Title 使用 默认key 解密
     * @Author 逆行
     * @Date 2020/5/21 17:02
     * @versioin V1.0
     **/
    public static String decrypt(String data) throws Exception
    {
        if (data == null)
        {
            return null;
        }
        byte[] buf = Base64.getDecoder().decode(data);
        byte[] bt = decrypt(buf, DEFAULT_KEY.getBytes(CHARSET));
        return new String(bt, CHARSET);
    }

    /**
     * @Title 根据键值进行加密
     * @Author 逆行
     * @Date 2020/5/21 17:03
     * @versioin V1.0
     **/
    public static String encrypt(String data, String key) throws Exception
    {
        byte[] bt = encrypt(data.getBytes(CHARSET), key.getBytes(CHARSET));
        return Base64.getEncoder().encodeToString(bt);
    }

    /**
     * @Title 根据键值进行解密
     * @Author 逆行
     * @Date 2020/5/21 17:03
     * @versioin V1.0
     **/
    public static String decrypt(String data, String key) throws Exception
    {
        if (data == null)
        {
            return null;
        }
        byte[] buf = Base64.getDecoder().decode(data);
        byte[] bt = decrypt(buf, key.getBytes(CHARSET));
        return new String(bt, CHARSET);
    }

    /**
     * @Title 根据键值进行加密
     * @Author 逆行
     * @Date 2020/5/21 17:04
     * @versioin V1.0
     **/
    private static byte[] encrypt(byte[] data, byte[] key) throws Exception
    {
        // 生成一个可信任的随机数源
        SecureRandom sr = new SecureRandom();

        // 从原始密钥数据创建DESKeySpec对象
        DESKeySpec dks = new DESKeySpec(key);

        // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
        SecretKey securekey = keyFactory.generateSecret(dks);

        // Cipher对象实际完成加密操作
        Cipher cipher = Cipher.getInstance(ALGORITHM);

        // 用密钥初始化Cipher对象
        cipher.init(Cipher.ENCRYPT_MODE, securekey, sr);

        return cipher.doFinal(data);
    }

    /**
     * @Title 根据键值进行解密
     * @Author 逆行
     * @Date 2020/5/21 17:04
     * @versioin V1.0
     **/
    private static byte[] decrypt(byte[] data, byte[] key) throws Exception
    {
        // 生成一个可信任的随机数源
        SecureRandom sr = new SecureRandom();

        // 从原始密钥数据创建DESKeySpec对象
        DESKeySpec dks = new DESKeySpec(key);

        // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
        SecretKey securekey = keyFactory.generateSecret(dks);

        // Cipher对象实际完成解密操作
        Cipher cipher = Cipher.getInstance(ALGORITHM);

        // 用密钥初始化Cipher对象
        cipher.init(Cipher.DECRYPT_MODE, securekey, sr);

        return cipher.doFinal(data);
    }
}

4、使用示例

@Encrypt(value = OuterChainCheckVo.class, encryptStrName = "encryptParam")
@PostMapping("/access/check")
public AjaxResult check(@RequestBody OuterChainCheckVo vo)
{
    
}
// OuterChainCheckVo对象中必须包含一个与@Encrypt注解中value属性值同名的属性,如本例中OuterChainCheckVo对象需要包含encryptParam属性

二、前端部分

1、依赖

import CryptoJS from 'crypto-js';

2、加解密方法

// 参数加密
encrypt(text) {
  var keyHex = CryptoJS.enc.Utf8.parse(key);
  var encrypted = CryptoJS.DES.encrypt(text, keyHex, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
  });
  return encrypted.toString();
},
// 参数解密
decrypt(ciphertext) {
  var keyHex = CryptoJS.enc.Utf8.parse(key);
  const bytes = CryptoJS.DES.decrypt(ciphertext, keyHex, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
  });
  const originalText = bytes.toString(CryptoJS.enc.Utf8);
  return originalText;
}

3、接口传参示例

{
    encryptParam:"加密后的请求参数对象JSON字符串"
}