Java 加解密工具类实战

2026-09-20T12:52:26+08:00 | 30分钟阅读

Mizar
Java 加解密工具类实战

Java 加解密工具类实战:AES、GCM、DES、RSA、Base64、Digest 与 bcrypt

一篇写给后端开发者的加解密实践指南,包含完整可运行代码、文件加解密、密钥持久化、Spring Boot 集成、常量管理与密码存储最佳实践。

前言

在日常开发中,我们经常需要处理"加密"相关的需求:

  • 用户密码怎么存?
  • 接口参数怎么加密传输?
  • 敏感文件怎么加密落盘?
  • 配置里的密钥怎么安全管理?
  • Spring Boot 项目里怎么优雅地集成加解密?
  • 数据完整性怎么校验?

很多人第一次接触时,会写出这样的代码:

public static String encode(String msg){
    try {
        MessageDigest messageDigest = MessageDigest.getInstance("md5");
        return Base64.getEncoder().encodeToString(messageDigest.digest(msg.getBytes()));
    } catch (Exception e){
        e.printStackTrace();
        return null;
    }
}

然后问:“这是加密,解密呢?”

答案是:这段代码根本不能解密,因为它不是加密,而是摘要(哈希)。

本文从这个问题出发,系统梳理 Java 中常见的加解密方式,给出可直接使用的工具类,并进一步给出 Spring Boot 集成方案与常量管理方案。


先搞清楚三个概念

在写代码之前,必须区分三个容易混淆的概念:

概念是否可逆是否需要密钥典型算法用途
加密 Encryption✅ 可逆需要AES、DES、RSA保护数据机密性
摘要 Hash❌ 不可逆不需要MD5、SHA-256完整性校验、密码存储
编码 Encoding✅ 可逆不需要Base64、Hex数据传输、格式转换

关键结论:

  • MD5 / SHA 不能解密,只能做校验。
  • Base64 不是加密,任何人拿到都能解开。
  • 密码存储不要用 MD5/SHA,要用 bcrypt / Argon2。

对称加密:AES

特点

  • 加密和解密用同一个密钥
  • 速度快,适合大量数据
  • 目前最推荐的对称加密算法

两种模式对比

模式完整性校验填充IV 长度推荐度
CBC❌ 无(需额外 HMAC)PKCS5Padding16 字节兼容用
GCM✅ 自带认证标签NoPadding12 字节推荐

新项目优先用 GCM,老系统兼容才用 CBC。

关键参数

参数说明
算法AES
模式CBC(兼容)、GCM(推荐)
填充CBC 用 PKCS5Padding,GCM 用 NoPadding
密钥长度128 / 192 / 256 位
IVCBC 16 字节,GCM 12 字节,每次加密应随机生成

为什么 IV 要随机?

如果 IV 固定,相同明文每次加密结果相同,攻击者可以通过比对密文推测内容。
随机 IV 后,相同明文每次密文都不同,安全性大幅提升。

IV 不需要保密,但要跟密文一起传输。常见做法是把 IV 拼在密文前面


对称加密:DES(仅兼容旧系统)

特点

  • 也是对称加密
  • 密钥只有 56 位,已被证明不安全
  • 新项目不要使用,仅用于兼容老系统

与 AES 的区别

对比项DESAES
密钥长度56 位128/192/256 位
分组长度64 位128 位
安全性❌ 已破解✅ 安全
IV 长度8 字节16 / 12 字节
推荐度不推荐强烈推荐

非对称加密:RSA

特点

  • 一对密钥:公钥 + 私钥
  • 公钥加密 → 私钥解密
  • 私钥加密 → 公钥解密(签名场景)
  • 速度慢,只适合加密短数据

长度限制

RSA 能加密的数据长度受密钥长度限制:

最大加密字节数 = 密钥长度(位) / 8 - 11

以 2048 位密钥为例:2048 / 8 - 11 = 245 字节。超过会报错:

javax.crypto.IllegalBlockSizeException: Data must not be longer than 245 bytes

解决方案:分块加密,或采用 RSA + AES 混合加密

混合加密思路

1. 随机生成一个 AES 密钥
2. 用 AES 密钥加密数据
3. 用 RSA 公钥加密 AES 密钥
4. 把"加密后的 AES 密钥 + 加密后的数据"一起传输
5. 接收方用 RSA 私钥解出 AES 密钥,再用它解密数据

这是 HTTPS、支付网关等场景的通用做法。


Base64:编码,不是加密

Base64 是把二进制数据转换成可打印的 ASCII 字符,方便在文本协议(如 JSON、XML、URL)中传输。它没有任何密钥,任何人拿到都能解码。

// ❌ 错误认知:以为 Base64 是加密
String secret = Base64.getEncoder().encodeToString("密码123".getBytes());
// 任何人 Base64.decode 都能看到 "密码123"

摘要:MD5 / SHA / HMAC

摘要 vs 加密

对比项加密摘要
是否可逆✅ 可逆❌ 不可逆
是否需要密钥需要普通摘要不需要,HMAC 需要
用途保护数据机密性完整性校验、防篡改
典型算法AES、RSAMD5、SHA-256、HMAC-SHA256

常见摘要算法

算法输出长度安全性用途
MD5128 位❌ 已碰撞仅兼容
SHA-256256 位✅ 安全完整性校验
SHA-512512 位✅ 更安全高安全场景
HMAC-SHA256256 位✅ 带密钥防篡改 + 身份认证

HMAC 和普通摘要的区别

  • 普通摘要:任何人拿到明文都能算出同样的摘要,只用于完整性校验
  • HMAC:需要密钥,攻击者没有密钥就算不出正确摘要,用于防篡改 + 身份认证

时序攻击

普通 String.equals 会在第一个不同字符处提前返回,攻击者可以通过测量响应时间逐字节推测摘要,这叫时序攻击。所以校验摘要时要用常量时间比较


密码存储:bcrypt

为什么不能用 MD5/SHA?

算法速度加盐抗暴力破解
MD5极快需手动❌ 易碰撞
SHA-256需手动❌ GPU 可爆破
bcrypt慢(可调)自动
Argon2慢(内存+时间)自动✅✅

关键点:密码哈希算法应该故意慢,让暴力破解成本极高。

使用示例

// 注册
String hash = BCrypt.hashpw(rawPassword, BCrypt.gensalt(12));
user.setPassword(hash);

// 登录
if (BCrypt.checkpw(inputPassword, user.getPassword())) {
    // 登录成功
}

cost 因子怎么选?

cost单次耗时(参考)建议
10~50ms最低可接受
12~200ms推荐
14~800ms高安全场景

原则:让单次验证耗时在 100~300ms 之间


CryptoConstant 常量类

在写工具类之前,先把所有算法名、长度、模式等常量集中管理,避免散落在各处、改起来到处找。

为什么要单独抽一个常量类?

  • 一处修改,全局生效:算法升级或参数调整只改一处
  • 避免拼写错误"AES/CBC/PKCS5Padding" 手写容易敲错
  • 可读性更好CryptoConstant.AES_KEY_SIZE 比裸数字 128 更清晰
  • 便于统一规范:团队约定统一用某个模式、某个长度

完整实现

package com.shiguangshe.crypto.constant;

/**
 * 加解密常量统一管理
 * 集中定义算法名、模式、填充、密钥长度、IV 长度、分块大小、字符集等
 */
public final class CryptoConstant {

    private CryptoConstant() {
        // 工具类禁止实例化
    }

    // =====================================================
    // 通用
    // =====================================================

    /** 默认字符集 */
    public static final String CHARSET = "UTF-8";

    /** 流式加解密缓冲区大小(字节) */
    public static final int BUFFER_SIZE = 8192;

    // =====================================================
    // 错误提示
    // =====================================================

    public static final String ERR_IV_READ_FAILED = "文件头 IV 读取失败";
    public static final String ERR_CIPHER_TEXT_INVALID = "密文长度不合法";
    public static final String ERR_PASSWORD_EMPTY = "密码不能为空";

    /** AES 密钥长度非法 */
    public static final String ERR_AES_KEY_LENGTH_INVALID = "AES 密钥长度不合法";

    /** 摘要算法名非法 */
    public static final String ERR_DIGEST_ALGORITHM_INVALID = "摘要算法名不能为空";

    /** HMAC 密钥不能为空 */
    public static final String ERR_HMAC_KEY_EMPTY = "HMAC 密钥不能为空";

    /** GCM 认证标签校验失败 */
    public static final String ERR_GCM_TAG_MISMATCH = "GCM 认证标签校验失败,数据可能被篡改";

    /** RSA 密钥长度非法 */
    public static final String ERR_RSA_KEY_LENGTH_INVALID = "RSA 密钥长度不合法";

    /** DES 密钥长度非法 */
    public static final String ERR_DES_KEY_LENGTH_INVALID = "DES 密钥长度不合法";

    // =====================================================
    // AES
    // =====================================================

    public static final String AES_ALGORITHM = "AES";

    /** CBC 模式 + PKCS5 填充 */
    public static final String AES_TRANSFORMATION_CBC = "AES/CBC/PKCS5Padding";

    /** GCM 模式(更安全,推荐新项目使用) */
    public static final String AES_TRANSFORMATION_GCM = "AES/GCM/NoPadding";

    /** AES 默认密钥长度(位) */
    public static final int AES_KEY_SIZE = 128;

    /** AES 密钥长度可选值 */
    public static final int AES_KEY_SIZE_192 = 192;
    public static final int AES_KEY_SIZE_256 = 256;

    /** AES 合法密钥字节数(128/192/256 位对应的字节数) */
    public static final int AES_KEY_BYTES_128 = 16;
    public static final int AES_KEY_BYTES_192 = 24;
    public static final int AES_KEY_BYTES_256 = 32;

    /**
     * 校验 AES 密钥字节长度是否合法
     * @param keyBytes 密钥字节数组
     * @return 是否合法
     */
    public static boolean isValidAesKeyLength(byte[] keyBytes) {
        if (keyBytes == null) return false;
        int len = keyBytes.length;
        return len == AES_KEY_BYTES_128
                || len == AES_KEY_BYTES_192
                || len == AES_KEY_BYTES_256;
    }

