尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java 加解密组件再设计
关于 Java 加密方案我若干年前写过一篇博客《一套清晰、简洁的 Java AES/DES/RSA 加密解密 API 》。那时最大的收获是通过重构代码进而感悟到“面向对象”的极大优势。但如今回头反思虽然当时已经应用了 OOP却仍显不成熟——从代码的痕迹中能明显感觉到为 OO 而 OO非常不自然甚至肉眼可见地生硬。另一方面自己当时也没有真正理解 RSA/AES 加解密的思想只是停留在 API 层面的生搬硬套。当然了那时候也没有 AI 辅助编程只是参考了相关博文及代码然后自己手搓了一套。于是趁着这次 AI 重构基础库的机会我也把加解密的库一并重构将代码放在 Codex 中一行一行地过。虽说不上多完美但也是心目中较为理想的一个代码版本。与纯粹 AI 生成代码不同我先是有了基本的构思然后与 AI 讨论方案接着把旧代码自己重构然后交给 AI review 修改这样子定稿的。我觉得这相当于 AI 是我学习某一门技术的导师编码只是这个学习中的一个过程。最终是让我学会了不但怎么去实现功能还有什么才为之好的代码以及完整的交付单测、文档等。当然了我也不是小白编程肯定有我自己的代码风格及细节要求约束。这些都跟 AI 交待以便最终形成自己中意的代码。同时一个侧面也说明了这个时代里面代码虽然廉价但输出符合个性化定制的代码还是有相当的教学意义的。更新版本说明舍弃某些 APIDES/3DES/PBE 加密算法已经不再安全了故不再提供。我没有直接删除代码或者保留在类库中标记为“遗留的”而是将这部分的代码移至 test 代码算是一种妥当的安置方式毕竟是亲手搓的心血。如此一来 API 便只剩 AES 及 RSA对于实际情形也足够了。其中 AES 并不是单纯一种算法而是有 N 种算法。当前 AES API 支持 GCM/CBC/PBE/Legacy足够涵盖大多数安全的l需求。API 重构上一版已经是 OO 风格了这一版仍是并持续改进。首先是入参的安排设计。涉及加解密的入参往往包括算法字符串类型、Key 密钥Key 对象类型或者 Base64、nonce 随机数、原文/密文等繁多的参数。应该怎么安排它们呢对于一些固定的参数安排在构造器参数中其余的则安排在实例方法中。有些参数比如原文/密文都是 String 入参UTF8 字符串与 Base64 字符串这时候就不能通过方法重载去定义了只能单设一个方法来区分。例如DoCipher.doCipher()方法定义是 UTF8 字符串入参doCipherFromBase64()则是供 Base64 字符串用。至于出参最开始的想法是通过方法重载加枚举类型指定的方式来适应不同类型的出参但这样下来感觉不是太合理重复的逻辑也多。于是我不得不考虑新的方案就是跳出这个类执行完核心业务逻辑之后返回一个中介的 Java Bean如下所示CipherResult类存储一个数据类型比如byte[]在 bean 的构造器入参然后有关的数据类型转换方法均在此 bean 上执行按需求调用即可。/** * Represents the raw result of a cryptographic cipher operation. * * pThe underlying result is binary data. It may be obtained directly as * bytes or represented as UTF-8, Base64 or hexadecimal text./p * * pEncrypted data is arbitrary binary data and should normally be represented * using {link #toBase64()} or {link #toHex()}. {link #toUtf8()} is primarily * intended for decrypted plaintext known to contain UTF-8 encoded text./p */RequiredArgsConstructorpublicclassCipherResult{/** * Raw result bytes produced by the cipher operation. */Getterprivatefinalbyte[]result;/** * Interprets the result bytes as UTF-8 text. * * pThis method is primarily intended for decrypted plaintext known to * contain UTF-8 encoded text. Arbitrary encrypted binary data should normally * be represented using {link #toBase64()} or {link #toHex()}./p * * return the result interpreted as a UTF-8 string */publicStringtoUtf8(){returnnewStringBytes(result).getUTF8_String();}/** * Encodes the result bytes as Base64. * * return Base64-encoded result */publicStringtoBase64(){returnnewBase64Utils(result).encodeAsString();}/** * Encodes the result bytes as hexadecimal text. * * return hexadecimal representation of the result */publicStringtoHex(){returnStringBytes.bytesToHex(result);}}根据使用的需要返回下面四种类型结果。原始数据byte[]UTF8 字符串Base64 编码字符串16进制的字符串AES 对称加密对称加密就放在DoCipher基类中围绕方法CipherResult doCipher(int mode, byte[] data, AlgorithmParameterSpec spec, byte[] associatedData)完成。虽然只是一个方法逻辑上也不复杂但入参比较多还是值得用一个类去完成。publicCipherResultdoCipher(intmode,byte[]data,AlgorithmParameterSpecspec,byte[]associatedData){CiphercipherCipher.getInstance(algorithmName);if(spec!null)cipher.init(mode,key,spec);elsecipher.init(mode,key);if(associatedData!null)cipher.updateAAD(associatedData);returnnewCipherResult(cipher.doFinal(data));}基础 AES不安全但简单类AesLegacy是最初的 AES 的版本本来安全性不足理应被遗弃使用的Deprecated但考虑到足够简单故而暂定保留并冠以 leagcy。该方法无非就是明文密码 key 即可完成加密没有其他额外的入参因此可以想象安全性是非常脆弱的不推荐使用。使用方法StringresultnewAesLegacy(psw).encrypt(content);StringdecryptnewAesLegacy(psw).decrypt(result);assertEquals(content,decrypt);AES-GCMAES-GCM 比上述的基础 AES 加密在安全性上大大加强是目前主流的加密方式之一也是本 API 默认推荐的方式。对比基础 AES 在 API 用法上可以明显感觉如下几点密钥仍可以是任意字符串但增加两种密钥的数据格式一种是字符串的 byte[]另外是密钥字符串的 Base64 编码字符串。也就是说支持密钥非明文的方式传输使用增加 nonce 参数通常为“随机数”或“一次性数值”这是必须的。可选的byte[] associatedData参数用于保证数据的完整性。密钥 keyAES 的密钥本质上是固定长度的字节数组byte[]而不是任意字符串。密钥可以由密码学安全随机数生成器产生也可以通过 AES-PBE 从密码派生下面会介绍当然不止这一种方式当前我们 API 只支持这种。这里有一个常见的认知误区需要澄清“密钥”与“密码”是两个不同的概念。密码是人类可读的字符串如myPassword123而密钥是密码学意义上的随机字节序列。密码必须经过 KDF如 PBKDF2、scrypt、Argon2拉伸和派生后才能成为合格的 AES 密钥直接拿密码字符串的字节当密钥使用会因长度不匹配带来严重的安全隐患。这也是本 API 将“密码密钥”单独划入 AES-PBE 模式的原因。AES 本身只关心最终得到的合法密钥字节所以密钥必须是符合 AES 要求的字节序列长度为16 字节AES-12824 字节AES-19232 字节AES-256你可以通过 API 方便地生成// 都返回 byte[]DoCipher.randomBytes(16);DoCipher.randomBytes(24);DoCipher.randomBytes(32);如果输入字符串本身是已经生成好的密钥的编码形式例如 Base64则可以解码得到原始密钥字节不需要再进行密码派生。API 支持直接传入 Base64 字符串作为构造器参数入参。如果使用者希望输入自定义密码这得使用 AES-PBE 模式我们下文会讲述。nonce关于 nonce 一般用法如下Nonce 参与加密计算使得同一份明文使用相同密钥加密时即使明文相同只要 Nonce 不同生成的密文通常也不同。因此Nonce 可以帮助避免相同明文反复加密时产生相同的密文模式。同一个密钥下不能重复使用 Nonce。个人感觉这个跟最早做用户密码 md5 加盐时候能够增强安全性有异曲同工之妙。都是加密时额外加入一个参数让每次加密结果有所变化。更加要明确的是这个 nonce 生成之后不仅加密时候需要而且解密时候也需要同一个。发送端和接收端必须使用完全一致的 nonce 字节。因此这是必须在加密之后交付的。顺带一提的是这个 nonce 不需要再加密了。Java 是不支持多返回值所以不能既返回 nonce 又返回密文。于是对应的做法是要么组合“nonce密文”返回要么新设一个 Java bean 对象在不同的字段上返回。当前我们 API 采用后者即返回对象AesCipherResult。publicclassAesCipherResultextendsCipherResult{publicAesCipherResult(byte[]result){super(result);}GetterSetterprivateCipherResultnonce;/** * associatedData */GetterSetterprivateCipherResultaad;}当然这个 nonce 也可以按需返回你要的格式。值得一提的是当前 API 中如果你不想手动生成 nonceAPI 可以为你代劳调用encrypt(String data)或encrypt(String data, byte[] associatedData)即可。如果你要手动生成那么 nonce 长度必须是 12 字节。所以值得注意的是这里所谓的随机数并不是随机字符串那么简单更准确的是随机字节和 key 类似或者你用 base64 字符串那么字符长度是 16。如下代码所示这个方法不要求传入 nonce内部实现。publicAesCipherResultencrypt(Stringdata,byte[]associatedData){byte[]noncerandomBytes(GCM_NONCE_LENGTH);returnencrypt(data,nonce,associatedData);}associatedData 关联数据这是一个非常有用的特性用于校验密文的数据完整性防止数据被篡改。做过 HTTP 报文加密的朋友都会有个关于签名的印象那个就是用于数据校验的。有时候密钥、解密都正常但是数据保不保真则是另外一回事了数据转手过程中有没有被修改呢用哈希校验是个好方法。此处关联数据就是考虑到这点为此而设的这样我们就不必另外再做数据校验非常地方便。使用的方法还是把字符串转换为 UTF8 字节byte[]aadcertificate.getBytes(StandardCharsets.UTF_8);byte[]keynewbyte[32];byte[]nonce123456789012.getBytes(StandardCharsets.US_ASCII);StringciphertextnewAesGcm(key).encrypt(certificate payload,nonce,aad).toBase64();assertEquals(certificate payload,newAesGcm(key).decrypt(ciphertext,nonce,aad));和 nonce 一样发送端和接收端必须使用完全一致的 associatedData 字节。该特性是可选的。AES-CBCCBC 是 AES 家族里另外一种加密方式相对于 GCM 安全性稍低。微信小程序用户手机号码解密就是使用这个方式。CBC 没有 GCM 的 nonce取而代之的是 IVInitialization Vector初始化向量。GCM nomce 所谓的“随机数”其实并不要求真随机或伪随机甚至带编号的 id 可以例如密钥 K Nonce 001 → 加密消息 A 密钥 K Nonce 002 → 加密消息 B 密钥 K Nonce 003 → 加密消息 C但 CBC 的 IV 不可以必须是不可预测的随机字节。在不同 AES 模式中有不同的名字和规则CBC 模式称为 IVGCM 模式通常称为 Nonce它们的目标有相似之处但不是完全相同的机制。GCM nonce 的核心要求是同一密钥下不可重复CBC IV 则必须不可预测并且每次加密使用新的随机 IV。CBC 本身不提供完整性认证而 GCM 提供认证标签来检测密文或 AAD 是否被篡改。下面是 CBC 加密的例子AesCbcaesnewAesCbc(newbyte[16]);AesCipherResultgeneratedIvaes.encrypt(CBC content);byte[]iv1234567890123456.getBytes(StandardCharsets.US_ASCII);AesCipherResultsuppliedIvaes.encrypt(CBC content,iv);assertEquals(16,generatedIv.getNonce().getResult().length);assertEquals(CBC content,aes.decrypt(generatedIv.toBase64(),generatedIv.getNonce().toBase64()));assertEquals(CBC content,aes.decrypt(suppliedIv.toBase64(),suppliedIv.getNonce().toBase64()));assertThrows(IllegalArgumentException.class,()-aes.encrypt(CBC content,newbyte[15]));解密的例子复制微信的StringsessionKeyIOv62NY75gNbTYVEe1ogWQ;Stringivb/OsOf6y4Hl6RXJWCjQ;Stringciphertext6fxmM2gjyAk5v9mzSnPXw3xv4WTYywHH/JK9A78Zb2K8i9kehzGLd3xalzx8qNgkZ/SG4/kfL8DgpvQBEoygi7K7YNguUW7HNYHkESUiGXIdDGpziBjmxmoPquFZ8N2XF71kn6MYfXVUiwxCHRu5YYlTbKr4IjA2xqKMgAhaK6YsyD1NE9iOH4eYnT9Ky7B54BW0yWVH3NFgkTmEBQTNg;StringdecryptedTextnewAesCbc(sessionKey).decrypt(ciphertext,iv);// Add assertions to validate the decrypted textSystem.out.println(decryptedText);AES-PBE基于密码的 AES 加密如果不想直接管理随机生成的 AES 密钥而是希望使用自己设置的密码可以使用 PBEPassword-Based Encryption基于密码的加密。AES-PBE 并不是一种独立的 AES 加密模式而是先通过 PBKDF2 等密钥派生算法KDF结合密码、随机 Salt盐值和迭代次数生成 AES 密钥再使用 AES-GCM 进行加密。本实现使用 PBKDF2 派生 AES-128 密钥并采用 AES-GCM 提供机密性与完整性认证。每次加密都会生成新的 12 字节随机 Nonce并将其与密文及认证标签组合返回[12-byte Nonce][Ciphertext][16-byte Authentication Tag]Salt 和迭代次数用于重新派生密钥因此解密时必须使用与加密时一致的密码、Salt、迭代次数及密钥派生算法。本实现返回的加密结果不包含 Salt调用方需要自行保存或传输。Salt 和 Nonce 都不需要保密但用途不同Salt 用于密码密钥派生Nonce 用于 AES-GCM 加密操作。用法如下byte[]saltDoCipher.randomBytes(AesPbe.PBE_SALT_LENGTH);AesPbeaesPbenewAesPbe(strong password,salt,AesPbe.MIN_PBE_ITERATIONS);byte[]firstaesPbe.encrypt(secret);byte[]secondaesPbe.encrypt(secret);assertEquals(AesPbe.PBE_SALT_LENGTH,salt.length);assertEquals(secret,aesPbe.decrypt(first));assertFalse(Arrays.equals(first,second),Each encryption must use a fresh GCM nonce);当前 API 还是比较原始并没有融入最新的 API 体系中。他日有时间再优化。RSA 非对称加密RSA 是一种非对称密码算法使用一对数学上相关的密钥公钥Public Key可以公开分发用于加密、验证签名。私钥Private Key必须妥善保管用于解密、生成签名。公钥可以从私钥推导出来反之不行。对比 AES 更安全但意味着性能消耗越高且耗时一般可以考虑混合使用。RSA 的操作总之记住一句话公钥加密 、私钥解密 私钥签名公钥验签。RSA 生成签名RSA 签名的意义在于这份数据确实是私钥持有者签发的而且数据在签名后没有被篡改也就是数据完整性的校验。一般提到签名我们很容易想到哈希算法比如 MD5、SHA-1、SHA-256 等等。只是说 MD5/SHA-1 已经不再安全一般不推荐使用。本 API 默认算法便是 SHA-256。类DoSignature复制生成签名用法很简单// 先生成必须得私钥KeyPairpairRestoreKey.generateKeyPair(2048);byte[]datasigned payload.getBytes(StandardCharsets.UTF_8);byte[]signaturenewDoSignature(pair.getPrivate()).sign(data);// 直接传入字符串也可以byte[]signaturenewDoSignature(pair.getPrivate()).sign(signed payload);既然我们说私钥生成签名那么自然私钥得入参是必须的。当前 API 支持两种方式入参一个是PrivateKey对象传入构造器另外一个 包含私钥的 PEM 字符串。如果不想返回byte[]可以调用signToBase64()返回 String。关于签名得算法默认是 SHA256_RSA当然你也可以通过 setter 指定其他得算法。RSA 验签 verify既然有了签名就得拿来验证签名也就是“验签”这一过程就是通过公钥验证签名是否由对应私钥生成的。如果验签通过表示1、数据未被修改2、签名者拥有对应私钥。RSA 的验签实际非常简单在类DoVerify完成只需下面一个函数即可完成。publicbooleanverify(byte[]data,byte[]signatureData){if(ObjectHelper.isEmptyText(algorithmName))thrownewIllegalArgumentException(Signature algorithm is required.);if(datanull)thrownewIllegalArgumentException(Data to verify is required.);if(signatureDatanull)thrownewIllegalArgumentException(Signature data is required.);if(publicKeynull)thrownewIllegalArgumentException(Public key is required.);try{SignaturesignatureSignature.getInstance(algorithmName);signature.initVerify(publicKey);signature.update(data);returnsignature.verify(signatureData);}catch(SignatureExceptione){thrownewIllegalStateException(Signature verification failed.,e);}catch(NoSuchAlgorithmExceptione){thrownewIllegalArgumentException(DoCipher.NO_SUCH_ALGORITHMalgorithmName,e);}catch(InvalidKeyExceptione){thrownewIllegalArgumentException(Invalid Public Key,e);}}问题是入参比较多且数据类型多样。典型的入参需要如下公钥 publicKey 可以是PublicKey或其 Base64 字符串在构造器参数传入算法名称 algorithmName算法一般是SHA256withRSA可以通过 setter 另外指定原始数据的摘要 data可以是byte[]或其 UTF8 字符串在verify方法传入签名数据 signatureData可以是byte[]或其 Base64 字符串在verify方法传入由此可见DoVerifyAPI 的风格也是与DoSignature类似用法如下KeyPairpairRestoreKey.generateKeyPair(2048);byte[]datasigned payload.getBytes(StandardCharsets.UTF_8);byte[]signaturenewDoSignature(pair.getPrivate()).sign(data);DoVerifyverifiernewDoVerify(pair.getPublic());assertTrue(verifier.verify(data,signature));assertTrue(verifier.verify(data,newcom.ajaxjs.util.Base64Utils(signature).encodeAsString()));assertFalse(verifier.verify(altered payload.getBytes(StandardCharsets.UTF_8),signature));RSA 加密解密写到这加密解密反而没啥好说——先列出支持的 RSA 算法/** * RSAES-PKCS1-v1_5 transformation; it does not use OAEP parameters. */privatestaticfinalStringRSA_PKCS1RSA/ECB/PKCS1Padding;/** * Generic OAEP transformation, explicitly configured as SHA-1/MGF1-SHA-1. */privatestaticfinalStringRSA_OAEP_SHA1RSA/ECB/OAEPPadding;/** * RSA-OAEP compatibility transformation using SHA-1 for both the OAEP digest and MGF1 digest. * * pUse only when an external protocol explicitly requires SHA-1 OAEP. * New integrations should use the default SHA-256 transformation./p */publicstaticfinalStringRSA_CIPHER_SHA1RSA/ECB/OAEPWithSHA-1AndMGF1Padding;/** * Primary RSA cipher transformation. * * pThis transformation uses RSA OAEP padding with SHA-256./p */privatestaticfinalStringRSA_CIPHER_SHA256RSA/ECB/OAEPWithSHA-256AndMGF1Padding;/** * RSA-OAEP transformation using SHA-384 for both OAEP and MGF1 digests. */privatestaticfinalStringRSA_CIPHER_SHA384RSA/ECB/OAEPWithSHA-384AndMGF1Padding;然后大家看例子即可。// 生成公钥私钥KeyPairpairRestoreKey.generateKeyPair(2048);StringpublicPemPemUtils.publicKeyToPem(pair.getPublic());StringprivatePemPemUtils.privateKeyToPem(pair.getPrivate());Stringword你好世界;RsapublicCiphernewRsa(publicPem,true);RsaprivateCiphernewRsa(privatePem,false);byte[]encWordpublicCipher.encrypt(word).getResult();StringeBodypublicCipher.encrypt(word).toBase64();StringdecWord2privateCipher.decrypt(newBase64Utils(encWord).encodeAsString());System.out.println(加密前: word\n\r密文eBody\n解密后: decWord2);assertEquals(word,decWord2);StringenglishHello, World!;byte[]encEnglishpublicCipher.encrypt(english).getResult();StringdecEnglishprivateCipher.decrypt(newBase64Utils(encEnglish).encodeAsString());System.out.println(加密前: english\n\r解密后: decEnglish);assertEquals(english,decEnglish);// 产生签名StringsignnewDoSignature(privatePem).signToBase64(encEnglish);// 验证签名assertTrue(newDoVerify(publicPem).verify(encEnglish,sign));小结不知不觉洋洋洒洒地写了一万多字于是后面的笔者匆匆结尾了——其实关于“证书、Key公钥私钥、PEM、X509”这些区别与联系的还可以再说说的不过文章太长了还是趁此打住——咱们以后有机会再讲。对了其实这个加密解密组件是 aj-util 库的一部分源码就在https://gitcode.com/lightweight-component/aj-util/tree/main/aj-util/src/main/java/com/ajaxjs/util/cryptography欢迎批评、斧正
RELATED

