前后端(JAVA)实现AES对称加解密方式

1 对称加密分类以及概括

文章《加解密篇 - 对称加密算法 (DES、3DES、AES、RC)》有对这几种对称加密的说明,我大概概括一下。

1.1 加密安全等级 DES < 3DES < AES < RC

依次是DES < 3DES < AES < RC

1.2 DES

DES全称 Data Encryption Standard,是一种使用密钥加密的块算法,该加密算法运用非常普遍,是一种标准的加密算法。现在认为是一种不安全的加密算法,因为现在已经有用穷举法攻破 DES 密码的报道了,所以诞生了3DES。

1.3 3DES

3DES(或称为 Triple DES)是三重数据加密算法(TDEA,Triple Data Encryption Algorithm)块密码的通称。它相当于是对每个数据块应用三次 DES 加密算法。

1.4 AES

AES 加密算法的安全性要高于 DES 和 3DES,所以 AES 已经成为了主要的对称加密算法。AES 加密算法就是众多对称加密算法中的一种,它的英文全称是 Advanced Encryption Standard,翻译过来是高级加密标准,它是用来替代之前的 DES 加密算法的。

AES 一共有四种加密模式,分别是 ECB(电子密码本模式)、CBC(密码分组链接模式)、CFB、OFB,我们一般使用的是 CBC 模式。四种模式中除了 ECB 相对不安全之外,其它三种模式的区别并没有那么大。

1.5 RC

RC加密包括RC2,RC4,RC5,RC2 是由著名密码学家 Ron Rivest 设计的一种传统对称分组加密算法,它可作为 DES 算法的建议替代算法。它的输入和输出都是64bit。密钥的长度是从1字节到128字节可变,但目前的实现是8字节(1998年)。

参考:
加解密篇 - 对称加密算法 (DES、3DES、AES、RC)

2 前后端实现AES对称加解密方式

  1. AES为对称加密算法,顾名思义,如果是前后端加解密场景,那前端需要保存一份秘钥,后端也需要保存一份秘钥,这两个秘钥是相同的,才可以实现加解密。

  2. AES的秘钥默认长度为16位,初始向量 IV也是16位,这两个默认长度一定要遵守,否则会有很多不可未知的错误。如果需要增加秘钥的长度增加复杂性,则推荐使用RC加密算法,因为该算法的秘钥长度可变。

  3. 待解密长度需要为16的倍数,否则会报以下错误,常用的解决办法为加密后使用Base64包装密文,则会自动补齐为16的倍数,解密时先使用Base64解密,则密文一定是16的整数倍。

    Input length must be multiple of 16 when decrypting with padded cipher
    

3 后端AES对称加解密(ECB和CBC模式)工具类

import org.apache.commons.lang3.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * @author 
 * @description AES加解密(ECB和CBC模式)
 * @date 2023/12/8 15:08
 */
public class AESEncryptUtil {

    /**
     * 加密模式
     * ECB: AES/ECB/PKCS5Padding
     * CBC: AES/CBC/NoPadding
     */
    private static final String[] TRANSFORM_ALGORITHM = new String[]{"AES/ECB/PKCS5Padding", "AES/CBC/NoPadding"};
    /**
     * 初始向量样式 IV
     */
    private static String iv =  "HBJNRU56MDk4NzK6";
    private static final String ALGORITHM = "AES";
    private static final String CHARSET_NAME = "UTF-8";
    /**
     * AES加解key样式
     */
    public static String ENCRYPT_KEY = "7CC408B24462ABD1";