    /** AES IV 长度(字节) */
    public static final int AES_IV_SIZE = 16;

    /** AES GCM 推荐 IV/Nonce 长度(字节) */
    public static final int AES_GCM_IV_SIZE = 12;

    /** AES GCM 认证标签长度(位) */
    public static final int AES_GCM_TAG_LENGTH = 128;

    // =====================================================
    // DES
    // =====================================================

    public static final String DES_ALGORITHM = "DES";
    public static final String DES_TRANSFORMATION = "DES/CBC/PKCS5Padding";
    public static final int DES_KEY_SIZE = 56;
    public static final int DES_IV_SIZE = 8;

    /** DES 合法密钥字节数 */
    public static final int DES_KEY_BYTES = 8;

    /**
     * 校验 DES 密钥字节长度是否合法
     * @param keyBytes 密钥字节数组
     * @return 是否合法
     */
    public static boolean isValidDesKeyLength(byte[] keyBytes) {
        return keyBytes != null && keyBytes.length == DES_KEY_BYTES;
    }

    // =====================================================
    // RSA
    // =====================================================

    public static final String RSA_ALGORITHM = "RSA";
    public static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";

    /** RSA 默认密钥长度(位) */
    public static final int RSA_KEY_SIZE = 2048;

    public static final int RSA_KEY_SIZE_1024 = 1024;
    public static final int RSA_KEY_SIZE_4096 = 4096;

    /**
     * 校验 RSA 密钥长度是否合法
     * @param keySize 密钥长度(位)
     * @return 是否合法
     */
    public static boolean isValidRsaKeyLength(int keySize) {
        return keySize == RSA_KEY_SIZE_1024
                || keySize == RSA_KEY_SIZE
                || keySize == RSA_KEY_SIZE_4096;
    }

    /** RSA 签名算法 */
    public static final String RSA_SIGN_ALGORITHM = "SHA256withRSA";

    /**
     * RSA PKCS1Padding 单块最大加密字节数(默认 2048 位密钥)
     * 公式:密钥长度(位) / 8 - 11
     */
    public static final int RSA_MAX_ENCRYPT_BLOCK = RSA_KEY_SIZE / 8 - 11;

    /**
     * 根据密钥长度动态计算 RSA 单块最大加密字节数
     * @param keySize 密钥长度(位),如 1024、2048、4096
     * @return 单块最大加密字节数
     */
    public static int rsaMaxEncryptBlock(int keySize) {
        return keySize / 8 - 11;
    }

    /**
     * RSA 单块最大解密字节数(等于密钥长度 / 8)
     * @param keySize 密钥长度(位)
     * @return 单块最大解密字节数
     */
    public static int rsaMaxDecryptBlock(int keySize) {
        return keySize / 8;
    }

    // =====================================================
    // 摘要(Hash)
    // =====================================================

    public static final String MD5 = "MD5";
    public static final String SHA_256 = "SHA-256";
    public static final String SHA_512 = "SHA-512";
    public static final String HMAC_SHA_256 = "HmacSHA256";

    /** 十六进制字符表 */
    public static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();

    // =====================================================
    // bcrypt
    // =====================================================

    /** bcrypt cost 因子,默认 10。越大越慢越安全,推荐 10~12 */
    public static final int BCRYPT_COST = 12;
}

设计说明

  • 算法名、模式、填充统一放一处,升级算法只改这里
  • 校验方法isValidAesKeyLength / isValidDesKeyLength / isValidRsaKeyLength)也放常量类,方便各工具类复用
  • RSA 分块大小用计算方法而不是硬编码,支持 1024/2048/4096 位
  • 错误提示全部走常量,避免文案分散


基础工具类

完整工具类清单:

用途算法/模式
CryptoConstant常量统一管理
AesUtils对称加密AES/CBC/PKCS5Padding
AesGcmUtils对称加密(推荐)AES/GCM/NoPadding
DesUtils对称加密(兼容)DES/CBC/PKCS5Padding
RsaUtils非对称加密RSA/ECB/PKCS1Padding
Base64Utils编码Base64
DigestUtils摘要MD5 / SHA-256 / SHA-512 / HmacSHA256
PasswordUtils密码存储bcrypt

下面逐个给出。

AesUtils(AES-CBC)

完整实现

package com.shiguangshe.crypto;

import com.shiguangshe.crypto.constant.CryptoConstant;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * AES 对称加密工具类
 * - 字符串加解密(随机 IV)
 * - 文件加解密
 * - 密钥持久化到文件
 */
public class AesUtils {

    private static final String ALGORITHM = CryptoConstant.AES_ALGORITHM;
    private static final String TRANSFORMATION = CryptoConstant.AES_TRANSFORMATION_CBC;
    private static final int KEY_SIZE = CryptoConstant.AES_KEY_SIZE;
    private static final int IV_SIZE = CryptoConstant.AES_IV_SIZE;
    private static final String CHARSET = CryptoConstant.CHARSET;

    // =====================================================
    // 1. 密钥生成 / 持久化
    // =====================================================

    public static SecretKey generateKey() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
        keyGen.init(KEY_SIZE, new SecureRandom());
        return keyGen.generateKey();
    }

    public static void saveKey(SecretKey key, File file) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(key.getEncoded());
        }
    }

    public static SecretKey loadKey(File file) throws IOException {
        try (FileInputStream fis = new FileInputStream(file);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
            int len;
            while ((len = fis.read(buffer)) != -1) bos.write(buffer, 0, len);
            return new SecretKeySpec(bos.toByteArray(), ALGORITHM);
        }
    }

    public static String keyToBase64(SecretKey key) {
        return Base64.getEncoder().encodeToString(key.getEncoded());
    }

    public static SecretKey keyFromBase64(String base64Key) {
        byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        if (!CryptoConstant.isValidAesKeyLength(keyBytes)) {
            throw new IllegalArgumentException(CryptoConstant.ERR_AES_KEY_LENGTH_INVALID);
        }
        return new SecretKeySpec(keyBytes, ALGORITHM);
    }

    // =====================================================
    // 2. 字符串加解密
    // =====================================================

    public static String encrypt(String plainText, SecretKey key) throws Exception {
        byte[] iv = new byte[IV_SIZE];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
        byte[] encrypted = cipher.doFinal(plainText.getBytes(CHARSET));

        byte[] combined = new byte[IV_SIZE + encrypted.length];
        System.arraycopy(iv, 0, combined, 0, IV_SIZE);
        System.arraycopy(encrypted, 0, combined, IV_SIZE, encrypted.length);

        return Base64.getEncoder().encodeToString(combined);
    }

    public static String decrypt(String cipherText, SecretKey key) throws Exception {
        byte[] combined = Base64.getDecoder().decode(cipherText);
        if (combined.length < IV_SIZE) {
            throw new IllegalArgumentException(CryptoConstant.ERR_CIPHER_TEXT_INVALID);
        }
        byte[] iv = new byte[IV_SIZE];
        byte[] encrypted = new byte[combined.length - IV_SIZE];
        System.arraycopy(combined, 0, iv, 0, IV_SIZE);
        System.arraycopy(combined, IV_SIZE, encrypted, 0, encrypted.length);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
        return new String(cipher.doFinal(encrypted), CHARSET);
    }

    // =====================================================
    // 3. 文件加解密
    // =====================================================

    public static void encryptFile(File src, File dest, SecretKey key) throws Exception {
        byte[] iv = new byte[IV_SIZE];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));

        try (FileOutputStream fos = new FileOutputStream(dest)) {
            fos.write(iv);
            try (FileInputStream fis = new FileInputStream(src);
                 CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
                byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
                int len;
                while ((len = fis.read(buffer)) != -1) {
                    cos.write(buffer, 0, len);
                }
            }
        }
    }

    public static void decryptFile(File src, File dest, SecretKey key) throws Exception {
        try (FileInputStream fis = new FileInputStream(src)) {
            byte[] iv = new byte[IV_SIZE];
            if (fis.read(iv) != IV_SIZE) {
                throw new IllegalArgumentException(CryptoConstant.ERR_IV_READ_FAILED);
            }

            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));

            try (CipherInputStream cis = new CipherInputStream(fis, cipher);
                 FileOutputStream fos = new FileOutputStream(dest)) {
                byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
                int len;
                while ((len = cis.read(buffer)) != -1) {
                    fos.write(buffer, 0, len);
                }
            }
        }
    }
}

使用示例

字符串加解密
import com.shiguangshe.crypto.AesUtils;

import javax.crypto.SecretKey;

public class AesDemo {
    public static void main(String[] args) throws Exception {
        // 1. 生成密钥
        SecretKey key = AesUtils.generateKey();
        System.out.println("密钥(Base64): " + AesUtils.keyToBase64(key));

        // 2. 加密
        String plain = "Hello, AES-CBC!";
        String encrypted = AesUtils.encrypt(plain, key);
        System.out.println("加密后: " + encrypted);

        // 3. 解密
        String decrypted = AesUtils.decrypt(encrypted, key);
        System.out.println("解密后: " + decrypted);

        // 4. 验证
        assert plain.equals(decrypted);
    }
}
密钥持久化与还原
import com.shiguangshe.crypto.AesUtils;

import javax.crypto.SecretKey;
import java.io.File;

public class AesKeyDemo {
    public static void main(String[] args) throws Exception {
        // 方式一:保存到文件
        SecretKey key = AesUtils.generateKey();
        AesUtils.saveKey(key, new File("aes.key"));

        // 从文件加载
        SecretKey loaded = AesUtils.loadKey(new File("aes.key"));
        System.out.println("加载的密钥与原密钥一致: "
                + AesUtils.keyToBase64(key).equals(AesUtils.keyToBase64(loaded)));

        // 方式二:转 Base64 字符串(推荐用于配置)
        String base64 = AesUtils.keyToBase64(key);
        SecretKey restored = AesUtils.keyFromBase64(base64);
        System.out.println("Base64 还原的密钥一致: "
                + AesUtils.keyToBase64(key).equals(AesUtils.keyToBase64(restored)));
    }
}
文件加解密
import com.shiguangshe.crypto.AesUtils;

import javax.crypto.SecretKey;
import java.io.File;

