FEATURED · 精选文章

Java加密算法实战:AES、RSA、SHA与HMAC完整指南

发布时间 / 2026/9/13 20:13:56
来源 / 创域科博编辑部
栏目 / 资讯中心
Java加密算法实战:AES、RSA、SHA与HMAC完整指南 1. 内容整体设计与思路拆解1.1 加密算法要解决的三个核心问题如果你参加过Java后端面试大概率被问过“加密算法有哪些”“AES和RSA有什么区别”这类问题。但面试归面试真正落到项目里我发现不少同事对加密算法的理解停留在“知道名字”的层面真要写代码就露馅了。这篇文章把Java开发中最常用的散列算法、对称加密、非对称加密和HMAC一次性讲清楚每个都有可直接运行的代码你照着抄就能用。在学习具体算法之前我们先想清楚加密算法到底在解决什么问题。我习惯把它拆成三个核心诉求机密性、完整性、真实性。机密性很好理解就是数据不能被别人看到对应对称和非对称加密完整性解决的是数据在传输或存储过程中有没有被篡改常用散列算法来保证真实性则是确认数据确实来自声称的发送方涉及签名和消息认证码。这三个诉求经常混在一起用比如一个完整的接口安全方案往往同时用到AES、RSA和SHA系列算法。用生活场景类比一下你想给朋友寄一封机密的信机密性就是把信装进保险箱完整性是保险箱上的封条有没有被人撕开过真实性则是保险箱上刻的签名是不是朋友本人的。三种能力互不替代各自解决不同问题。搞清楚了这层逻辑选型就不会乱。1.2 算法分类与技术选型的核心逻辑从实现和密钥管理的角度Java里常用的算法可以分成四大类。我做了个表方便你对照着看算法类别代表算法密钥特点主要用途性能特点散列算法MD5、SHA-1、SHA-256无密钥完整性校验、口令存储快对称加密AES、DES、3DES同一个密钥加解密大数据量加密传输快非对称加密RSA、EC、DSA公钥/私钥成对密钥交换、数字签名慢消息认证码HMAC-MD5、HMAC-SHA256共享密钥防篡改身份认证快选型的核心逻辑就一句话场景决定算法算法决定密钥管理方式。比如你要加密大量业务数据AES是标配因为快且安全但AES的密钥怎么安全地传给对方这就轮到RSA登场了。实际项目里最常见的组合拳是RSAAES用RSA加密AES的密钥用AES加密真正的数据。这样做既发挥了RSA在密钥管理上的优势又避免了RSA性能差、加密长度受限的短板这是Java安全通信中最经典的一套混合方案。反过来如果只是存口令就完全不应该用AES或RSA因为可解密的算法都不适合存口令一旦数据库泄露攻击者只要有密钥就能还原所有密码。正确做法是散列加盐最好直接用BCrypt这类自适应慢哈希把碰撞和暴力破解的成本拉满。1.3 从面试到落地这篇文章的编写逻辑我写这篇文章的初衷很直接把面试八股文和项目实战打通。很多Java程序员背得下AES是高级加密标准、RSA是非对称加密但问他AES的CBC模式为什么要IV、RSA加密明文长度为什么有限制、JDK8跑256位AES报错怎么处理就哑火了。这些细节恰恰是项目里最容易踩坑的地方。所以这篇文章的安排是第2章讲清每个算法的原理和适用边界这是选型的依据第3章给出完整可运行的代码覆盖散列、AES、RSA和HMAC并解释参数选择的理由第4章集中排查实战中高频出现的异常和问题。你可以把它当成一份Java加密的工具手册用到的时候直接翻对应章节。2. 核心细节解析与实操要点2.1 散列算法MD5、SHA系列的原理与安全边界散列算法的特点是不可逆输出固定长度输入微小变化输出差异巨大。MD5输出128位即32个十六进制字符SHA-1输出160位SHA-256输出256位。MD5和SHA-1已经被学术界证明存在碰撞攻击SHA-1的碰撞攻击成本甚至已经降到可以实际执行。所以凡是依赖“不同输入必然产生不同摘要”的安全场景MD5和SHA-1都已经不再合格。但也不要一棍子打死。MD5在非安全场景下用处依然很多比如下载文件后算一个MD5值来校验文件是否完整、日志里去重、缓存键生成等。只要不涉及对抗恶意攻击MD5的高性能和实现简单就是优势。而涉及安全场景最低标准是SHA-256这是目前行业的基本共识。口令存储这里要单独强调直接用SHA-256加盐也不够理想因为SHA系列属于快速散列GPU和专用硬件可以做到每秒数十亿次计算暴力破解非常快。我现在做口令存储都会优先选BCrypt因为它的设计目标就是“故意慢”可以通过cost因子调整计算耗时让攻击者付出极高的破解代价。这个建议展开说就是安全场景选专门为口令设计的慢哈希算法通用场景选SHA-256普通字符串校验任务才轮到MD5。2.2 对称加密AES的工作模式与填充机制AES是目前对称加密的事实标准密钥长度支持128、192、256位。密钥越长越安全但性能差异很小所以安全要求高的场景直接上256位。AES有几种常见工作模式理解它们比背代码更重要。ECB模式最简单也最不安全。同一个明文块会被加密成同一个密文块从密文中能看出明文数据的重复模式所以ECB只适合加密随机数据或单块数据加密有规律的业务数据时坚决不用。CBC模式加了一个初始向量IV每个明文块先与前一个密文块异或再加密解决了ECB的重复模式问题是目前兼容性最好的通用选择但需要注意IV不能重复使用而且CBC不支持并行加密。GCM模式增加了认证能力加密的同时生成认证标签能检测密文是否被篡改安全等级更高Java 8之后原生支持新项目我建议优先考虑。填充模式也要提一嘴最常见的是PKCS5Padding它保证明文长度不是块大小的整数倍时补足如果明文本身刚好是16的倍数会额外补一整块16字节的填充解密时再自动去掉。这带来一个很可能遇到的现象AES加密同一个值每次结果都不同CBC模式随机IV时或者加密相同内容得到相同结果的反而要警惕——前者是正常安全表现后者则说明IV设计有问题。2.3 非对称加密RSA原理、性能短板与签名机制RSA的安全性基于大整数分解问题公钥和私钥成对出现。公钥加密的数据只能用私钥解密私钥签名产生的签名只能用公钥验证。密钥长度常见1024、2048、4096位1024位已经不被安全社区推荐新项目最低直接用2048位条件允许上4096位。RSA有个新手必踩的坑公钥加密有长度限制。使用PKCS1Padding时一次能加密的明文最大长度是密钥长度字节数减去11。我用2048位密钥举例2048位等于256字节减去11等于245字节也就是说一次最多只能加密245字节的明文。超过这个长度必须分段加密或者干脆换方案——用AES加密大段数据用RSA加密AES密钥这就是第1章说的混合加密方案。RSA还有两种完全不同的用法加密和签名。加密用公钥做密文解密用私钥签名则反过来私钥做签名公钥验签。很多初学者会把“RSA加密”和“RSA签名”混为一谈实际上它们是两个操作流程底层虽然共用同一套密钥对但语义完全不同。加密保护的是数据的机密性签名保护的是数据的真实性和不可否认性。在代码里对应Cipher和Signature两套API别搞混。2.4 HMAC消息认证码与盐值设计要点HMAC全称是Hash-based Message Authentication Code即基于散列算法的消息认证码。它和普通散列的核心区别在于引入了密钥。普通SHA-256任何人都可以算无法证明数据来源HMAC则要求计算方持有共享密钥只有持有同一密钥的双方才能生成和验证摘要因此同时具备完整性和真实性两个能力。HMAC的典型应用场景是接口签名。前后端共享一个secret发起方把请求参数拼接后用HMAC-SHA256计算签名接收方用同样的secret重算一次两个值一致就代表参数没被篡改且确实来自持有secret的合法调用方。在设计签名规则时要注意参与签名的字段顺序要固定否则两边拼出来的串不一样secret不能跟着请求参数一起传一般通过请求头传递或提前线下约定必要时可以加入时间戳字段防止重放攻击。加盐设计这里再补充一点。散列算法里“盐”的英文是salt它与HMAC的密钥key作用类似但不相同。加盐是为了对抗彩虹表盐值必须随机、每个用户独立、长度至少16字节并且要和散列结果一起存储。千万不要使用项目名、用户名这类可预测的值当盐否则等于没加。3. 实操过程与核心环节实现3.1 开发环境与编码约定本文代码基于JDK 8以上版本核心依赖只有java.base不需要引入任何第三方包。Base64编码用JDK自带的java.util.Base64所有乱码类问题建议先检查是不是编码字符集不一致代码里统一使用UTF-8。这个前置约定看起来不起眼实际排查问题的时候能帮你省大量时间。加密领域里Base64和十六进制是用来描述字节的两种编码方式很多入门者在这里栽跟头。AES和RSA的加解密结果都是byte[]直接转String会乱码必须先编码成可打印的字符串再传输或存储。推荐用Base64因为编码后的字符串更短Hex则更直观适合肉眼调试。两者可互相转换核心是别把这两层编码当成加密。3.2 散列算法的Java实现与文件校验先看一个散列工具类覆盖MD5和SHA-256代码直接可运行。import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; public class HashUtil { private static final String MD5 MD5; private static final String SHA_256 SHA-256; private static final String UTF_8 UTF-8; /** * 计算字符串的MD5值返回32位小写十六进制字符串 */ public static String md5(String input) { return hashToString(input, MD5); } /** * 计算字符串的SHA-256值返回64位十六进制字符串 */ public static String sha256(String input) { return hashToString(input, SHA_256); } private static String hashToString(String input, String algorithm) { try { MessageDigest digest MessageDigest.getInstance(algorithm); byte[] bytes digest.digest(input.getBytes(UTF_8)); return bytesToHex(bytes); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(不支持的算法: algorithm, e); } catch (Exception e) { throw new RuntimeException(散列计算异常, e); } } private static String bytesToHex(byte[] bytes) { StringBuilder sb new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } }使用方式很简单HashUtil.md5(hello)和HashUtil.sha256(hello)直接返回字符串。项目里如果要做文件完整性校验可以读文件的字节数组再走同样的散列流程。这里有个小性能建议校验大文件时用digest.update()分块喂数据不要一次把整个文件读进内存避免撑爆堆内存。还有一个基础知识点顺便复习一下b 0xff是把byte转成无符号整数否则负数会转成超长的十六进制串。这种细节我在初学的时候踩过写出来给大家避坑。3.3 AES加解密的完整实现与参数说明AES代码我给出CBC和GCM两个版本。CBC版本适用面最广网上资料最多GCM模式安全性更高还能校验数据完整性新项目建议优先用它。import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; import java.util.Base64; public class AesUtil { private static final String AES AES; private static final int AES_KEY_SIZE 256; /** * CBC模式加解密IV长度16字节 */ public static String encryptCbc(String plaintext, String base64Key, String base64Iv) throws Exception { byte[] keyBytes Base64.getDecoder().decode(base64Key); byte[] ivBytes Base64.getDecoder().decode(base64Iv); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted cipher.doFinal(plaintext.getBytes(UTF-8)); return Base64.getEncoder().encodeToString(encrypted); } public static String decryptCbc(String ciphertext, String base64Key, String base64Iv) throws Exception { byte[] keyBytes Base64.getDecoder().decode(base64Key); byte[] ivBytes Base64.getDecoder().decode(base64Iv); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(ciphertext)); return new String(decrypted, UTF-8); } /** * GCM模式加解密IV长度12字节GCM推荐使用12字节 */ public static String encryptGcm(String plaintext, String base64Key, byte[] iv) throws Exception { byte[] keyBytes Base64.getDecoder().decode(base64Key); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); GCMParameterSpec gcmSpec new GCMParameterSpec(128, iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); byte[] encrypted cipher.doFinal(plaintext.getBytes(UTF-8)); return Base64.getEncoder().encodeToString(encrypted); } public static String decryptGcm(String ciphertext, String base64Key, byte[] iv) throws Exception { byte[] keyBytes Base64.getDecoder().decode(base64Key); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); GCMParameterSpec gcmSpec new GCMParameterSpec(128, iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(ciphertext)); return new String(decrypted, UTF-8); } public static String generateKeyBase64() throws Exception { KeyGenerator keyGen KeyGenerator.getInstance(AES); keyGen.init(AES_KEY_SIZE); SecretKey key keyGen.generateKey(); return Base64.getEncoder().encodeToString(key.getEncoded()); } }CBC模式下每次加密都应该使用随机生成的16字节IV同一个密钥下IV重复会导致安全性大幅下降。IV不需要保密通常直接拼接在密文字符串前面传输接收方从中截取再解密。GCM模式的IV推荐12字节认证标签长度用128位也就是GCMParameterSpec(128, iv)里的第一个参数。代码里我用了SecretKeySpec而不是SecretKeyFactory原因是AES的密钥就是固定长度的随机字节直接用字节构造密钥规格更简单也较少出错。如果是从KeyGenerator生成的密钥直接getEncoded()拿到字节数组再Base64编码存起来即可后续解密用同一个Base64字符串构造SecretKeySpec就好了。3.4 RSA加解密、签名验签的完整实现RSA的实现分成两块加解密和签名验签。因为代码较长我把核心方法放在同一个工具类里密钥对用字符串形式保存方便配置到文件或数据库。import javax.crypto.Cipher; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class RsaUtil { private static final String RSA RSA; public static KeyPair generateKeyPair() throws Exception { KeyPairGenerator generator KeyPairGenerator.getInstance(RSA); generator.initialize(2048); return generator.generateKeyPair(); } public static String getPublicKeyBase64(KeyPair keyPair) { return Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()); } public static String getPrivateKeyBase64(KeyPair keyPair) { return Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()); } public static String encrypt(String plaintext, String publicKeyBase64) throws Exception { PublicKey publicKey restorePublicKey(publicKeyBase64); Cipher cipher Cipher.getInstance(RSA); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encrypted cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } public static String decrypt(String ciphertext, String privateKeyBase64) throws Exception { PrivateKey privateKey restorePrivateKey(privateKeyBase64); Cipher cipher Cipher.getInstance(RSA); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(ciphertext)); return new String(decrypted, StandardCharsets.UTF_8); } public static String sign(String data, String privateKeyBase64) throws Exception { PrivateKey privateKey restorePrivateKey(privateKeyBase64); Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(signature.sign()); } public static boolean verify(String data, String signBase64, String publicKeyBase64) throws Exception { PublicKey publicKey restorePublicKey(publicKeyBase64); Signature signature Signature.getInstance(SHA256withRSA); signature.initVerify(publicKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); return signature.verify(Base64.getDecoder().decode(signBase64)); } private static PublicKey restorePublicKey(String publicKeyBase64) throws Exception { byte[] keyBytes Base64.getDecoder().decode(publicKeyBase64); X509EncodedKeySpec keySpec new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(RSA); return keyFactory.generatePublic(keySpec); } private static PrivateKey restorePrivateKey(String privateKeyBase64) throws Exception { byte[] keyBytes Base64.getDecoder().decode(privateKeyBase64); PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(RSA); return keyFactory.generatePrivate(keySpec); } }签名算法的选择我用了SHA256withRSA也就是先对原始数据做SHA-256散列再做RSA签名。这样做的好处是签名速度快、签名结果长度固定而且比直接对大段数据做RSA运算更安全。验签时的数据必须和签名时的数据完全一致包括字符编码和字段顺序否则验证必然失败。使用这套工具类有个很重要的注意点公钥和私钥保存的是X.509和PKCS#8标准格式经Base64编码后的字符串生成一次后就不要频繁更换。密钥轮换时要做到新旧密钥并存一段时间避免正在传输中的密文突然无法解密。3.5 场景化参数计算RSA长度限制与AESRSA混合加密最后一个实操主题说说参数计算。有人问为什么RSA加密一长串字就抛异常这就要算一下。以1024位密钥为例密钥长度为128字节减去11等于117字节一次最多加密117字节明文2048位密钥最多245字节。如果明文是包含中文的字符串UTF-8下一个汉字占3字节所以2048位密钥一次也仅够容纳七八十个汉字。超过就得换方案。实际项目里的标准解法是混合加密。客户端生成一个随机的AES密钥用AES加密业务数据再用服务端的RSA公钥加密这个AES密钥最后把AES密文和RSA密文一起发给服务端服务端先用RSA私钥解出AES密钥再用AES密钥解出业务数据。这套流程兼顾效率和安全性很多开放平台网关的核心加密链路就是这个思路。实现时给RSA加密的数据固定使用Base64编码避免二进制数据传输中的编码问题AES密钥随机生成一次即可不需要重复使用同一个密钥。4. 常见问题与排查技巧实录4.1 高频异常与解决方案速查表我整理了项目里最常见的七个加密相关异常每一条都是真实踩过的坑按出现频率排了个序异常信息根因解决方案java.security.InvalidKeyException: Illegal key sizeJDK8默认不支持256位AES密钥安装Oracle官方JCE无限制权限策略文件或升级到JDK9javax.crypto.IllegalBlockSizeException: Data must not be longer than...RSA明文超过长度限制改用分段加密或使用AESRSA混合方案javax.crypto.BadPaddingException: Given final block not properly padded密钥错误、密文被篡改、填充模式不一致检查解密密钥是否与加密时一致密文是否完整java.security.NoSuchAlgorithmException: Cannot find any provider supporting AES/GCM/NoPaddingJDK版本过低升级JDK8以上版本java.security.InvalidAlgorithmParameterException: IV length must be 16 bytesCBC模式IV长度必须16字节GCM推荐12字节确认IV字节数组长度java.lang.IllegalArgumentException: Last encoded character (before Base64 trailer) is a valid base64 alphabet character...Base64字符串被截断或包含多余字符检查Base64字符串完整性去除换行符SignatureException: Signature length not correct签名与验签算法不匹配或密钥对不对应确认签名算法检查公钥私钥是否同一对这些异常本身并不难解决难的是快速定位。我的排查习惯是先确认密钥是否一致再确认算法和模式是否匹配最后检查数据在传输过程中有没有被处理过。90%的加密解密失败问题都出在这三处。4.2 独家避坑经验IV设计、编码选择与密钥管理在项目里实际跑加密功能有几个细节是官方文档不会专门强调、但踩过一次就印象深刻的。CBC模式的IV设计一定不能忽略。我接手过一个老项目IV写死在代码里且全局固定结果测试环境发现相同明文加密结果完全相同。IV重复会导致相同明文块产生相同密文块攻击者可以通过分析密文模式推断明文信息这属于严重的安全设计缺陷。正确做法是每次加密生成随机IV把IV拼在密文前面一起传输服务端解密时再截取前16字节做IV。编码选择也要注意。AES和RSA加密后得到的是二进制字节转字符串只有两种正确姿势Base64或Hex。我见过有人直接new String(byte[])结果解密时数据已经不是原来的字节序列报错还是小事更怕的是数据在数据库里被字符集转换悄悄改掉。指定列存储时建议用varchar存Base64字符串别用text类型避免部分数据库对text有特殊处理。密钥管理是个老生常谈但总有人翻车的话题。不要把AES密钥、RSA私钥硬编码在Java代码里通过Git提交到仓库。哪怕项目是私有仓库一旦代码外泄密钥就全废了。稳妥的做法是把密钥放在独立的配置中心或环境变量中至少也要放到classpath外的配置文件里并且加入.gitignore。高安全要求的系统建议引入专门的密钥管理服务比如KMS由系统负责密钥的生命周期管理。至少要做到开发、测试、生产环境使用不同的密钥否则环境切换时一把密钥处处通用出问题就是全局事件。结尾我在实战中最大的感受是加密算法的代码本身不难写难点全在设计决策上该用哪一种算法、密钥长度选多少、怎么存、怎么轮换、怎么保证前后端不串味。这篇文章里给的代码和参数都是我反复在项目里验证过的方案你可以直接当作工具类使用也可以根据业务场景调整细节。比如你用的是Spring Boot可以包一层加密服务把密钥管理放到配置中心再加一个切面统一的加解密入口会比到处散落调用更可控。加密这条线没有那种“一篇看完就不用再学”的捷径但只要把散列、对称、非对称、HMAC这四个基础模型吃透后面再遇到SM2、SM4这些国密算法或者BCrypt、Argon2这类新方案你都能很快看懂它们的定位和取舍逻辑。希望这篇文章能帮你在面试里答得从容也在项目里少踩几个坑。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