基于SpringBoot的AES加密算法接口处理

2021-12-07 16:36:43 浏览数 (1)

Advanced Encryption Standard

Advanced Encryption Standard缩写:AES,译为高级加密标准。

AES是用于取代DES的对称加密算法,既然有对称加密,那么会有非对称加密,常见的非对称加密有RSA加密。

何谓对称和非对称?

对称加密即为只有一个公钥,数据加密者和数据解密者共有一个公钥,可使用公钥完成数据的加密和解密,密钥由双方商定共同保管。而非对称加密的密钥可分为公钥和私钥,私钥用于数据的加密,公钥用于数据的解密,公私钥的其中一方无法完成数据的加密和解密,且加密后的数据无法被反解密。

因此,对于安全性而言,显而易见的是非对称加密更加安全,但对称加密效率更高。

本篇文章的主要内容是AES对称加密。

AES加密过程

前置条件:

  • 明文P,待加密数据
  • 密钥K,分组密码,每16字节一个分组,用于设定加密轮数
  • AES加密函数(E)
  • AES解密函数 (D)
  • 密文C,经密钥K加密后的明文

设加密函数为E,则有

​ C = E(K,P)

代码语言:javascript复制
所以,密文是由明文P作为入参经过密钥K加密特定轮数得到。

设解密函数为D,则有

​ P = D (K, C)

​ 所以,密文解密是由密文C和密钥K作为解密入参经解密函数得到。

AES密钥可由Hex生成

代码语言:javascript复制
// 使用密钥生成器 KeyGenerator 生成的 16 字节随机密钥的 hex 字符串,使用时解 hex 得到二进制密钥
byte[] bytes = AesEncryptUtil.initKey();

String aesKey = Hex.encodeHexString(bytes);

SpringBoot整合AES加密步骤

  1. AesEncryptUtils
  2. 自定义注解
  3. 全局统一数据处理
  4. Controller中使用注解

SpringBoot整合AES加密

AesEncryptUtil.class

代码语言:javascript复制
/**
 * AES 加/解密工具类
 * 使用密钥时请使用 initKey() 方法来生成随机密钥
 * initKey 方法内部使用 java.crypto.KeyGenerator 密钥生成器来生成特定于 AES 算法参数集的随机密钥
 */
public class AesEncryptUtil {

    private AesEncryptUtil() {
    }

    /**
     * 密钥算法类型
     */
    public static final String KEY_ALGORITHM = "AES";

    /**
     * 密钥的默认位长度
     */
    public static final int DEFAULT_KEY_SIZE = 128;

    /**
     * 加解密算法/工作模式/填充方式
     */
    private static final String ECB_PKCS_5_PADDING = "AES/ECB/PKCS5Padding";
    public static final String ECB_NO_PADDING = "AES/ECB/NoPadding";

    public static String base64Encode(byte[] bytes) {
        return Base64.encodeBase64String(bytes);
    }

    public static byte[] base64Decode(String base64Code) {
        return Base64.decodeBase64(base64Code);
    }

    public static byte[] aesEncryptToBytes(String content, String hexAesKey) throws Exception {
        return encrypt(content.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(hexAesKey.toCharArray()));
    }

    public static String aesEncrypt(String content, String hexAesKey) throws Exception {
        return base64Encode(aesEncryptToBytes(content, hexAesKey));
    }

    public static String aesDecryptByBytes(byte[] encryptBytes, String hexAesKey) throws Exception {
        byte[] decrypt = decrypt(encryptBytes, Hex.decodeHex(hexAesKey.toCharArray()));
        return new String(decrypt, StandardCharsets.UTF_8);
    }

    public static String aesDecrypt(String encryptStr, String hexAesKey) throws Exception {
        return aesDecryptByBytes(base64Decode(encryptStr), hexAesKey);
    }


    /**
     * 生成 Hex 格式默认长度的随机密钥
     * 字符串长度为 32,解二进制后为 16 个字节
     *
     * @return String Hex 格式的随机密钥
     */
    public static String initHexKey() {
        return Hex.encodeHexString(initKey());
    }

    /**
     * 生成默认长度的随机密钥
     * 默认长度为 128
     *
     * @return byte[] 二进制密钥
     */
    public static byte[] initKey() {
        return initKey(DEFAULT_KEY_SIZE);
    }

    /**
     * 生成密钥
     * 128、192、256 可选
     *
     * @param keySize 密钥长度
     * @return byte[] 二进制密钥
     */
    public static byte[] initKey(int keySize) {
        // AES 要求密钥长度为 128 位、192 位或 256 位
        if (keySize != 128 && keySize != 192 && keySize != 256) {
            throw new RuntimeException("error keySize: "   keySize);
        }
        // 实例化
        KeyGenerator keyGenerator;
        try {
            keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("no such algorithm exception: "   KEY_ALGORITHM, e);
        }
        keyGenerator.init(keySize);
        // 生成秘密密钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 获得密钥的二进制编码形式
        return secretKey.getEncoded();
    }