public class AesFileDemo {
    public static void main(String[] args) throws Exception {
        SecretKey key = AesUtils.generateKey();

        File src = new File("photo.jpg");
        File enc = new File("photo.jpg.enc");
        File dec = new File("photo_dec.jpg");

        // 加密
        AesUtils.encryptFile(src, enc, key);
        System.out.println("加密完成: " + enc.getAbsolutePath());

        // 解密
        AesUtils.decryptFile(enc, dec, key);
        System.out.println("解密完成: " + dec.getAbsolutePath());

        // 验证文件大小一致
        System.out.println("原文件大小: " + src.length());
        System.out.println("解密后大小: " + dec.length());
    }
}
典型场景:配置加密
public class ConfigDemo {
    public static void main(String[] args) throws Exception {
        // 数据库密码加密后存配置
        String dbPassword = "MyP@ssw0rd!";
        SecretKey key = AesUtils.keyFromBase64(System.getenv("AES_KEY"));

        String encrypted = AesUtils.encrypt(dbPassword, key);
        System.out.println("存入配置: " + encrypted);

        // 读取时解密
        String decrypted = AesUtils.decrypt(encrypted, key);
        System.out.println("使用密码: " + decrypted);
    }
}
异常处理
public class AesErrorDemo {
    public static void main(String[] args) {
        try {
            SecretKey key = AesUtils.generateKey();

            // 密钥不对 → 解密失败
            SecretKey wrongKey = AesUtils.generateKey();
            String enc = AesUtils.encrypt("test", key);
            AesUtils.decrypt(enc, wrongKey);  // 抛异常
        } catch (Exception e) {
            System.out.println("解密失败: " + e.getMessage());
            // javax.crypto.BadPaddingException: Given final block not properly padded
        }

        try {
            // 非法 Base64 密钥
            AesUtils.keyFromBase64("short");  // 抛 IllegalArgumentException
        } catch (IllegalArgumentException e) {
            System.out.println("密钥长度不合法: " + e.getMessage());
        }
    }
}

AesGcmUtils(AES-GCM,推荐)

为什么推荐 GCM

GCM 属于 AEAD(Authenticated Encryption with Associated Data),特点:

  • 加密 + 认证一体:密文被篡改,解密时会抛 AEADBadTagException
  • 无需填充:NoPadding
  • IV 推荐 12 字节:不是 CBC 的 16 字节

和 AesUtils(CBC)的关键区别

对比项AesUtils(CBC)AesGcmUtils(GCM)
参数类IvParameterSpecGCMParameterSpec
IV 长度16 字节12 字节
填充PKCS5PaddingNoPadding
文件加解密可用 CipherOutputStream必须用 cipher.update() + doFinal()
解密失败BadPaddingExceptionAEADBadTagException
完整性✅ 自带认证标签

GCM 文件加解密为什么不能用 CipherOutputStream

因为 GCM 的认证标签是在 doFinal() 时输出的,CipherOutputStream 会自动调用 doFinal() 但不方便取出最后的标签,可能丢数据。所以必须手动 update() + doFinal()

完整实现

package com.shiguangshe.crypto;

import com.shiguangshe.crypto.constant.CryptoConstant;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * AES-GCM 认证加密工具类(推荐新项目使用)
 * 相比 CBC:
 * - 自带完整性校验(认证标签),密文被篡改会解密失败
 * - 无需单独 PKCS5 填充
 * - IV/Nonce 推荐 12 字节,每次加密随机生成
 * 格式:IV(12 字节) + 密文(含 16 字节认证标签),Base64 输出
 */
public class AesGcmUtils {

    private static final String ALGORITHM = CryptoConstant.AES_ALGORITHM;
    private static final String TRANSFORMATION = CryptoConstant.AES_TRANSFORMATION_GCM;
    private static final int KEY_SIZE = CryptoConstant.AES_KEY_SIZE;
    private static final int IV_SIZE = CryptoConstant.AES_GCM_IV_SIZE;
    private static final int TAG_LENGTH = CryptoConstant.AES_GCM_TAG_LENGTH;
    private static final String CHARSET = CryptoConstant.CHARSET;

    // =====================================================
    // 1. 密钥生成 / 持久化
    // =====================================================

    public static SecretKey generateKey() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
        keyGen.init(KEY_SIZE, new SecureRandom());
        return keyGen.generateKey();
    }

    public static String keyToBase64(SecretKey key) {
        return Base64.getEncoder().encodeToString(key.getEncoded());
    }

    public static SecretKey keyFromBase64(String base64Key) {
        byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        if (!CryptoConstant.isValidAesKeyLength(keyBytes)) {
            throw new IllegalArgumentException(CryptoConstant.ERR_AES_KEY_LENGTH_INVALID);
        }
        return new SecretKeySpec(keyBytes, ALGORITHM);
    }

    // =====================================================
    // 2. 字符串加解密
    // =====================================================

    public static String encrypt(String plainText, SecretKey key) throws Exception {
        byte[] iv = new byte[IV_SIZE];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes(CHARSET));

        byte[] combined = new byte[IV_SIZE + encrypted.length];
        System.arraycopy(iv, 0, combined, 0, IV_SIZE);
        System.arraycopy(encrypted, 0, combined, IV_SIZE, encrypted.length);

        return Base64.getEncoder().encodeToString(combined);
    }

    public static String decrypt(String cipherText, SecretKey key) throws Exception {
        byte[] combined = Base64.getDecoder().decode(cipherText);
        if (combined.length < IV_SIZE) {
            throw new IllegalArgumentException(CryptoConstant.ERR_CIPHER_TEXT_INVALID);
        }
        byte[] iv = new byte[IV_SIZE];
        byte[] encrypted = new byte[combined.length - IV_SIZE];
        System.arraycopy(combined, 0, iv, 0, IV_SIZE);
        System.arraycopy(combined, IV_SIZE, encrypted, 0, encrypted.length);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH, iv);
        cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec);
        return new String(cipher.doFinal(encrypted), CHARSET);
    }

    // =====================================================
    // 3. 文件加解密
    // =====================================================

    public static void encryptFile(File src, File dest, SecretKey key) throws Exception {
        byte[] iv = new byte[IV_SIZE];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec);

        try (FileOutputStream fos = new FileOutputStream(dest)) {
            fos.write(iv);
            try (FileInputStream fis = new FileInputStream(src)) {
                byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
                byte[] output;
                int len;
                while ((len = fis.read(buffer)) != -1) {
                    output = cipher.update(buffer, 0, len);
                    if (output != null) fos.write(output);
                }
                output = cipher.doFinal();
                if (output != null) fos.write(output);
            }
        }
    }

    public static void decryptFile(File src, File dest, SecretKey key) throws Exception {
        try (FileInputStream fis = new FileInputStream(src)) {
            byte[] iv = new byte[IV_SIZE];
            if (fis.read(iv) != IV_SIZE) {
                throw new IllegalArgumentException(CryptoConstant.ERR_IV_READ_FAILED);
            }

            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH, iv);
            cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec);

            try (FileOutputStream fos = new FileOutputStream(dest)) {
                byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
                byte[] output;
                int len;
                while ((len = fis.read(buffer)) != -1) {
                    output = cipher.update(buffer, 0, len);
                    if (output != null) fos.write(output);
                }
                output = cipher.doFinal();
                if (output != null) fos.write(output);
            }
        }
    }
}

使用示例

GCM 是认证加密(AEAD),比 CBC 更安全:自带完整性校验,密文被篡改会解密失败。新项目优先使用。


字符串加解密
import com.shiguangshe.crypto.AesGcmUtils;

import javax.crypto.SecretKey;

public class AesGcmDemo {
    public static void main(String[] args) throws Exception {
        // 1. 生成密钥
        SecretKey key = AesGcmUtils.generateKey();
        System.out.println("密钥(Base64): " + AesGcmUtils.keyToBase64(key));

        // 2. 加密
        String plain = "Hello, AES-GCM!";
        String encrypted = AesGcmUtils.encrypt(plain, key);
        System.out.println("加密后: " + encrypted);

        // 3. 解密
        String decrypted = AesGcmUtils.decrypt(encrypted, key);
        System.out.println("解密后: " + decrypted);

        // 4. 验证
        assert plain.equals(decrypted);
    }
}

篡改检测(GCM 核心特性)
import com.shiguangshe.crypto.AesGcmUtils;
import com.shiguangshe.crypto.constant.CryptoConstant;

import javax.crypto.AEADBadTagException;
import javax.crypto.SecretKey;

public class AesGcmTamperDemo {
    public static void main(String[] args) throws Exception {
        SecretKey key = AesGcmUtils.generateKey();
        String plain = "转账金额:100元";
        String encrypted = AesGcmUtils.encrypt(plain, key);

        System.out.println("原密文: " + encrypted);

        // 模拟攻击者篡改密文(修改最后一个字符)
        String tampered = encrypted.substring(0, encrypted.length() - 2) + "xx";
        System.out.println("篡改后: " + tampered);

        try {
            AesGcmUtils.decrypt(tampered, key);
            System.out.println("不应该走到这里!");
        } catch (AEADBadTagException e) {
            // GCM 自动检测到数据被篡改
            System.out.println("检测到篡改: " + CryptoConstant.ERR_GCM_TAG_MISMATCH);
        }
    }
}

对比 CBC:CBC 模式下篡改密文可能解出乱码而不报错,GCM 则一定会抛 AEADBadTagException,这是 GCM 最大的优势。


密钥持久化与还原
import com.shiguangshe.crypto.AesGcmUtils;

import javax.crypto.SecretKey;

public class AesGcmKeyDemo {
    public static void main(String[] args) throws Exception {
        // 方式一:转 Base64(推荐用于配置)
        SecretKey key = AesGcmUtils.generateKey();
        String base64 = AesGcmUtils.keyToBase64(key);
        System.out.println("密钥 Base64: " + base64);

        // 从 Base64 还原
        SecretKey restored = AesGcmUtils.keyFromBase64(base64);
        System.out.println("还原的密钥一致: "
                + AesGcmUtils.keyToBase64(key).equals(AesGcmUtils.keyToBase64(restored)));

        // 方式二:用环境变量注入(生产推荐)
        // String envKey = System.getenv("AES_KEY");
        // SecretKey key2 = AesGcmUtils.keyFromBase64(envKey);
    }
}