    /**
     * AES的ECB模式加解
     * @param data 待加密参数
     * @param key 加密key
     * @return
     */
    public static String encryptECB(String data, String key) {
        if (StringUtils.isEmpty(key)) {
            throw new IllegalArgumentException("加密失败,加密key为空");
        }
        SecretKeySpec aesKey = new SecretKeySpec(key.getBytes(Charset.forName(CHARSET_NAME)), ALGORITHM);
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORM_ALGORITHM[0]);
            cipher.init(Cipher.ENCRYPT_MODE, aesKey);
            byte[] encrypted = cipher.doFinal(data.getBytes(Charset.forName(CHARSET_NAME)));
            // 使用Base64来包装是规避报错Input length must be multiple of 16 when decrypting with padded cipher
            // 解密的字节数组必须是16的倍数
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new IllegalArgumentException("加密失败: "+ e.getMessage());
        }
    }

    /**
     * AES的ECB模式解密
     * @param data 待解密参数
     * @param key 解密key
     * @return
     */
    public static String decryptECB(String data, String key) {
        if (StringUtils.isEmpty(key)) {
            throw new IllegalArgumentException("解密失败,解密key为空");
        }
        byte[] decode = Base64.getDecoder().decode(data.getBytes(StandardCharsets.UTF_8));
        SecretKeySpec aesKey = new SecretKeySpec(key.getBytes(Charset.forName(CHARSET_NAME)), ALGORITHM);
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORM_ALGORITHM[0]);
            cipher.init(Cipher.DECRYPT_MODE, aesKey);
            return new String(cipher.doFinal(decode));
        } catch (Exception e) {
            throw new IllegalArgumentException("解密失败: "+ e.getMessage());
        }
    }

    /**
     * AES的CBC模式加密
     * @param data 要加密的数据
     * @param key 加密key
     * @return 加密的结果
     */
    public static String encryptCBC(String data, String key) {
        try {
            // "算法/模式/补码方式"
            Cipher cipher = Cipher.getInstance(TRANSFORM_ALGORITHM[1]);
            int blockSize = cipher.getBlockSize();
            byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
            int plaintextLength = dataBytes.length;
            if (plaintextLength % blockSize != 0) {
                plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
            }
            byte[] plaintext = new byte[plaintextLength];
            System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
            SecretKeySpec keyStr = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
            IvParameterSpec ivStr = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
            cipher.init(Cipher.ENCRYPT_MODE, keyStr, ivStr);
            byte[] encrypted = cipher.doFinal(plaintext);
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new IllegalArgumentException("加密失败: "+ e.getMessage());
        }
    }

    /**
     * AES的CBC模式解密
     * @param data 要解密的数据
     * @param key 解密key
     * @return 解密的结果
     */
    public static String decryptCBC(String data, String key) {
        try {
            byte[] encrypted1 = Base64.getDecoder().decode(data);
            Cipher cipher = Cipher.getInstance(TRANSFORM_ALGORITHM[1]);
            SecretKeySpec keyStr = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
            IvParameterSpec ivStr = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
            cipher.init(Cipher.DECRYPT_MODE, keyStr, ivStr);
            byte[] original = cipher.doFinal(encrypted1);
            return new String(original, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new IllegalArgumentException("解密失败: "+ e.getMessage());
        }
    }

    public static void main(String[] args) throws Exception {
        String data = "hello Test symmetric encry";
        String keyStr = "7CC408B24462ABD1";
        String encryDataStr = encryptECB(data, keyStr);
        System.out.println("encryptECB = " + encryDataStr);
        System.out.println("decryptECB = " + decryptECB(encryDataStr, keyStr));
        encryDataStr = encryptCBC(data, keyStr);
        System.out.println("encryptCBC = " + encryDataStr);
        System.out.println("decryptCBC = " + decryptCBC(encryDataStr, keyStr));

    }

}

4 前端(VUE)AES对称加解密(CBC模式)工具类

前端其他工程配置请看文章《java前后端参数和返回加密解密AES+CBC》

npm install crypto-js

4.1 aseKeConfig.js AES+CBC配置