相关推荐

网络拓扑结构大全:从星型到网状的实战选型与避坑指南

网络拓扑结构大全:从星型到网状的实战选型与避坑指南

简介:面向网络初学者与备考者的网络拓扑图解资料,系统整理了星型、总线型、环型、树型、分布式与网状六类常见拓扑,并补充了扩展星型、混合型等变体。文档逐类讲解组网方式和适用场景:星型依赖中心节点集中转发,便于管…

📅 2026/10/4 17:23:16
能做家庭影音的迷你主机有哪些:来酷 AI MINI 的 Windows 11 与 HDMI 2.1 接口实战

能做家庭影音的迷你主机有哪些:来酷 AI MINI 的 Windows 11 与 HDMI 2.1 接口实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/4 17:23:16
岛屿模型:各子群独立进化如何破解多峰优化难题

岛屿模型:各子群独立进化如何破解多峰优化难题

去年做演化计算项目时,我把一个种群拆成 8 个互不干扰的子群,让它们各自独立进化了大几十代,结果解决了单个大种群死活收敛不动的多峰问题。这法子听着玄,其实就是把“各子群独立进化”这个思路用到了优化算法里——整个种群被分成…

📅 2026/10/4 17:23:16
MORE NEWS

更多资讯

📰

【八个月网安课程】第八周·周六:靶场命令/代码执行漏洞综合通关——从单点利用到攻击链