文件加解密
import com.shiguangshe.crypto.AesGcmUtils;

import javax.crypto.SecretKey;
import java.io.File;

public class AesGcmFileDemo {
    public static void main(String[] args) throws Exception {
        SecretKey key = AesGcmUtils.generateKey();

        File src = new File("report.pdf");
        File enc = new File("report.pdf.enc");
        File dec = new File("report_dec.pdf");

        // 加密
        AesGcmUtils.encryptFile(src, enc, key);
        System.out.println("加密完成: " + enc.getAbsolutePath()
                + " 大小: " + enc.length());

        // 解密
        AesGcmUtils.decryptFile(enc, dec, key);
        System.out.println("解密完成: " + dec.getAbsolutePath()
                + " 大小: " + dec.length());

        // 验证
        System.out.println("原文件大小: " + src.length());
        System.out.println("解密后大小: " + dec.length());
    }
}

和 CBC 的差异:GCM 文件加解密必须用 cipher.update() + cipher.doFinal() 手动分块,不能使用 CipherOutputStream,因为认证标签需要 doFinal() 输出。


大文件流式加解密
import com.shiguangshe.crypto.AesGcmUtils;

import javax.crypto.SecretKey;
import java.io.File;

public class AesGcmLargeFileDemo {
    public static void main(String[] args) throws Exception {
        SecretKey key = AesGcmUtils.generateKey();

        // GCM 内部就是流式处理,大文件不会一次性读入内存
        File bigFile = new File("big-video.mp4");
        File enc = new File("big-video.mp4.enc");
        File dec = new File("big-video_dec.mp4");

        long start = System.currentTimeMillis();

        AesGcmUtils.encryptFile(bigFile, enc, key);
        System.out.println("加密耗时: " + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        AesGcmUtils.decryptFile(enc, dec, key);
        System.out.println("解密耗时: " + (System.currentTimeMillis() - start) + "ms");
    }
}
典型场景:接口敏感字段加密
public class ApiEncryptDemo {
    public static void main(String[] args) throws Exception {
        // 服务端从配置读取密钥
        SecretKey key = AesGcmUtils.keyFromBase64(System.getenv("AES_KEY"));

        // 客户端加密身份证号
        String idCard = "110101199001011234";
        String encrypted = AesGcmUtils.encrypt(idCard, key);
        System.out.println("传输密文: " + encrypted);

        // 服务端解密
        String decrypted = AesGcmUtils.decrypt(encrypted, key);
        System.out.println("服务端解密: " + decrypted);
    }
}

典型场景:敏感数据落库
@Service
public class SensitiveDataService {

    private final AesGcmUtils aesGcm;
    private final SecretKey key;

    public SensitiveDataService() throws Exception {
        // 从配置中心或环境变量读取密钥
        this.key = AesGcmUtils.keyFromBase64(System.getenv("AES_KEY"));
    }

    /** 保存时加密 */
    public void savePhone(Long userId, String phone) throws Exception {
        String encrypted = AesGcmUtils.encrypt(phone, key);
        // jdbcTemplate.update("UPDATE user SET phone = ? WHERE id = ?", encrypted, userId);
    }

    /** 查询时解密 */
    public String getPhone(String encryptedPhone) throws Exception {
        return AesGcmUtils.decrypt(encryptedPhone, key);
    }
}

Spring Boot 集成
package com.shiguangshe.crypto.service;

import com.shiguangshe.crypto.AesGcmUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;

@Service
public class AesGcmService {

    @Value("${crypto.aes.key}")
    private String base64Key;

    private SecretKey key;

    @PostConstruct
    public void init() throws Exception {
        this.key = AesGcmUtils.keyFromBase64(base64Key);

        // 启动自检
        String probe = "health-check";
        String enc = AesGcmUtils.encrypt(probe, key);
        String dec = AesGcmUtils.decrypt(enc, key);
        if (!probe.equals(dec)) {
            throw new IllegalStateException("AES-GCM 密钥自检失败");
        }
    }

    public String encrypt(String plain) throws Exception {
        return AesGcmUtils.encrypt(plain, key);
    }

    public String decrypt(String cipher) throws Exception {
        return AesGcmUtils.decrypt(cipher, key);
    }
}

application.yml

crypto:
  aes:
    key: ${AES_KEY}

启动:

export AES_KEY="MTIzNDU2Nzg5MGFiY2RlZg=="
java -jar app.jar
异常处理
import com.shiguangshe.crypto.AesGcmUtils;
import com.shiguangshe.crypto.constant.CryptoConstant;

import javax.crypto.AEADBadTagException;
import javax.crypto.SecretKey;

public class AesGcmErrorDemo {
    public static void main(String[] args) {
        try {
            SecretKey key = AesGcmUtils.generateKey();
            SecretKey wrongKey = AesGcmUtils.generateKey();

            String enc = AesGcmUtils.encrypt("test", key);

            // 1. 密钥错误 → AEADBadTagException
            try {
                AesGcmUtils.decrypt(enc, wrongKey);
            } catch (AEADBadTagException e) {
                System.out.println("密钥错误: " + CryptoConstant.ERR_GCM_TAG_MISMATCH);
            }

            // 2. 密文被篡改 → AEADBadTagException
            String tampered = enc.substring(0, enc.length() - 2) + "xx";
            try {
                AesGcmUtils.decrypt(tampered, key);
            } catch (AEADBadTagException e) {
                System.out.println("数据篡改: " + CryptoConstant.ERR_GCM_TAG_MISMATCH);
            }

            // 3. 密文过短 → IllegalArgumentException
            try {
                AesGcmUtils.decrypt("AAAA", key);
            } catch (IllegalArgumentException e) {
                System.out.println("密文不合法: " + e.getMessage());
            }

            // 4. 密钥长度非法 → IllegalArgumentException
            try {
                AesGcmUtils.keyFromBase64("c2hvcnQ=");  // "short" → 5 字节
            } catch (IllegalArgumentException e) {
                System.out.println("密钥非法: " + e.getMessage());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
与 AesUtils(CBC)对比使用
public class AesCompareDemo {
    public static void main(String[] args) throws Exception {
        String plain = "Hello";

        // CBC
        SecretKey cbcKey = AesUtils.generateKey();
        String cbcEnc = AesUtils.encrypt(plain, cbcKey);
        System.out.println("[CBC] 密文长度: " + cbcEnc.length());

        // GCM
        SecretKey gcmKey = AesGcmUtils.generateKey();
        String gcmEnc = AesGcmUtils.encrypt(plain, gcmKey);
        System.out.println("[GCM] 密文长度: " + gcmEnc.length());

        // GCM 密文略长(含认证标签 16 字节),但换来完整性保护
    }
}
性能对比(简单测一下)
public class AesPerfDemo {
    public static void main(String[] args) throws Exception {
        String plain = "性能测试数据性能测试数据性能测试数据";
        int times = 10000;

        SecretKey cbcKey = AesUtils.generateKey();
        SecretKey gcmKey = AesGcmUtils.generateKey();

        long t1 = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            AesUtils.decrypt(AesUtils.encrypt(plain, cbcKey), cbcKey);
        }
        System.out.println("CBC 1万次: " + (System.currentTimeMillis() - t1) + "ms");

        long t2 = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            AesGcmUtils.decrypt(AesGcmUtils.encrypt(plain, gcmKey), gcmKey);
        }
        System.out.println("GCM 1万次: " + (System.currentTimeMillis() - t2) + "ms");
    }
}

结论:GCM 比 CBC 略慢(因为多了认证标签计算),但在现代 CPU 上差异很小。安全 > 性能,新项目优先 GCM。

速查
场景方法
字符串加密AesGcmUtils.encrypt(plain, key)
字符串解密AesGcmUtils.decrypt(cipher, key)
文件加密AesGcmUtils.encryptFile(src, dest, key)
文件解密AesGcmUtils.decryptFile(src, dest, key)
生成密钥AesGcmUtils.generateKey()
密钥转 Base64AesGcmUtils.keyToBase64(key)
Base64 还原密钥AesGcmUtils.keyFromBase64(base64)
篡改检测catch (AEADBadTagException e)

GCM vs CBC 选择建议
场景推荐
新项目AesGcmUtils(GCM)
老系统只支持 CBCAesUtils(CBC),但建议加 HMAC
需要认证加密AesGcmUtils
性能极致AesUtils(CBC),但安全性弱
需要兼容 Java 8 以下检查 JDK 版本(GCM 需要 Java 8+)

常见错误速查
错误原因解决
AEADBadTagException密钥错误 / 密文被篡改检查密钥、密文完整性
IllegalArgumentException: 密文长度不合法密文短于 IV 长度检查传输过程
InvalidKeyException密钥长度不是 16/24/32 字节keyFromBase64 校验
GCM authentication tag mismatchAEADBadTagException 同义同处理
CipherOutputStream 丢数据GCM 不能用 CipherOutputStreamupdate() + doFinal()

DesUtils(DES,兼容)

完整实现

package com.shiguangshe.crypto;

import com.shiguangshe.crypto.constant.CryptoConstant;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * DES 对称加密工具类(仅用于老系统兼容,新项目请用 AES)
 */
public class DesUtils {

    private static final String ALGORITHM = CryptoConstant.DES_ALGORITHM;
    private static final String TRANSFORMATION = CryptoConstant.DES_TRANSFORMATION;
    private static final int KEY_SIZE = CryptoConstant.DES_KEY_SIZE;
    private static final int IV_SIZE = CryptoConstant.DES_IV_SIZE;
    private static final String CHARSET = CryptoConstant.CHARSET;

    // =====================================================
    // 1. 密钥生成 / 持久化
    // =====================================================

    public static SecretKey generateKey() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
        keyGen.init(KEY_SIZE, new SecureRandom());
        return keyGen.generateKey();
    }

    public static void saveKey(SecretKey key, File file) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(key.getEncoded());
        }
    }

    public static SecretKey loadKey(File file) throws IOException {
        try (FileInputStream fis = new FileInputStream(file);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
            int len;
            while ((len = fis.read(buffer)) != -1) bos.write(buffer, 0, len);
            return new SecretKeySpec(bos.toByteArray(), ALGORITHM);
        }
    }