    /**
     * 转换密钥
     *
     * @param key 二进制密钥
     * @return Key 密钥
     */
    private static Key toKey(byte[] key) {
        // 实例化 DES 密钥材料
        return new SecretKeySpec(key, KEY_ALGORITHM);
    }

    /**
     * 加密
     *
     * @param data 待加密数据
     * @param key  密钥
     * @return byte[] 加密的数据
     */
    public static byte[] encrypt(byte[] data, byte[] key) {
        return encrypt(data, key, ECB_PKCS_5_PADDING);
    }

    /**
     * 加密
     *
     * @param data            待加密数据
     * @param key             密钥
     * @param cipherAlgorithm 算法/工作模式/填充模式
     * @return byte[] 加密的数据
     */
    public static byte[] encrypt(byte[] data, byte[] key, final String cipherAlgorithm) {
        // 还原密钥
        Key k = toKey(key);
        try {
            Cipher cipher = Cipher.getInstance(cipherAlgorithm);
            // 初始化,设置为加密模式
            cipher.init(Cipher.ENCRYPT_MODE, k);

            // 发现使用 NoPadding 时,使用 ZeroPadding 填充
            if (ECB_NO_PADDING.equals(cipherAlgorithm)) {
                return cipher.doFinal(formatWithZeroPadding(data, cipher.getBlockSize()));
            }

            // 执行操作
            return cipher.doFinal(data);
        } catch (Exception e) {
            throw new RuntimeException("AES encrypt error", e);
        }
    }

    /**
     * 解密
     *
     * @param data 待解密数据
     * @param key  密钥
     * @return byte[] 解密的数据
     */
    public static byte[] decrypt(byte[] data, byte[] key) {
        return decrypt(data, key, ECB_PKCS_5_PADDING);
    }

    /**
     * 解密
     *
     * @param data            待解密数据
     * @param key             密钥
     * @param cipherAlgorithm 算法/工作模式/填充模式
     * @return byte[] 解密的数据
     */
    public static byte[] decrypt(byte[] data, byte[] key, final String cipherAlgorithm) {
        // 还原密钥
        Key k = toKey(key);
        try {
            Cipher cipher = Cipher.getInstance(cipherAlgorithm);
            // 初始化,设置为解密模式
            cipher.init(Cipher.DECRYPT_MODE, k);

            // 发现使用 NoPadding 时,使用 ZeroPadding 填充
            if (ECB_NO_PADDING.equals(cipherAlgorithm)) {
                return removeZeroPadding(cipher.doFinal(data), cipher.getBlockSize());
            }

            // 执行操作
            return cipher.doFinal(data);
        } catch (Exception e) {
            throw new RuntimeException("AES decrypt error", e);
        }
    }

    private static byte[] formatWithZeroPadding(byte[] data, final int blockSize) {
        final int length = data.length;
        final int remainLength = length % blockSize;

        if (remainLength > 0) {
            byte[] inputData = new byte[length   blockSize - remainLength];
            System.arraycopy(data, 0, inputData, 0, length);
            return inputData;
        }
        return data;
    }

    private static byte[] removeZeroPadding(byte[] data, final int blockSize) {
        final int length = data.length;
        final int remainLength = length % blockSize;
        if (remainLength == 0) {
            // 解码后的数据正好是块大小的整数倍,说明可能存在补 0 的情况,去掉末尾所有的 0
            int i = length - 1;
            while (i >= 0 && 0 == data[i]) {
                i--;
            }
            byte[] outputData = new byte[i   1];
            System.arraycopy(data, 0, outputData, 0, outputData.length);
            return outputData;
        }
        return data;
    }

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

        byte[] bytes = AesEncryptUtil.initKey();

        // 使用密钥生成器 KeyGenerator 生成的 16 字节随机密钥的 hex 字符串,使用时解 hex 得到二进制密钥
        String aesKey = Hex.encodeHexString(bytes);
        System.out.println(aesKey);

//        String aesKey = "d86d7bab3d6ac01ad9dc6a897652f2d2";

        String content = "你好";
        System.out.println("加密前:"   content);

        String encrypt = aesEncrypt(content, aesKey);
        System.out.println(encrypt.length()   ":加密后:"   encrypt);

        String decrypt = aesDecrypt(encrypt, aesKey);
        System.out.println("解密后:"   decrypt);
    }

}
编写自定义注解

SecurityParameter.class

