资讯动态

对称加密、非对称加密与哈希算法:从原理到工程选型全面梳理

发布时间:2026/10/14 18:14:59 来源:尧图企业网站定制
1. 先从一道高频面试题说起两个分类维度经常被搞混刚入行那会儿我在项目里写了一个“加密工具类”把Base64编码、MD5摘要、AES加密全部塞进了一个叫EncryptUtil的类里接口名就一个encode和decode。当时觉得“反正都是把数据变乱码”直到后来做登录模块被同事指着代码问“你这个MD5怎么解密”我才意识到很多人包括当时的我对加密算法最基本的分类逻辑都没搞清楚。这其实也是这个标题最有意思的地方对称加密与非对称加密是按“加密和解密用不用同一个密钥”来分的可逆加密与不可逆加密是按“密文能不能还原成明文”来分的。这两个维度是相互交错的并不是并列关系。拿具体的算法去套AES、DES、SM4既属于对称加密也属于可逆加密RSA、ECC、SM2既属于非对称加密也属于可逆加密MD5、SHA-256、bcrypt属于不可逆加密也就是我们常说的哈希摘要算法。换句话说“可逆/不可逆”是站在结果角度看的“对称/非对称”是站在密钥管理角度看的。搞清楚这个交叉关系比死记硬背一堆算法名字有用得多。这篇文章我会从这两条主线展开逐个把常用的算法拿出来讲明白它是什么、怎么工作的、在真实项目里一般用在哪儿、有哪些坑。内容会贴合后端开发、接口设计、安全加固这些实际场景如果你是刚接触安全这块的开发者或者准备系统整理加密知识可以参考整条梳理思路。先说结论对称加密胜在速度非对称加密胜在密钥分发哈希胜在不可逆。真实项目里几乎没有只用一种算法就能解决所有问题的场景组合使用才是常态。理解了这句话后面所有细节都好办了。2. 对称加密一把钥匙开门速度快但钥匙怎么给是个问题2.1 算法运行的直观过程对称加密是所有加密体系里最直观的。你可以把它想象成一把物理挂锁用这把锁锁上箱子再用同一把钥匙打开。发送方和接收方手里拿的是同一把密钥加密用的密钥和解密用的密钥完全一致。流程拆开看就是四步发送方用密钥K对明文做加密运算得到密文密文通过网络传输给接收方接收方用同一个密钥K做解密运算得到原始明文。这里有一个关键问题发送方是怎么把密钥K安全地告诉接收方的如果通过网络直接传那密钥一旦被截获加密就形同虚设。这是对称加密最大的痛点也是后面非对称加密存在的理由之一。2.2 三个主流算法的核心对比对称加密算法很多但真正在工程里频繁出现的其实就那几个。DES数据加密标准1977年由IBM设计密钥长度只有56位。放到今天的硬件条件下穷举攻击已经可以做到在数小时甚至数分钟内破译。我见过很多老系统里还在用DES通常是因为历史包袱太重改造成本高。但新项目里再碰DES基本属于给自己挖坑。3DES就是DES做了三次加密有效密钥长度提升到了112位或168位。安全性比DES好得多但性能只有DES的三分之一左右而且块大小还是64位在现代应用中已经逐渐被AES取代。它最大的存在意义在于兼容旧系统——比如一些金融支付场景的老协议还在用3DES。AES高级加密标准是目前全球使用最广泛的对称加密算法。它支持128位、192位、256位三种密钥长度分组大小固定128位。AES-256的密钥空间是2的256次方这个数字大到什么程度呢哪怕用全球最快的超级计算机去穷举也要远超宇宙年龄的时间才能遍历完。所以从工程层面讲AES-256的暴力破解基本可以忽略不计。除了这三位国内还有一种需要熟悉SM4国密算法中的对称加密标准。分组长度128位密钥长度128位算法结构公开在国内政府和金融行业的合规项目里经常是硬性要求。如果你的系统需要过等保或信创相关的审查SM4大概率绕不开。2.3 分组模式为什么同样的明文会得到不同的密文AES和DES都是分组密码也就是说它们把明文分成固定长度的块一块一块地加密。但是如果你的数据有重复的块直接逐块加密就会导致“相同明文块对应相同密文块”的问题。于是出现了分组模式。常用模式有这几种ECB电子密码本模式最简单的模式每个块独立加密可以并行计算。但相同明文组会产生相同密文组明文的统计规律会直接暴露在密文中。比如加密一张头像图片用ECB模式加密后密文仍然能看出人脸的轮廓因为颜色分布规律被保留了下来。所以这个模式我建议在生产环境直接禁用。CBC密码分组链接模式每个明文块先与前一个密文块异或再加密。第一个块需要和一个随机生成的初始向量IV做异或。同一个明文只要IV不同密文就不同。CBC的缺点是加密过程是串行的不能并行而且解密时需要先解出前一个块才能解当前块。GCM伽罗瓦计数器模式基于CTR计数器模式并行度高同时自带认证能力GMAC。也就是说GCM不仅能加密数据还能校验数据有没有被篡改。这是目前我在所有新项目里的首选模式。2.4 实际编码中的完整示例下面这段Java代码展示了最标准、最安全的AES-GCM用法包含随机IV、密钥建议、密文附带认证标签的做法。import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import java.security.SecureRandom; import java.util.Base64; public class AesGcmDemo { // AES-GCM加密返回 Base64 字符串IV 密文 认证标签 public static String encrypt(String plaintext, SecretKey key) throws Exception { byte[] iv new byte[12]; // GCM 推荐 12 字节的 IV SecureRandom random new SecureRandom(); random.nextBytes(iv); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); byte[] ciphertext cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); byte[] result new byte[iv.length ciphertext.length]; System.arraycopy(iv, 0, result, 0, iv.length); System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length); return Base64.getEncoder().encodeToString(result); } // 解密先从 Base64 中拆出 IV 和密文 public static String decrypt(String data, SecretKey key) throws Exception { byte[] decoded Base64.getDecoder().decode(data); byte[] iv Arrays.copyOfRange(decoded, 0, 12); byte[] ciphertext Arrays.copyOfRange(decoded, 12, decoded.length); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); byte[] plaintext cipher.doFinal(ciphertext); return new String(plaintext, StandardCharsets.UTF_8); } public static void main(String[] args) throws Exception { KeyGenerator kg KeyGenerator.getInstance(AES); kg.init(256); SecretKey key kg.generateKey(); String encrypted encrypt(这是一个需要保护的敏感信息, key); System.out.println(密文 encrypted); String decrypted decrypt(encrypted, key); System.out.println(解密 decrypted); } }几个容易犯的错我在代码里已经刻意规避了IV不能固定必须随机生成并在密文里一起传递。如果IV重复GCM的安全性会崩塌GCM的认证标签长度建议128位解密时如果数据被篡改doFinal会直接抛AEADBadTagException这个异常一定要捕获处理不要把IV和密钥混淆。密钥是双方事先约定好的、保密的IV是随机的、可以公开的。2.5 密钥长度和Key派生一句话说清AES密钥就是一段随机字节16字节128位32字节256位。但日常开发中我们拿到的往往是一个用户输入的口令比如字符串直接拿字符串当密钥是危险的因为口令的熵通常不够。正确的做法是用PBKDF2、bcrypt或HKDF等密钥派生函数把口令转换成固定长度的高质量密钥。比如SecretKeyFactory factory SecretKeyFactory.getInstance(PBKDF2WithHmacSHA256); KeySpec spec new PBEKeySpec(password.toCharArray(), salt, 10000, 256); SecretKey derivedKey factory.generateSecret(spec);这里salt是随机盐10000是迭代次数。迭代次数越高暴力破解成本越大。生产环境建议看着硬件配置调到几万甚至几十万。3. 非对称加密两把钥匙各司其职解决信任和分发难题3.1 公钥和私钥的设计思想非对称加密的思路我一般用“信箱”来类比。公共信箱的投递口所有人都能往里投信用公钥加密但只有拿着信箱钥匙的人能打开取信用私钥解密。在这个体系里公钥是公开的任何人都可以拿到私钥是私密的只有你自己知道。用公钥加密的数据只有对应的私钥能解密反过来用私钥签名的数据大家都可以用公钥验证签名确实是你发出的。这解决了对称加密最头疼的“密钥分发”问题。比如客户端和服务端通信服务端把自己的公钥发给客户端客户端用公钥加密一个随机会话密钥然后传给服务端服务端用私钥解密得到会话密钥。之后双方就用这个会话密钥进行对称加密通信。这个流程就是HTTPS握手和混合加密的基本逻辑。3.2 RSA和ECC家族两种主流的数学根基RSA是目前最普及的非对称加密算法核心基于大整数因子分解难题。两个大素数相乘很容易但从乘积反推出是哪两个素数却极其困难。RSA的密钥长度一般有1024位、2048位、4096位。1024位已经被认为不安全新项目至少要用2048位。ECC椭圆曲线加密则基于椭圆曲线离散对数难题在相同安全强度下ECC的密钥长度比RSA短得多。举个直观数字256位的ECC约等于3072位的RSA。密钥短带来的直接好处是计算量小、存储空间小、带宽占用低这在移动端和物联网场景里优势尤其明显。ECC在工程里通常以两种形式出现ECDHE用于密钥交换前端知名框架的HTTPS握手常用它实现向前保密ECDSA用于数字签名很多区块链和Token场景里都在用。国密标准里对应的非对称算法是SM2同样基于椭圆曲线在合规项目里和SM4配合使用。3.3 RSA和ECC的参数差异对照拿几个实用维度做个表方便选型时快速决策对比维度RSAECC推荐密钥长度2048位起256位起同等安全强度密钥长运算重密钥短运算快典型场景兼容旧系统、签名、密钥传输移动端、IoT、实时交互运算开销相对较大相对较小算法复杂度大整数运算实现简单椭圆曲线运算实现稍复杂实践中还有一点需要特别注意非对称加密不适合加密大块数据。RSA-2048最多只能加密约245字节的明文2048位减去填充开销超过这个长度就需要分段加密不仅慢而且脆弱。所以生产项目里非对称加密通常只用来加密“密钥”本身而不是“数据”本身。3.4 RSA在Java里的一段签名代码签名和非对称加密是两个方向加密是“用公钥加、用私钥解”签名是“用私钥签、用公钥验”。下面是一个标准的SHA256withRSA签名流程import java.security.*; import java.util.Base64; public class RsaSignDemo { public static void main(String[] args) throws Exception { KeyPairGenerator gen KeyPairGenerator.getInstance(RSA); gen.initialize(2048); KeyPair pair gen.generateKeyPair(); PrivateKey privateKey pair.getPrivate(); PublicKey publicKey pair.getPublic(); String content 订单金额100订单号20250101; // 私钥签名 Signature signer Signature.getInstance(SHA256withRSA); signer.initSign(privateKey); signer.update(content.getBytes(StandardCharsets.UTF_8)); byte[] signature signer.sign(); String signed Base64.getEncoder().encodeToString(signature); System.out.println(签名 signed); // 公钥验签 Signature verifier Signature.getInstance(SHA256withRSA); verifier.initVerify(publicKey); verifier.update(content.getBytes(StandardCharsets.UTF_8)); boolean verified verifier.verify(Base64.getDecoder().decode(signed)); System.out.println(验签结果 verified); } }签名的意义不是保密而是防篡改防抵赖。对方用自己的私钥签了名就无法否认这个动作接收方用公钥验签就能确定数据在传输中没有被改过。在支付回调、开放平台接口、版本发布这些对真实性和完整性要求高的场景里签名是标配。3.5 混合加密一次完整的HTTPS简化流程很多读者会问我既然非对称加密解决了密钥传输问题那直接用RSA加密所有数据不就行了性能不允许。公钥加密做一次几百字节的操作耗时可能是毫秒级同样时间AES能加密几个量级的数据。所以实际工程里普遍采用“混合加密”客户端生成一个随机的AES会话密钥临时密钥用服务端公钥RSA或ECC加密这个会话密钥服务端用私钥解密得到会话密钥后续所有业务数据都用AES-GCM加密配合会话密钥传输。这个设计兼顾了非对称加密的安全分发能力和对称加密的高吞吐量。理解了混合加密HTTPS中间人攻击怎么防、证书是干什么的也就顺带通了。4. 不可逆加密单向散列与密码存储为什么说“哈希不是加密”4.1 核心特性为什么哈希不可“解密”不可逆加密的正规名字叫密码散列函数它接受任意长度的输入输出固定长度的摘要。最核心的特性有三个单向性从输出无法推导出输入抗碰撞性很难找到两个不同的输入产生相同的输出雪崩效应输入哪怕只改一个比特输出也会面目全非。因为不可逆所以哈希不能用来做数据加解密它只能用来做“完整性校验”和“口令验证”。每次用户输入口令时系统把输入做同样的哈希运算然后对比库里存的摘要是否一致。即使数据库泄露攻击者拿到的也是密文摘要没法直接还原成明文密码。但这里有个前提如果用户密码本身就是弱密码如123456攻击者可以预先算好海量常见密码的哈希值组成一张彩虹表然后反查摘要。所以拿MD5直接存用户密码在今天是高危行为。4.2 从MD5到SHA-256的演进逻辑MD5曾经是最流行的摘要算法输出128位32个十六进制字符至今很多老系统还在用。2004年前后学者已经发现了非常高效的碰撞方法用普通PC就能构造两个内容不同但MD5相同的文件。今天再做文件校验我是不建议用MD5的除非只是给数据打一个简易指纹。SHA-1的输出是160位前几年Google团队发布了SHAttered碰撞攻击真实地生成了两个不同的PDF文件但SHA-1值相同。所以SHA-1也基本被判了死刑主流浏览器在HTTPS证书里已经彻底放弃SHA-1。SHA-2家族SHA-224、SHA-256、SHA-384、SHA-512是目前应用最广泛的。SHA-256输出256位碰撞攻击在可预见的时间内都不现实。比特币的工作量证明用的就是SHA-256衍生算法。SHA-3是新一代标准设计结构和SHA-2完全不同目前主要作为未来储备日常场景用SHA-256就够了。4.3 慢哈希为什么存密码要用bcrypt而不是SHA-256很多人有一个误区SHA-256不可逆那用它存密码是不是就够了不够。因为普通哈希算法是追求速度的每秒能算几十亿次。攻击者拿到数据库后可以用GPU高速跑字典攻击弱密码瞬间就能试出来。所以存储密码的正确做法是用慢哈希算法也就是故意让哈希计算变得很慢、很耗资源。主流的三个选项bcrypt广泛用于后端框架自带随机盐输出固定60字符算法中有代价因子可以调整运算轮数PBKDF2基于HMAC的迭代口令派生函数可以在不同HMAC算法上调优是各类标准体系里最常见的Argon22015年密码哈希竞赛冠军内存占用高对GPU攻击的抵抗力更强Java和Go生态都有实现。下面是一份选型建议场景推荐方案原因Web应用用户密码存储bcrypt成熟、生态好、自动带盐老系统升级兼容PBKDF2迭代次数可调兼容性好高安全或新系统Argon2抗GPU更优内存参数可控一般完整性校验SHA-256 / SHA-512速度快用于文件、内容摘要4.4 加盐的正确姿势盐Salt是一段随机数据在哈希前和口令拼在一起。作用很简单让相同密码的哈希值也完全不同从而彻底打掉彩虹表的复用性。加盐的三个实践原则每一条都是我踩过坑后总结的盐要足够长至少16字节最好是密码学安全随机数生成器生成每个用户用独立的盐不能全系统共用一个盐盐不需要保密直接和哈希值一起存数据库即可攻击者拿到盐也推不出原始口令。用一个通用伪代码说明正确流程1. 用户注册时输入 password 2. salt random(16字节) 3. hash bcrypt_hash(password, salt, cost12) 4. 数据库存储salt hash 5. 用户登录时输入 password 6. 从数据库中取出 salt 和 hash 7. 用同样的 cost 参数重新计算 bcrypt_hash 8. 如果结果一致则通过有几个细节会坑到新手cost参数太高会让登录接口明显变慢CPU耗尽太低又不够安全Java里BCrypt.gensalt(12)Go里是bcrypt.GenerateFromPassword([]byte(pwd), cost)Node里用bcrypt.hash(pwd, 12)反正核心都是“让计算慢到暴力破解难以承受但又不至于影响用户体验”。5. 真实项目里的选型思路和避坑清单5.1 按场景而不是按算法选型我在调研了几十个项目后悟出一个道理不要先选算法要先定场景。不同的安全需求对应完全不同的方案组合。下面对照知乎上常见的开发场景做个整理业务场景推荐方案理由用户密码存储bcrypt / Argon2不可逆 慢哈希 加盐接口传输数据加密AES-256-GCM速度快自带完整性认证会话密钥协商 / 握手ECDHE或RSA2048解决公钥分发可前向保密开放平台签名RSA-SHA256或ECDSA防篡改、抗抵赖数据库敏感字段AES-256-GCM应用层加密兼顾安全性和可用性文件完整性校验SHA-256快速碰撞难度可接受国密合规项目SM2 SM3 SM4国内政企合规要求如果拿不准某个具体场景我的保守建议是**所有可逆加密一律AES-GCM所有签名一律RSA-2048或ECC-256所有密码存储一律bcrypt或Argon2。**这套组合在现代工程里基本不会出大错。5.2 我见过的五类典型翻车现场第一类用ECB模式加密业务数据。某公司内部后台把用户手机号用AES-ECB加密存库结果相同手机号产生了相同密文导致数据去重分析时可以直接按密文分组整个加密形同虚设。解决方法是迁移到GCM模式同时把旧的密文做一次批量解密再迁移。第二类用固定IV初始化AES-CBC。每个请求都用同一个IV密文的前16字节完全一致针对https这类协议流量观察者就能根据重复密文头判断请求来源。正确做法是每次生成随机IV并随密文一起传输。第三类用MD5或者SHA-256直接存用户密码。这种设计在数据库泄露事件里极其被动攻击者拿到后用彩虹表一查一大半弱密码直接还原。宁愿启动时多花100毫秒也要换成bcrypt。存量数据可以考虑做一轮哈希升级用户下次登录时自动用新算法重新存储。第四类RSA只生成512位密钥。这类代码通常是从网上抄的Demo根本没有考虑安全强度。RSA-512已经被证明能被快速分解任何时候都不要在生产代码里出现小于2048位的RSA密钥。第五类以为Base64就是加密。这是一个更基本的概念混淆。Base64只是一种编码方式把二进制数据转换成可打印字符它没有任何保密能力任何人拿到Base64字符串都能解码还原。编码是可逆的、公开的加密是可逆的、需要密钥的这两件事要分清楚。5.3 HMAC容易被忽略的完整性校验工具最后补一个很多文章不提、但工程里非常常用的工具HMAC基于哈希的消息认证码。它不是用来加密的而是用来验证消息完整性和来源真实性的。HMAC与普通哈希的最大区别是普通哈希任何人都能算HMAC需要密钥。只有持有相同密钥的双方才能计算和验证MAC。所以HMAC常用于接口签名、Webhook回调校验、开放平台防篡改等场景。比如某开放平台的回调通知服务端用AppSecret作为HMAC密钥对请求体按约定拼接后计算HMAC-SHA256客户端收到后同样计算并对比。只要两边结果一致就能确认消息确实是该服务端发出的且在传输过程中没有被修改。HMAC-SHA256在隐私合规和数据防篡改里非常常用面试中经常作为区分“懂不懂工程安全”的隐藏考点。5.4 最后的实操体会说实话加密这块内容多且杂我过去也走过“什么流行用什么”的弯路。后来项目做得多了慢慢形成了一套自己的判断顺序先确认“这个数据能不能脱敏”“这个数据需不需要可逆”“谁持有密钥”然后再去选算法和模式。尤其是对称加密的分组模式和密码存储的慢哈希选择这两块是最容易踩坑、也最影响安全结果的地方。如果你现在正要给一个新系统做安全设计我的建议是把这篇文章里的对照表打印出来贴在工位先从场景出发回过来再对照算法特性。加密不是算法堆得越多越安全而是把每把锁用在它该用的地方。这一点想明白了安全问题其实就解决了一大半。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