    public static String keyToBase64(SecretKey key) {
        return Base64.getEncoder().encodeToString(key.getEncoded());
    }

    public static SecretKey keyFromBase64(String base64Key) {
        byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        if (!CryptoConstant.isValidDesKeyLength(keyBytes)) {
            throw new IllegalArgumentException(CryptoConstant.ERR_DES_KEY_LENGTH_INVALID);
        }
        return new SecretKeySpec(keyBytes, ALGORITHM);
    }

    // =====================================================
    // 2. 字符串加解密
    // =====================================================

    public static String encrypt(String plainText, SecretKey key) throws Exception {
        byte[] iv = new byte[IV_SIZE];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
        byte[] encrypted = cipher.doFinal(plainText.getBytes(CHARSET));

        byte[] combined = new byte[IV_SIZE + encrypted.length];
        System.arraycopy(iv, 0, combined, 0, IV_SIZE);
        System.arraycopy(encrypted, 0, combined, IV_SIZE, encrypted.length);
        return Base64.getEncoder().encodeToString(combined);
    }

    public static String decrypt(String cipherText, SecretKey key) throws Exception {
        byte[] combined = Base64.getDecoder().decode(cipherText);
        if (combined.length < IV_SIZE) {
            throw new IllegalArgumentException(CryptoConstant.ERR_CIPHER_TEXT_INVALID);
        }
        byte[] iv = new byte[IV_SIZE];
        byte[] encrypted = new byte[combined.length - IV_SIZE];
        System.arraycopy(combined, 0, iv, 0, IV_SIZE);
        System.arraycopy(combined, IV_SIZE, encrypted, 0, encrypted.length);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
        return new String(cipher.doFinal(encrypted), CHARSET);
    }

    // =====================================================
    // 3. 文件加解密
    // =====================================================

    public static void encryptFile(File src, File dest, SecretKey key) throws Exception {
        byte[] iv = new byte[IV_SIZE];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));

        try (FileOutputStream fos = new FileOutputStream(dest)) {
            fos.write(iv);
            try (FileInputStream fis = new FileInputStream(src);
                 CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
                byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
                int len;
                while ((len = fis.read(buffer)) != -1) {
                    cos.write(buffer, 0, len);
                }
            }
        }
    }

    public static void decryptFile(File src, File dest, SecretKey key) throws Exception {
        try (FileInputStream fis = new FileInputStream(src)) {
            byte[] iv = new byte[IV_SIZE];
            if (fis.read(iv) != IV_SIZE) {
                throw new IllegalArgumentException(CryptoConstant.ERR_IV_READ_FAILED);
            }
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));

            try (CipherInputStream cis = new CipherInputStream(fis, cipher);
                 FileOutputStream fos = new FileOutputStream(dest)) {
                byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
                int len;
                while ((len = cis.read(buffer)) != -1) {
                    fos.write(buffer, 0, len);
                }
            }
        }
    }
}

使用示例

DES 用法与 AES 几乎一样,仅算法不同。新项目请用 AesUtils

字符串加解密
import com.shiguangshe.crypto.DesUtils;

import javax.crypto.SecretKey;

public class DesDemo {
    public static void main(String[] args) throws Exception {
        // 1. 生成密钥
        SecretKey key = DesUtils.generateKey();
        System.out.println("DES 密钥(Base64): " + DesUtils.keyToBase64(key));

        // 2. 加密
        String plain = "Hello, DES!";
        String encrypted = DesUtils.encrypt(plain, key);
        System.out.println("加密后: " + encrypted);

        // 3. 解密
        String decrypted = DesUtils.decrypt(encrypted, key);
        System.out.println("解密后: " + decrypted);
    }
}
与老系统对接
public class DesLegacyDemo {
    public static void main(String[] args) throws Exception {
        // 假设老系统用固定的 DES 密钥,通过 Base64 传输
        String legacyKeyBase64 = "R2FyeVdlYXRoZXI=";  // 从老系统配置读取

        SecretKey key = DesUtils.keyFromBase64(legacyKeyBase64);

        // 解密老系统传来的密文
        String legacyCipher = "...";  // 老系统传过来的密文
        String plain = DesUtils.decrypt(legacyCipher, key);
        System.out.println("老系统明文: " + plain);
    }
}
文件加解密
import com.shiguangshe.crypto.DesUtils;

import javax.crypto.SecretKey;
import java.io.File;

public class DesFileDemo {
    public static void main(String[] args) throws Exception {
        SecretKey key = DesUtils.generateKey();

        DesUtils.encryptFile(new File("old-data.txt"), new File("old-data.enc"), key);
        DesUtils.decryptFile(new File("old-data.enc"), new File("old-data.dec"), key);

        System.out.println("DES 文件加解密完成");
    }
}
迁移建议
public class DesMigrationDemo {
    public static void main(String[] args) throws Exception {
        // 迁移思路:读旧数据用 DES,写新数据用 AES
        SecretKey oldKey = DesUtils.keyFromBase64("...");  // 老密钥
        SecretKey newKey = AesUtils.generateKey();         // 新密钥

        // 读旧
        String oldCipher = "...";
        String plain = DesUtils.decrypt(oldCipher, oldKey);

        // 写新
        String newCipher = AesUtils.encrypt(plain, newKey);
        System.out.println("迁移完成: " + newCipher);
    }
}

RsaUtils(RSA + 签名)

完整实现

package com.shiguangshe.crypto;

import com.shiguangshe.crypto.constant.CryptoConstant;

import javax.crypto.Cipher;
import java.io.*;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * RSA 非对称加密工具类
 * - 公钥加密 / 私钥解密
 * - 私钥加密 / 公钥解密(签名场景,正式签名请用 Signature)
 * - 文件加解密(大文件需注意 RSA 块长度限制)
 * - 密钥对持久化
 * - 数字签名 / 验签
 */
public class RsaUtils {

    private static final String ALGORITHM = CryptoConstant.RSA_ALGORITHM;
    private static final String TRANSFORMATION = CryptoConstant.RSA_TRANSFORMATION;
    private static final int KEY_SIZE = CryptoConstant.RSA_KEY_SIZE;
    private static final String CHARSET = CryptoConstant.CHARSET;

    // =====================================================
    // 1. 密钥对生成
    // =====================================================

    public static KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
        keyGen.initialize(KEY_SIZE, new SecureRandom());
        return keyGen.generateKeyPair();
    }

    public static KeyPair generateKeyPair(int keySize) throws Exception {
        if (!CryptoConstant.isValidRsaKeyLength(keySize)) {
            throw new IllegalArgumentException(CryptoConstant.ERR_RSA_KEY_LENGTH_INVALID);
        }
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
        keyGen.initialize(keySize, new SecureRandom());
        return keyGen.generateKeyPair();
    }

    // =====================================================
    // 2. 密钥对持久化
    // =====================================================

    public static void savePublicKey(PublicKey publicKey, File file) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(publicKey.getEncoded());
        }
    }

    public static void savePrivateKey(PrivateKey privateKey, File file) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(privateKey.getEncoded());
        }
    }

    public static PublicKey loadPublicKey(File file) throws Exception {
        byte[] keyBytes = readAllBytes(file);
        return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(keyBytes));
    }

    public static PrivateKey loadPrivateKey(File file) throws Exception {
        byte[] keyBytes = readAllBytes(file);
        return KeyFactory.getInstance(ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
    }

    // =====================================================
    // 3. 密钥 <-> Base64 字符串
    // =====================================================

    public static String publicKeyToBase64(PublicKey publicKey) {
        return Base64.getEncoder().encodeToString(publicKey.getEncoded());
    }

    public static String privateKeyToBase64(PrivateKey privateKey) {
        return Base64.getEncoder().encodeToString(privateKey.getEncoded());
    }

    public static PublicKey publicKeyFromBase64(String base64) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(base64);
        return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(keyBytes));
    }

    public static PrivateKey privateKeyFromBase64(String base64) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(base64);
        return KeyFactory.getInstance(ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
    }

    // =====================================================
    // 4. 字符串加解密
    // =====================================================

    public static String encryptByPublicKey(String plainText, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encrypted = cipher.doFinal(plainText.getBytes(CHARSET));
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public static String decryptByPrivateKey(String cipherText, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
        return new String(decrypted, CHARSET);
    }

    public static String encryptByPrivateKey(String plainText, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte[] encrypted = cipher.doFinal(plainText.getBytes(CHARSET));
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public static String decryptByPublicKey(String cipherText, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
        return new String(decrypted, CHARSET);
    }

    // =====================================================
    // 5. 文件加解密(分块处理)
    // =====================================================

    public static void encryptFileByPublicKey(File src, File dest, PublicKey publicKey) throws Exception {
        encryptFileByPublicKey(src, dest, publicKey, KEY_SIZE);
    }

    public static void encryptFileByPublicKey(File src, File dest, PublicKey publicKey, int keySize) throws Exception {
        if (!CryptoConstant.isValidRsaKeyLength(keySize)) {
            throw new IllegalArgumentException(CryptoConstant.ERR_RSA_KEY_LENGTH_INVALID);
        }

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        int maxBlock = CryptoConstant.rsaMaxEncryptBlock(keySize);

        try (FileInputStream fis = new FileInputStream(src);
             DataOutputStream dos = new DataOutputStream(new FileOutputStream(dest))) {
            byte[] buffer = new byte[maxBlock];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                byte[] out = cipher.doFinal(buffer, 0, len);
                dos.writeInt(out.length);
                dos.write(out);
            }
        }
    }

    public static void decryptFileByPrivateKey(File src, File dest, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        try (DataInputStream dis = new DataInputStream(new FileInputStream(src));
             FileOutputStream fos = new FileOutputStream(dest)) {

            while (true) {
                int blockLen;
                try {
                    blockLen = dis.readInt();
                } catch (EOFException e) {
                    break;
                }
                byte[] block = new byte[blockLen];
                dis.readFully(block);
                byte[] out = cipher.doFinal(block);
                fos.write(out);
            }
        }
    }

    // =====================================================
    // 6. 数字签名
    // =====================================================

    public static String sign(String data, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance(CryptoConstant.RSA_SIGN_ALGORITHM);
        signature.initSign(privateKey);
        signature.update(data.getBytes(CHARSET));
        return Base64.getEncoder().encodeToString(signature.sign());
    }

    public static boolean verify(String data, String signBase64, PublicKey publicKey) throws Exception {
        Signature signature = Signature.getInstance(CryptoConstant.RSA_SIGN_ALGORITHM);
        signature.initVerify(publicKey);
        signature.update(data.getBytes(CHARSET));
        return signature.verify(Base64.getDecoder().decode(signBase64));
    }

    // =====================================================
    // 工具方法
    // =====================================================

    private static byte[] readAllBytes(File file) throws IOException {
        try (FileInputStream fis = new FileInputStream(file);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        }
    }
}