以下是第八周周六的详细学习内容。今天将不再有新的漏洞类型,而是将你过去一周所学的目录遍历、文件包含、命令注入(有回显与无回显)全部串联起来,在多个靶场场景中完成综合实战。你将脱离“单漏洞利用”的思维,开始构…

📰

从IDE到ADE:智能体开发环境的核心技术与落地实践

1. 从IDE到ADE:开发环境正在经历一次范式迁移如果你最近在开发者社区里频繁看到"ADE"这个词,不是拼写错误,也不是某个新品牌的缩写,而是智能体开发环境(Agentic Development Environment)。这个词…

📰

Cursor插件开发核心机制与避坑指南

1. “plugins”不是功能菜单,而是Cursor生态的神经中枢很多人第一次在Cursor里点开Settings → Extensions,看到“Plugins”标签页时,下意识以为这只是个“插件市场入口”——就像VS Code里点Extensions Marketplace那样,搜一搜、…

📰

90DaysOfDevOps 第 30 天:Microsoft Azure 安全模型——身份、目录、RBAC 与 Azure Policy 实战

文档/教程 【免费下载链接】90DaysOfDevOps This repository started out as a learning in public project for myself and has now become a structured learning map for many in the community. We have 3 years under our belt covering all things DevOps, including Pri…

📰

DeepSeek Harness桌面端安装部署与插件Skill机制全解析

1. 从命令行到桌面窗口:DeepSeek Harness 桌面端到底解决了谁的痛点第一次听说 DeepSeek Harness 出了桌面端,我的反应是"终于有人干了这件事"。如果你之前用过命令行版本的 Harness,应该能理解那种感受——功能确实强,…

📰

用Qt实现鼠标自动点击:从GetCursorPos到mouse_event的完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