代码语言:javascript复制
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Mapping
@Documented
public @interface SecurityParameter {
    /**
     * 入参是否解密,默认解密
     * @return
     */
    boolean inDecode() default true;

    /**
     * 出参是否加密,默认加密
     * @return
     */
    boolean outEncode() default true;
}

加解密请求体处理

定义全局处理: 请求:拿密文来请求(request)到接口时,我们先在请求到接口前做一步解密处理,即:DecodeRequestBodyAdvice 返回:接口数据在Controller返回之后,在请求体中(Response)对数据先进行加密处理用于脱敏,即 EncodeResponseBodyAdvice

DecodeRequestBodyAdvice.class

代码语言:javascript复制
@Slf4j
@ControllerAdvice(basePackages = "com.ltx.aes_demo.controller")
public class DecodeRequestBodyAdvice implements RequestBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        try {
            boolean encode = false;
            if (Objects.requireNonNull(methodParameter.getMethod()).isAnnotationPresent(SecurityParameter.class)) {
                //获取注解配置的包含和去除字段
                SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class);
                //入参是否需要解密
                encode = serializedField.inDecode();
            }
            if (encode) {
                log.info("对方法method :【"   methodParameter.getMethod().getName()   "】返回数据进行解密");
                return new MyHttpInputMessage(inputMessage);
            } else {
                return inputMessage;
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("对方法method :【"   Objects.requireNonNull(methodParameter.getMethod()).getName()   "】返回数据进行解密出现异常:"   e.getMessage());
            return inputMessage;
        }
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    class MyHttpInputMessage implements HttpInputMessage {
        private HttpHeaders headers;

        private InputStream body;

        public MyHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
            this.headers = inputMessage.getHeaders();
            this.body = IOUtils.toInputStream(AesEncryptUtil.aesDecrypt(easpString(IOUtils.toString(inputMessage.getBody(), StandardCharsets.UTF_8)), AesConstant.aesKey));
        }

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

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


        public String easpString(String requestData) {
            if (StringUtils.isNotBlank(requestData)) {
                String s = "{"requestData":";
                if (!requestData.startsWith(s)) {
                    throw new RuntimeException("参数【requestData】缺失异常!");
                } else {
                    int closeLen = requestData.length() - 1;
                    int openLen = "{"requestData":".length();
                    return StringUtils.substring(requestData, openLen, closeLen);
                }
            }
            return "";
        }
    }
}

请求的json格式为: {“requestData”:“0Wa795xrq/8Gp9pUKZcYgEOPreQvHXZJYhWoNH9bbuSfbTDjb65VdzHZEsx3FhRiysxU04KACg1gXFBla4WBggj5u4EjmVfUP3hotGOeY M=”}

EncodeResponseBodyAdvice.class

代码语言:javascript复制
@Slf4j
@ControllerAdvice(basePackages = "com.ltx.aes_demo.controller")
public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {


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

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        boolean encode = false;
        if (Objects.requireNonNull(methodParameter.getMethod()).isAnnotationPresent(SecurityParameter.class)) {
            //获取注解配置的包含和去除字段
            SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class);
            //出参是否需要加密
            encode = serializedField.outEncode();

            /**
             * 测试自定义注解,学习AES加解密可忽略删除此if
             */
            if (Objects.requireNonNull(methodParameter.getMethod()).isAnnotationPresent(Decrypt.class)){
                log.info("own @interface");
                Decrypt decryptField = methodParameter.getMethodAnnotation(Decrypt.class);
                boolean test = decryptField.test();
                if (test){
                    log.info("奥里给!");
                }
            }
        }
        if (encode) {
            log.info("对方法method :【"   methodParameter.getMethod().getName()   "】返回数据进行加密");
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                String result = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
                return AesEncryptUtil.aesEncrypt(result, AesConstant.aesKey);
            } catch (Exception e) {
                e.printStackTrace();
                log.error("对方法method :【"   methodParameter.getMethod().getName()   "】返回数据进行解密出现异常:"   e.getMessage());
            }
        }
        return body;
    }
}
测试
代码语言:javascript复制
    @SecurityParameter
    @GetMapping("getUser")
    public User getUser() {
        return userService.getUserById();
    }

    @Decrypt
    @SecurityParameter
    @PostMapping("test")
    public String test(@RequestBody User user) {
        return "hello world";
    }


    @PostMapping("/save")
    @ResponseBody
    @SecurityParameter(outEncode = false)
    public User save(@RequestBody User info) {
        System.out.println(info);
        return info;
    }

写在最后

如果不明白自定义注解的使用请查看

Spring自定义注解常用元素

如果不明白全局统一异常处理请查看

ResponseBody、RequestBodyAdvice解读

0 人点赞