使用示例

密钥对生成与持久化
import com.shiguangshe.crypto.RsaUtils;

import java.io.File;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;

public class RsaKeyDemo {
    public static void main(String[] args) throws Exception {
        // 1. 生成密钥对
        KeyPair keyPair = RsaUtils.generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();

        // 2. 保存到文件
        RsaUtils.savePublicKey(publicKey, new File("public.key"));
        RsaUtils.savePrivateKey(privateKey, new File("private.key"));

        // 3. 从文件加载
        PublicKey pub = RsaUtils.loadPublicKey(new File("public.key"));
        PrivateKey pri = RsaUtils.loadPrivateKey(new File("private.key"));

        // 4. 或转 Base64(用于配置)
        String pubBase64 = RsaUtils.publicKeyToBase64(publicKey);
        String priBase64 = RsaUtils.privateKeyToBase64(privateKey);
        System.out.println("公钥 Base64 长度: " + pubBase64.length());
        System.out.println("私钥 Base64 长度: " + priBase64.length());

        // 从 Base64 还原
        PublicKey pub2 = RsaUtils.publicKeyFromBase64(pubBase64);
        PrivateKey pri2 = RsaUtils.privateKeyFromBase64(priBase64);
    }
}
公钥加密 / 私钥解密
public class RsaEncryptDemo {
    public static void main(String[] args) throws Exception {
        KeyPair keyPair = RsaUtils.generateKeyPair();

        String plain = "Hello, RSA!";

        // 公钥加密
        String encrypted = RsaUtils.encryptByPublicKey(plain, keyPair.getPublic());
        System.out.println("加密后: " + encrypted);

        // 私钥解密
        String decrypted = RsaUtils.decryptByPrivateKey(encrypted, keyPair.getPrivate());
        System.out.println("解密后: " + decrypted);
    }
}
数字签名 / 验签
public class RsaSignDemo {
    public static void main(String[] args) throws Exception {
        KeyPair keyPair = RsaUtils.generateKeyPair();

        String data = "这是一条重要消息";

        // 私钥签名
        String sign = RsaUtils.sign(data, keyPair.getPrivate());
        System.out.println("签名: " + sign);

        // 公钥验签
        boolean ok = RsaUtils.verify(data, sign, keyPair.getPublic());
        System.out.println("验签结果: " + ok);  // true

        // 篡改数据后验签
        boolean tampered = RsaUtils.verify("篡改后的消息", sign, keyPair.getPublic());
        System.out.println("篡改后验签: " + tampered);  // false
    }
}
文件加解密(分块)
import com.shiguangshe.crypto.RsaUtils;

import java.io.File;
import java.security.KeyPair;

public class RsaFileDemo {
    public static void main(String[] args) throws Exception {
        KeyPair keyPair = RsaUtils.generateKeyPair();

        File src = new File("small.txt");
        File enc = new File("small.enc");
        File dec = new File("small.dec");

        // 加密(默认 2048 位分块)
        RsaUtils.encryptFileByPublicKey(src, enc, keyPair.getPublic());

        // 解密
        RsaUtils.decryptFileByPrivateKey(enc, dec, keyPair.getPrivate());

        System.out.println("RSA 文件加解密完成");

        // 也可以用 4096 位分块
        // RsaUtils.encryptFileByPublicKey(src, enc, keyPair.getPublic(), 4096);
    }
}
典型场景:接口签名验证
public class ApiSignDemo {
    public static void main(String[] args) throws Exception {
        // 服务端生成密钥对,公钥下发给客户端
        KeyPair serverKeyPair = RsaUtils.generateKeyPair();

        // 客户端:用私钥签名请求
        String requestBody = "{\"userId\":123,\"amount\":100}";
        String clientSign = RsaUtils.sign(requestBody, serverKeyPair.getPrivate());

        // 服务端:用公钥验签
        boolean valid = RsaUtils.verify(requestBody, clientSign, serverKeyPair.getPublic());
        if (valid) {
            System.out.println("请求合法,处理业务");
        } else {
            System.out.println("请求被篡改,拒绝处理");
        }
    }
}
异常处理
public class RsaErrorDemo {
    public static void main(String[] args) {
        try {
            // 非法的密钥长度
            RsaUtils.generateKeyPair(512);  // 抛 IllegalArgumentException
        } catch (IllegalArgumentException e) {
            System.out.println("密钥长度不合法: " + e.getMessage());
        }

        try {
            // 数据过长(超过 245 字节)
            KeyPair kp = RsaUtils.generateKeyPair();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 300; i++) sb.append("a");
            RsaUtils.encryptByPublicKey(sb.toString(), kp.getPublic());  // 抛异常
        } catch (Exception e) {
            System.out.println("数据过长: " + e.getMessage());
            // Data must not be longer than 245 bytes
        }
    }
}

Base64Utils(编码)

完整实现

package com.shiguangshe.crypto;

import com.shiguangshe.crypto.constant.CryptoConstant;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * Base64 编码/解码工具类
 * 注意:Base64 不是加密,只是编码,任何人都能解码
 */
public class Base64Utils {

    public static String encode(String plainText) {
        return Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));
    }

    public static String decode(String base64Text) {
        return new String(Base64.getDecoder().decode(base64Text), StandardCharsets.UTF_8);
    }

    public static String encodeUrlSafe(String plainText) {
        return Base64.getUrlEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));
    }

    public static String decodeUrlSafe(String base64Text) {
        return new String(Base64.getUrlDecoder().decode(base64Text), StandardCharsets.UTF_8);
    }

    public static String encodeFile(File file) throws IOException {
        try (FileInputStream fis = new FileInputStream(file);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[CryptoConstant.BUFFER_SIZE];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return Base64.getEncoder().encodeToString(bos.toByteArray());
        }
    }

    public static void decodeToFile(String base64Text, File dest) throws IOException {
        byte[] data = Base64.getDecoder().decode(base64Text);
        try (FileOutputStream fos = new FileOutputStream(dest)) {
            fos.write(data);
        }
    }

    public static void encodeFileToTextFile(File src, File destText) throws IOException {
        String base64 = encodeFile(src);
        try (FileWriter fw = new FileWriter(destText, StandardCharsets.UTF_8)) {
            fw.write(base64);
        }
    }
}

使用示例

再次强调:Base64 不是加密,只是编码,任何人都能解码。

字符串编码/解码
import com.shiguangshe.crypto.Base64Utils;

public class Base64Demo {
    public static void main(String[] args) {
        String plain = "Hello, 世界!";

        // 标准编码
        String encoded = Base64Utils.encode(plain);
        System.out.println("编码: " + encoded);

        // 解码
        String decoded = Base64Utils.decode(encoded);
        System.out.println("解码: " + decoded);

        // URL 安全版本(用于 URL 参数、JWT)
        String urlSafe = Base64Utils.encodeUrlSafe(plain);
        System.out.println("URL 安全编码: " + urlSafe);

        String decodedUrlSafe = Base64Utils.decodeUrlSafe(urlSafe);
        System.out.println("URL 安全解码: " + decodedUrlSafe);
    }
}
文件编码/解码
import com.shiguangshe.crypto.Base64Utils;

import java.io.File;

public class Base64FileDemo {
    public static void main(String[] args) throws Exception {
        File image = new File("logo.png");

        // 文件 → Base64 字符串
        String base64 = Base64Utils.encodeFile(image);
        System.out.println("Base64 长度: " + base64.length());

        // Base64 字符串 → 文件
        Base64Utils.decodeToFile(base64, new File("logo_copy.png"));

        // 文件 → Base64 文本文件
        Base64Utils.encodeFileToTextFile(image, new File("logo.b64.txt"));
    }
}
典型场景:DataURL
public class DataUrlDemo {
    public static void main(String[] args) throws Exception {
        File image = new File("avatar.png");
        String base64 = Base64Utils.encodeFile(image);

        // 生成 DataURL,可直接放 HTML <img src="...">
        String dataUrl = "data:image/png;base64," + base64;
        System.out.println("DataURL 长度: " + dataUrl.length());

        // 在网页里使用:
        // <img src="data:image/png;base64,iVBORw0KGgoAAAANS..." />
    }
}
典型场景:JWT 风格拼接
public class JwtLikeDemo {
    public static void main(String[] args) throws Exception {
        String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
        String payload = "{\"userId\":123,\"exp\":1700000000}";

        String h = Base64Utils.encodeUrlSafe(header);
        String p = Base64Utils.encodeUrlSafe(payload);
        String signature = "fake-signature";  // 实际用 HMAC 计算

        String jwt = h + "." + p + "." + signature;
        System.out.println("JWT: " + jwt);
    }
}

DigestUtils(摘要)

为什么需要 DigestUtils

回到开头那个问题:

“这是加密,解密呢?”

MessageDigest.getInstance("md5")

这行代码用的是 MD5 摘要算法,它没有解密。但很多场景确实需要摘要:

  • 文件完整性校验
  • 接口参数签名(防篡改)
  • 缓存 key 生成
  • 数据指纹

所以我们把摘要能力也封装成一个工具类,和加密工具类并列。

完整实现

package com.shiguangshe.crypto;

import com.shiguangshe.crypto.constant.CryptoConstant;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

/**
 * 摘要(Hash)工具类
 * - MD5 / SHA-256 / SHA-512:单向不可逆,用于完整性校验
 * - HMAC-SHA256:带密钥的摘要,用于防篡改校验
 * 注意:
 * - 摘要不能"解密",只能比对
 * - 不要用 MD5/SHA 存储密码,密码请用 {@link PasswordUtils}
 */