export const AES_KEY = 'MTIzNDU2Nzg5MEFC'
export const AES_IV = 'QUJDRURGMDk4NzY1'
// 参数是否进行加密设置,需要与后端配置保持一致
export const PARAM_ENCRYPT_ABLE = true
// 结果是否进行加密
export const RESULT_ENCRYPT_ABLE = true
// 需要排除的不进行加密的接口,正则匹配
export const EXCLUE_PATH = ['.*/orc/.*', '.*/fastdfs/.*', '.*/eempFastdfs/.*', ',.*/homepage/preview', '.*oauth/getClickApplicationInfo', '.*/common/defaultKaptcha.*', '.*/autoKeyword$', '.*/getLayerCount$', '.*/getLayerInfoListByPage$',
  '.*/epUiImgSaveAndAnalysisMultipartFile$', '/v7/weather']

4.2 加密解密工具类 aesSecretUtil.js

import CryptoJS from 'crypto-js'
import {
    AES_KEY,
    AES_IV,
    PARAM_ENCRYPT_ABLE,
    EXCLUE_PATH,
    RESULT_ENCRYPT_ABLE
}
from '../config/aesKeyConfig.js'
const key = CryptoJS.enc.Utf8.parse(AES_KEY) // 16位
    const iv = CryptoJS.enc.Utf8.parse(AES_IV)
    const excluePath = EXCLUE_PATH
    const paramEncryptAble = PARAM_ENCRYPT_ABLE
    const resultEncryptAble = RESULT_ENCRYPT_ABLE
    /**
    *Description AES CBC BASE64加密解密
     *@author
     *@date 13:38 2022/3/31
     */
    export default {
        // aes加密
        encrypt(word) {
            let encrypted = ''
                if (typeof word === 'string') {
                    const srcs = CryptoJS.enc.Utf8.parse(word)
                        encrypted = CryptoJS.AES.encrypt(srcs, key, {
                            iv: iv,
                            mode: CryptoJS.mode.CBC,
                            padding: CryptoJS.pad.ZeroPadding
                        })
                } else if (typeof word === 'object') {
                    // 对象格式的转成json字符串
                    const data = JSON.stringify(word)
                        const srcs = CryptoJS.enc.Utf8.parse(data)
                        encrypted = CryptoJS.AES.encrypt(srcs, key, {
                            iv: iv,
                            mode: CryptoJS.mode.CBC,
                            padding: CryptoJS.pad.ZeroPadding
                        })
                }
                return CryptoJS.enc.Base64.stringify(encrypted.ciphertext)
        },
        // aes解密
        decrypt(word) {
            if (word) {
                let base64 = CryptoJS.enc.Base64.parse(word)
                    let src = CryptoJS.enc.Base64.stringify(base64)
                    var decrypt = CryptoJS.AES.decrypt(src, key, {
                        iv: iv,
                        mode: CryptoJS.mode.CBC,
                        padding: CryptoJS.pad.ZeroPadding
                    })
                    var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
                    return decryptedStr.toString()
            } else {
                return word
            }
        },
        // 判断url是否在匹配的正则表达式上,匹配则不进行加密,不配则需要加密
        checkIsExcluePath(url) {
            // 如果包含需要排除加密的接口返回true
            let flag = false
                for (let i = 0; i < excluePath.length; i++) {
                    if (new RegExp('^' + excluePath[i]).test(url)) {
                        flag = true
                            break
                    } else {
                        flag = false
                    }
                }
                return flag
        },
        // 判断是否请求需要进行加密,配置值true的时候需要加密否则不需要
        checkParamEncryptAble() {
            // console.log(encryptAble)
            return paramEncryptAble
        },
        // 判断是否只对结果进行加密
        checkResultEncryptAble() {
            // console.log(encryptAble)
            return resultEncryptAble
        }
    }

参考:
java前后端参数和返回加密解密
对称加密( 共享密钥密码 ) —用相同的密钥进行加密和解密
手写一个java加密工具类Securit
Java中的AES加解密(CBC模式)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