Java 加解密工具类实战
2026-09-20T12:52:26+08:00 | 30分钟阅读

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) | PKCS5Padding | 16 字节 | 兼容用 |
| GCM | ✅ 自带认证标签 | NoPadding | 12 字节 | 推荐 |
新项目优先用 GCM,老系统兼容才用 CBC。
关键参数
| 参数 | 说明 |
|---|---|
| 算法 | AES |
| 模式 | CBC(兼容)、GCM(推荐) |
| 填充 | CBC 用 PKCS5Padding,GCM 用 NoPadding |
| 密钥长度 | 128 / 192 / 256 位 |
| IV | CBC 16 字节,GCM 12 字节,每次加密应随机生成 |
为什么 IV 要随机?
如果 IV 固定,相同明文每次加密结果相同,攻击者可以通过比对密文推测内容。
随机 IV 后,相同明文每次密文都不同,安全性大幅提升。
IV 不需要保密,但要跟密文一起传输。常见做法是把 IV 拼在密文前面。
对称加密:DES(仅兼容旧系统)
特点
- 也是对称加密
- 密钥只有 56 位,已被证明不安全
- 新项目不要使用,仅用于兼容老系统
与 AES 的区别
| 对比项 | DES | AES |
|---|---|---|
| 密钥长度 | 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、RSA | MD5、SHA-256、HMAC-SHA256 |
常见摘要算法
| 算法 | 输出长度 | 安全性 | 用途 |
|---|---|---|---|
| MD5 | 128 位 | ❌ 已碰撞 | 仅兼容 |
| SHA-256 | 256 位 | ✅ 安全 | 完整性校验 |
| SHA-512 | 512 位 | ✅ 更安全 | 高安全场景 |
| HMAC-SHA256 | 256 位 | ✅ 带密钥 | 防篡改 + 身份认证 |
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) |
|---|---|---|
| 参数类 | IvParameterSpec | GCMParameterSpec |
| IV 长度 | 16 字节 | 12 字节 |
| 填充 | PKCS5Padding | NoPadding |
| 文件加解密 | 可用 CipherOutputStream | 必须用 cipher.update() + doFinal() |
| 解密失败 | 抛 BadPaddingException | 抛 AEADBadTagException |
| 完整性 | 无 | ✅ 自带认证标签 |
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() |
| 密钥转 Base64 | AesGcmUtils.keyToBase64(key) |
| Base64 还原密钥 | AesGcmUtils.keyFromBase64(base64) |
| 篡改检测 | catch (AEADBadTagException e) |
GCM vs CBC 选择建议
| 场景 | 推荐 |
|---|---|
| 新项目 | AesGcmUtils(GCM) |
| 老系统只支持 CBC | AesUtils(CBC),但建议加 HMAC |
| 需要认证加密 | AesGcmUtils |
| 性能极致 | AesUtils(CBC),但安全性弱 |
| 需要兼容 Java 8 以下 | 检查 JDK 版本(GCM 需要 Java 8+) |
常见错误速查
| 错误 | 原因 | 解决 |
|---|---|---|
AEADBadTagException | 密钥错误 / 密文被篡改 | 检查密钥、密文完整性 |
IllegalArgumentException: 密文长度不合法 | 密文短于 IV 长度 | 检查传输过程 |
InvalidKeyException | 密钥长度不是 16/24/32 字节 | 用 keyFromBase64 校验 |
GCM authentication tag mismatch | 和 AEADBadTagException 同义 | 同处理 |
CipherOutputStream 丢数据 | GCM 不能用 CipherOutputStream | 用 update() + 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×tamp=1700000000";
String clientSign = DigestUtils.hmacSha256(params, appSecret);
// 服务端收到请求后
String serverParams = "userId=123&amount=100×tamp=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 |
参考
- Oracle Java Cryptography Architecture (JCA)
- OWASP Password Storage Cheat Sheet
- Spring Boot Configuration Properties
- jBCrypt GitHub
本文代码已整理为 Maven 工程 crypto-demo,包含 constant / utils / service / config 四个包,可直接 clone 使用。

湘公网安备43040002000293号