public class DigestUtils {

    private static final String CHARSET = CryptoConstant.CHARSET;

    // =====================================================
    // 1. 普通摘要(无密钥)
    // =====================================================

    public static String md5(String plainText) throws Exception {
        return digest(CryptoConstant.MD5, plainText);
    }

    public static String sha256(String plainText) throws Exception {
        return digest(CryptoConstant.SHA_256, plainText);
    }

    public static String sha512(String plainText) throws Exception {
        return digest(CryptoConstant.SHA_512, plainText);
    }

    public static String digest(String algorithm, String plainText) throws Exception {
        if (algorithm == null || algorithm.isEmpty()) {
            throw new IllegalArgumentException(CryptoConstant.ERR_DIGEST_ALGORITHM_INVALID);
        }
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        byte[] bytes = messageDigest.digest(plainText.getBytes(CHARSET));
        return toHex(bytes);
    }

    // =====================================================
    // 2. HMAC 摘要(带密钥)
    // =====================================================

    public static String hmacSha256(String plainText, String secretKey) throws Exception {
        if (secretKey == null || secretKey.isEmpty()) {
            throw new IllegalArgumentException(CryptoConstant.ERR_HMAC_KEY_EMPTY);
        }
        Mac mac = Mac.getInstance(CryptoConstant.HMAC_SHA_256);
        SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(CHARSET), CryptoConstant.HMAC_SHA_256);
        mac.init(keySpec);
        byte[] bytes = mac.doFinal(plainText.getBytes(CHARSET));
        return toHex(bytes);
    }

    public static boolean verifyDigest(String plainText, String algorithm, String expectedHex) throws Exception {
        String actualHex = digest(algorithm, plainText);
        return constantTimeEquals(actualHex, expectedHex);
    }

    // =====================================================
    // 工具方法
    // =====================================================

    public static String toHex(byte[] bytes) {
        if (bytes == null) return null;
        char[] chars = new char[bytes.length * 2];
        for (int i = 0; i < bytes.length; i++) {
            int v = bytes[i] & 0xFF;
            chars[i * 2] = CryptoConstant.HEX_CHARS[v >>> 4];
            chars[i * 2 + 1] = CryptoConstant.HEX_CHARS[v & 0x0F];
        }
        return new String(chars);
    }

    private static boolean constantTimeEquals(String a, String b) {
        if (a == null || b == null) return false;
        byte[] aBytes = a.getBytes(StandardCharsets.UTF_8);
        byte[] bBytes = b.getBytes(StandardCharsets.UTF_8);
        if (aBytes.length != bBytes.length) return false;
        int result = 0;
        for (int i = 0; i < aBytes.length; i++) {
            result |= aBytes[i] ^ bBytes[i];
        }
        return result == 0;
    }
}

使用示例

普通摘要
import com.shiguangshe.crypto.DigestUtils;

public class DigestDemo {
    public static void main(String[] args) throws Exception {
        String data = "Hello, Digest!";

        String md5 = DigestUtils.md5(data);
        String sha256 = DigestUtils.sha256(data);
        String sha512 = DigestUtils.sha512(data);

        System.out.println("MD5:    " + md5);      // 32 位
        System.out.println("SHA256: " + sha256);   // 64 位
        System.out.println("SHA512: " + sha512);   // 128 位
    }
}
校验摘要
import com.shiguangshe.crypto.DigestUtils;
import com.shiguangshe.crypto.constant.CryptoConstant;

public class DigestVerifyDemo {
    public static void main(String[] args) throws Exception {
        String data = "重要文件内容";
        String expected = DigestUtils.sha256(data);

        // 校验(用常量时间比较,防时序攻击)
        boolean ok = DigestUtils.verifyDigest(data, CryptoConstant.SHA_256, expected);
        System.out.println("校验结果: " + ok);  // true

        // 篡改数据
        boolean tampered = DigestUtils.verifyDigest("被篡改", CryptoConstant.SHA_256, expected);
        System.out.println("篡改后校验: " + tampered);  // false
    }
}
HMAC 摘要
import com.shiguangshe.crypto.DigestUtils;

public class HmacDemo {
    public static void main(String[] args) throws Exception {
        String data = "订单号:12345,金额:100";
        String secretKey = "mySecretKey123";

        // 计算 HMAC
        String hmac = DigestUtils.hmacSha256(data, secretKey);
        System.out.println("HMAC: " + hmac);

        // 服务端用同样的密钥重新计算,比对
        String serverHmac = DigestUtils.hmacSha256(data, secretKey);
        System.out.println("校验: " + hmac.equals(serverHmac));  // true
    }
}
典型场景:文件完整性校验
import com.shiguangshe.crypto.DigestUtils;

import java.io.File;
import java.nio.file.Files;

public class FileIntegrityDemo {
    public static void main(String[] args) throws Exception {
        File file = new File("installer.zip");

        // 下载后计算 SHA-256
        String content = new String(Files.readAllBytes(file.toPath()));
        String actualHash = DigestUtils.sha256(content);

        // 官网公布的哈希
        String officialHash = "abc123...";

        if (DigestUtils.verifyDigest(content, "SHA-256", officialHash)) {
            System.out.println("文件完整,可以使用");
        } else {
            System.out.println("文件被篡改,请勿使用");
        }
    }
}
典型场景:接口参数签名
import com.shiguangshe.crypto.DigestUtils;

public class ApiSignatureDemo {
    public static void main(String[] args) throws Exception {
        // 客户端
        String appSecret = "abcdef123456";
        String params = "userId=123&amount=100&timestamp=1700000000";
        String clientSign = DigestUtils.hmacSha256(params, appSecret);

        // 服务端收到请求后
        String serverParams = "userId=123&amount=100&timestamp=1700000000";
        String serverSign = DigestUtils.hmacSha256(serverParams, appSecret);

        if (clientSign.equals(serverSign)) {
            System.out.println("签名校验通过");
        } else {
            System.out.println("签名不匹配,拒绝请求");
        }
    }
}
异常处理
public class DigestErrorDemo {
    public static void main(String[] args) {
        try {
            // 算法名为空
            DigestUtils.digest("", "test");
        } catch (IllegalArgumentException e) {
            System.out.println("算法名非法: " + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            // HMAC 密钥为空
            DigestUtils.hmacSha256("data", "");
        } catch (IllegalArgumentException e) {
            System.out.println("HMAC 密钥为空: " + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

PasswordUtils(bcrypt)

完整实现

package com.shiguangshe.crypto;

import com.shiguangshe.crypto.constant.CryptoConstant;
import org.mindrot.jbcrypt.BCrypt;

/**
 * 密码存储工具类(基于 bcrypt)
 * 特点:
 * - 自动加盐,无需手动管理 salt
 * - 慢哈希,抗 GPU / 彩虹表暴力破解
 * - 单向哈希,不能"解密",只能 verify
 * - 每次加密结果不同(因为盐随机),但都能验证通过
 */
public class PasswordUtils {

    public static String hash(String plainPassword) {
        if (plainPassword == null || plainPassword.isEmpty()) {
            throw new IllegalArgumentException(CryptoConstant.ERR_PASSWORD_EMPTY);
        }
        return BCrypt.hashpw(plainPassword, BCrypt.gensalt(CryptoConstant.BCRYPT_COST));
    }

    public static boolean verify(String plainPassword, String hashedPassword) {
        if (plainPassword == null || hashedPassword == null || hashedPassword.isEmpty()) {
            return false;
        }
        try {
            return BCrypt.checkpw(plainPassword, hashedPassword);
        } catch (Exception e) {
            return false;
        }
    }
}

使用示例

注册与登录
import com.shiguangshe.crypto.PasswordUtils;

public class PasswordDemo {
    public static void main(String[] args) {
        // 1. 用户注册:明文 → hash 存数据库
        String rawPassword = "MyP@ssw0rd!";
        String hash = PasswordUtils.hash(rawPassword);
        System.out.println("存入数据库的哈希: " + hash);

        // 2. 用户登录:校验
        String inputPassword = "MyP@ssw0rd!";
        boolean ok = PasswordUtils.verify(inputPassword, hash);
        System.out.println("密码正确: " + ok);  // true

        // 3. 密码错误
        boolean wrong = PasswordUtils.verify("wrongPassword", hash);
        System.out.println("密码错误: " + wrong);  // false
    }
}
每次哈希结果不同
public class PasswordSaltDemo {
    public static void main(String[] args) {
        String password = "samePassword";

        // 两次 hash 结果不同(因为盐随机)
        String hash1 = PasswordUtils.hash(password);
        String hash2 = PasswordUtils.hash(password);

        System.out.println("hash1: " + hash1);
        System.out.println("hash2: " + hash2);
        System.out.println("是否相同: " + hash1.equals(hash2));  // false

        // 但都能验证通过
        System.out.println("verify hash1: " + PasswordUtils.verify(password, hash1));  // true
        System.out.println("verify hash2: " + PasswordUtils.verify(password, hash2));  // true
    }
}
典型场景:用户服务
public class UserService {

    private final UserDao userDao = new UserDao();

    /** 注册 */
    public void register(String username, String rawPassword) {
        // 存明文密码?绝对不行!
        // 存 MD5?也不行,容易被彩虹表破解
        // 正确做法:bcrypt
        String hash = PasswordUtils.hash(rawPassword);

        User user = new User();
        user.setUsername(username);
        user.setPassword(hash);  // 存哈希
        userDao.save(user);
    }

    /** 登录 */
    public boolean login(String username, String rawPassword) {
        User user = userDao.findByUsername(username);
        if (user == null) {
            return false;  // 用户不存在
        }

        // 用 verify 校验
        return PasswordUtils.verify(rawPassword, user.getPassword());
    }

    /** 修改密码 */
    public void changePassword(String username, String oldPassword, String newPassword) {
        User user = userDao.findByUsername(username);
        if (user == null || !PasswordUtils.verify(oldPassword, user.getPassword())) {
            throw new RuntimeException("原密码错误");
        }

        user.setPassword(PasswordUtils.hash(newPassword));
        userDao.update(user);
    }
}
异常处理

java

public class PasswordErrorDemo {
    public static void main(String[] args) {
        try {
            // 空密码
            PasswordUtils.hash("");
        } catch (IllegalArgumentException e) {
            System.out.println("密码不能为空: " + e.getMessage());
        }

        try {
            // null 密码
            PasswordUtils.hash(null);
        } catch (IllegalArgumentException e) {
            System.out.println("密码不能为空: " + e.getMessage());
        }

        // verify 对 null / 空 是安全的,不会抛异常,直接返回 false
        System.out.println(PasswordUtils.verify(null, "hash"));       // false
        System.out.println(PasswordUtils.verify("pwd", null));        // false
        System.out.println(PasswordUtils.verify("pwd", ""));          // false
        System.out.println(PasswordUtils.verify("pwd", "invalid"));   // false
    }
}

Spring Boot 集成版

工具类解决"怎么加密",Spring Boot 集成解决"怎么优雅地用"。

设计思路

  • AesGcmUtils 能力封装成 AesService,注册为 @Component
  • 密钥从 application.yml 读取,通过 @ConfigurationProperties 注入
  • 启动时校验密钥是否合法(长度、格式)
  • 对外只暴露 encrypt / decrypt 方法,隐藏密钥细节

application.yml

crypto:
  aes:
    # Base64 编码的 AES 密钥(128 位 → 16 字节 → Base64 后 24 字符)
    # 生产环境请用环境变量或配置中心注入:${AES_KEY}
    key: "MTIzNDU2Nzg5MGFiY2RlZg=="
    # 可选,是否打印加密日志(生产建议 false)
    log-enabled: false

生成密钥的小技巧:

SecretKey key = AesGcmUtils.generateKey();
System.out.println(AesGcmUtils.keyToBase64(key));

把输出粘贴到 key 处即可。

CryptoProperties.java

package com.shiguangshe.crypto.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "crypto.aes")
public class CryptoProperties {

    /** Base64 编码的 AES 密钥 */
    private String key;

    /** 是否打印加密日志 */
    private boolean logEnabled = false;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public boolean isLogEnabled() {
        return logEnabled;
    }

    public void setLogEnabled(boolean logEnabled) {
        this.logEnabled = logEnabled;
    }
}

AesService.java

package com.shiguangshe.crypto.service;

import com.shiguangshe.crypto.AesGcmUtils;
import com.shiguangshe.crypto.config.CryptoProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;

/**
 * AES-GCM 加解密服务
 * - 密钥从 application.yml 读取
 * - 启动时校验密钥合法性
 * - 对外暴露 encrypt / decrypt
 */
@Component
public class AesService {

    private static final Logger log = LoggerFactory.getLogger(AesService.class);

    private final CryptoProperties properties;
    private SecretKey secretKey;

    public AesService(CryptoProperties properties) {
        this.properties = properties;
    }

    @PostConstruct
    public void init() {
        String base64Key = properties.getKey();
        if (base64Key == null || base64Key.isEmpty()) {
            throw new IllegalStateException("crypto.aes.key 未配置");
        }

        try {
            this.secretKey = AesGcmUtils.keyFromBase64(base64Key);
        } catch (Exception e) {
            throw new IllegalStateException("AES 密钥格式非法,请检查 crypto.aes.key", e);
        }

        // 自检:加密再解密
        try {
            String probe = "health-check";
            String enc = AesGcmUtils.encrypt(probe, secretKey);
            String dec = AesGcmUtils.decrypt(enc, secretKey);
            if (!probe.equals(dec)) {
                throw new IllegalStateException("AES 密钥自检失败");
            }
        } catch (Exception e) {
            throw new IllegalStateException("AES 密钥自检异常", e);
        }

        if (properties.isLogEnabled()) {
            log.info("AES 服务初始化完成,密钥长度: {} 位",
                    secretKey.getEncoded().length * 8);
        }
    }

    public String encrypt(String plainText) {
        if (plainText == null) return null;
        try {
            return AesGcmUtils.encrypt(plainText, secretKey);
        } catch (Exception e) {
            throw new RuntimeException("AES 加密失败", e);
        }
    }

    public String decrypt(String cipherText) {
        if (cipherText == null) return null;
        try {
            return AesGcmUtils.decrypt(cipherText, secretKey);
        } catch (Exception e) {
            throw new RuntimeException("AES 解密失败", e);
        }
    }
}

使用示例

@Service
public class UserService {

    private final AesService aesService;

    public UserService(AesService aesService) {
        this.aesService = aesService;
    }

    public void saveSensitiveData(String idCard) {
        String encrypted = aesService.encrypt(idCard);
        // 存数据库
    }

    public String getSensitiveData(String encrypted) {
        return aesService.decrypt(encrypted);
    }
}

生产环境密钥注入

不要把真实密钥写在 application.yml 里提交到 Git。推荐用环境变量:

crypto:
  aes:
    key: ${AES_KEY}

启动时注入:

export AES_KEY="MTIzNDU2Nzg5MGFiY2RlZg=="
java -jar app.jar

或者用 K8s Secret / Docker Secret / 配置中心(Nacos、Apollo)。


如何选择?

场景推荐方案
普通业务数据加密AES-GCM(推荐) / AES-CBC(兼容)
需要完整性校验AES-GCM 或 AES-CBC + HMAC
文件完整性校验DigestUtils.sha256
接口防篡改DigestUtils.hmacSha256
缓存 key 生成DigestUtils.md5(仅非安全场景)
跨端密钥交换RSA + AES 混合加密
数字签名RSA + SHA256withRSA
二进制转文本Base64
用户密码存储bcrypt / Argon2
数据完整性校验SHA-256 / HMAC-SHA256
老系统兼容DES(尽快迁移 AES)

生产环境最佳实践

优先使用 GCM

新项目优先用 AesGcmUtils,而不是 AesUtils。GCM 自带完整性校验,能检测出密文篡改;CBC 需要额外配合 HMAC 才能防篡改,实现复杂且容易出错。

如果是老系统只支持 CBC,至少要:

  • IV 每次随机生成
  • IV 和密文一起存储
  • 密钥不要硬编码

密钥管理

  • ❌ 不要把密钥硬编码在代码里
  • ❌ 不要把密钥提交到 Git
  • ✅ 使用 KMS / Vault / 配置中心 管理密钥
  • ✅ 通过环境变量注入密钥
  • ✅ 密钥定期轮换
  • ✅ 不同环境用不同密钥

加解密使用

  • AES 的 IV 每次加密都要随机生成
  • 加密后的数据要带上 IV(或使用 GCM 模式自带的 nonce)
  • RSA 只用来加密短数据,长数据用混合加密
  • 大文件用流式加密,不要一次读进内存
  • 常量集中管理,方便统一升级

日志安全

  • 禁止在日志里打印明文密码
  • 禁止打印完整密钥
  • 敏感字段输出前要脱敏
  • 生产环境关闭加解密细节日志

密码存储

  • 永远不存明文密码
  • 不使用 MD5/SHA 存密码
  • 使用 bcrypt / Argon2 / PBKDF2
  • cost 因子调到"单次验证 100~300ms"

总结

回到开头那个问题:

“这是加密,解密呢?”

MessageDigest.getInstance("md5")

这行代码用的是 MD5 摘要算法,它是单向不可逆的,所以没有解密

正确的心智模型是:

  • 加密 → 可逆,有密钥 → AES、RSA、DES
  • 摘要 → 不可逆,无密钥 → MD5、SHA-256
  • 编码 → 可逆,无密钥 → Base64

工程化实践是:

  • 常量集中CryptoConstant
  • 工具类封装AesUtils / AesGcmUtils / RsaUtils / DigestUtils / PasswordUtils
  • Spring Boot 集成AesService + application.yml
  • 密钥不落地代码 → 环境变量 / KMS

最后送上一句话:

能用 GCM 就不要用 CBC,能用 AES 就不要用 DES,能存 bcrypt 就不要存 MD5,能用 KMS 就不要硬编码密钥。


完整工具类清单

用途算法/模式
CryptoConstant常量统一管理
AesUtils对称加密AES/CBC/PKCS5Padding
AesGcmUtils对称加密(推荐)AES/GCM/NoPadding
DesUtils对称加密(兼容)DES/CBC/PKCS5Padding
RsaUtils非对称加密RSA/ECB/PKCS1Padding
Base64Utils编码Base64
DigestUtils摘要MD5 / SHA-256 / SHA-512 / HmacSHA256
PasswordUtils密码存储bcrypt

参考


本文代码已整理为 Maven 工程 crypto-demo,包含 constant / utils / service / config 四个包,可直接 clone 使用。

© 2024 - 2026 拾光舍

湘ICP备2025117227号-1 | 公安备案图标 湘公网安备43040002000293号
在线工具资源网站

在线工具官网

ProcessOn:👉ProcessOn在线作图

腾讯文档:👉腾讯文档

FlowUs 息流:👉FlowUs 息流

Mermaid Live Editor:👉在线流程图和图表编辑器 - 美人鱼实时编辑器

ME2在线工具(加密工具):👉ME2在线工具-在线开发学习生活工具箱

Textln:👉Textln文本智能云平台-通用文本解析

MinerU:👉MinerU数据提取工具

Crontab:👉Cron Expression Examples - Crontab.guru

正则表达式测试网站:👉regex101: build, test, and debug regex

技术资源网站

技术资源官网

Spring:👉Spring官网

MyBatis中文网 :👉MyBatis 中文网

Redis中文网:👉Redis中文网

Zookeeper:👉Apache ZooKeeper官网

Raft算法:👉Raft Consensus Algorithm官网

Hadoop:👉Apache Hadoop官网

Hive:👉Apache Hive官网

Phoenix:👉Apache Phoenix官网

Bootstrap中文网:👉Bootstrap中文网

Element:👉组件 | Element

Layui :👉Layui - 极简模块化前端 UI 组件库(官方文档)

FreeMarker :👉FreeMarker 中文官方参考手册

ztree树插件: 👉zTree – jQuery 树插件官网

x File Storage:👉x File Storage官网

社交链接

© 2024 - 2026 拾光舍

湘ICP备2025117227号-1 | 公安备案图标 湘公网安备43040002000293号

ESC

使用扫一扫,扫码加我