md5() 给 32 位十六进制,sha1() 给 40 位,
都是「摘要」——不可逆,同一输入永远同一输出。
正因为永远同一输出,裸存 md5 等于没加密:md5('123456') 在哪儿都是 e10adc39…,
拖库的人查彩虹表就还原了。
加盐能解决这个问题:
md5(concat('x7#', '123456')) 和 md5(concat('q2@', '123456')) 给两个完全不同的结果。
上一类算排名,这一篇算摘要——用途跟「排名」完全不搭,但都属于内置函数。
这一篇最该带走的不是函数怎么用,而是「什么时候不该用」。
md5 被误用来存密码,是十几年来最常见的安全事故之一。
一、md5 与 sha1:固定长度的指纹
md5() 不管输入多长,输出永远是 32 个十六进制字符(128 位)。
sha1() 给 40 位(160 位)。
它们是「摘要」不是「加密」:没有密钥,也就没法解密。 只能拿「另一个输入的摘要」来比对,
这正是它们被用来存密码的原因,也是出问题的原因。
md5('123456') 在任何机器、任何数据库、任何语言里都是同一串。
这个「稳定」是它的价值,也是它的漏洞。
二、为什么裸存 md5 等于没加密
同一个密码算两次,结果完全一样,比值给 1。 这意味着「拖走用户表」的人不需要解密——
他只要把常见密码的 md5 算一遍做成表(彩虹表),一比对就还原了。
123456 这种密码的 md5,网上直接就能搜到原文。
也就是说,存了 md5 的密码库,弱密码基本等于明文。
还有一层:md5 算得很快。
现代显卡每秒能算几十亿次,暴力枚举短密码根本不算事。 这两条加起来,
md5 存密码就彻底不成立了。
三、加盐:同一串密码算出不同结果
做法:给每个用户生成一个随机串(盐),存密码时算 md5(盐 + 密码),盐和摘要一起存进表里。
效果:同一个 123456,配不同的盐算出两个完全不同的摘要。
彩虹表就没用了——它得为每个盐单独做一张表,成本乘上用户数。
加盐的要点:每个用户一个、随机生成、够长(16 字节以上)。
全库共用一个盐等于没加,因为攻击者只要针对那一个盐重建一次表。
四、正经做法:别用 md5 存密码
严格的做法是用「故意算得慢」的算法:bcrypt、scrypt、argon2。
它们每次算要几十到几百毫秒,正常登录感觉不到,但暴力枚举的速度被压到几乎没法用。
这些算法在 SQL 里没有,得在程序里做(PHP 的 password_hash、Python 的 bcrypt)。
SQL 的 md5 只适合「校验」不适合「保密」。
MySQL 8 里 sha2() 还能用(sha2(str, 256) 给 64 位),
比 md5 强一点,但同样是「算得快」的摘要,存密码依旧不合格。
本沙箱的 sha2 是垫片,行为一致。
五、md5 的正经用途
一、内容比对:判断两条长文本是不是一模一样,比字符串逐字比快得多。 二、文件校验:下载页给一个 md5,校验文件有没有传坏。 注意这只能发现「损坏」,不能防「被换过」——能改文件的人也能改 md5。 三、生成短标识:长 URL 或长文本 md5 一下,当缓存键、去重键。 这些场景只要求「不同内容几乎不撞」,不要求抗暴力破解,md5 够用。
六、动手 Demo:同一串 123456,三种算法三种结果
Demo 四段:md5、sha1、比对「同一输入是否同一输出」、加盐。
第三段是这一篇的核心——它证明的不是「md5 算得对」,而是「md5 每次都一样,所以能被反查」。
import _q46 as Q
c = Q.con()
c.execute("SELECT MD5('123456') AS h")
c.execute("SELECT SHA1('123456') AS h, LENGTH(SHA1('123456')) AS n")
# 同一串每次都一样,所以彩虹表能反查
c.execute("SELECT MD5('123456') AS a, MD5('123456') AS b, "
"MD5('123456') = MD5('123456') AS same")
# 加盐:同一密码算出不同摘要
c.execute("SELECT MD5(CONCAT('x7#', '123456')) AS s1, "
"MD5(CONCAT('q2@', '123456')) AS s2")
实跑输出:
--------------------------------------------------------------------
第二十八课:加密函数,以及为什么不能光用 md5
--------------------------------------------------------------------
md5:固定 32 位十六进制
SQL> SELECT MD5('123456') AS h
h
---------------------------------
e10adc3949ba59abbe56e057f20f883e
(1 行)
**不管输入多长,输出永远是 32 个十六进制字符**(128 位)。
**这是「摘要」不是「加密」——它不可逆。**
**`md5('123456')` 在任何机器、任何数据库上都是这一串。** **正因为固定,它才成了问题**——
下面就演这件事。
sha1:40 位,比 md5 长一点
SQL> SELECT SHA1('123456') AS h, LENGTH(SHA1('123456')) AS n
h n
------------------------------------------------
7c4a8d09ca3762af61e59520943dc26494f8941b 40
(1 行)
**`sha1` 给 40 位,`md5` 给 32 位。** **两个都已经不该用来存密码了**,理由一样,见下一段。
为什么不能裸存 md5:同一串每次都一样
SQL> SELECT MD5('123456') AS a, MD5('123456') AS b, MD5('123456') = MD5('123456') AS same
a b same
-------------------------------------------------------------------------------
e10adc3949ba59abbe56e057f20f883e e10adc3949ba59abbe56e057f20f883e 1
(1 行)
**两次完全一样,比值给 1。** **这意味着:如果有人拖走了整张用户表,他不需要「解密」——**
**把常见密码的 md5 算一遍做成表(彩虹表),一比对就出来了。**
**`123456` 这种密码的 md5,网上直接就能搜到原文。**
加盐:同一串密码算出不同的结果
SQL> SELECT MD5(CONCAT('x7#', '123456')) AS s1, MD5(CONCAT('q2@', '123456')) AS s2
s1 s2
----------------------------------------------------------------------
dd1d4afe2e205a02b849bf1ee03feecf 90f04da1a2f90d5765551d5682ace3eb
(1 行)
**同一个密码 `123456`,前面拼上不同的盐,算出来的摘要完全不同。**
**彩虹表就没用了——它得为每个盐单独做一张表。**
**盐要每个用户单独一个、随机生成、和摘要一起存。**
严格的做法是用 `bcrypt`/`argon2` 这类「故意算得慢」的算法,
**SQL 里的 `md5` 只是教学和校验文件完整性用的。**
正经用法:存校验值、去重、比对
**md5 的正经用途不是存密码**,而是:**比对两条内容是不是一模一样**(比如去重导入的文本)、
**给文件算校验值**、**给长 URL 生成短标识**。
**这些场景只要求「不同内容几乎不撞」,不要求抗暴力破解,md5 够用。**
**一旦涉及「猜不出来」,就得换算法。**
第一段 md5。 e10adc3949ba59abbe56e057f20f883e,32 位。 这一串在网上直接能搜到原文。
第二段 sha1。 40 位,比 md5 长,但同样算得快、同样不能存密码。
第三段同输入同输出。 两列一模一样,比值给 1。 这一格就是「彩虹表能反查」的证据。
第四段加盐。 同一个 123456,配 x7# 和 q2@ 两个盐,摘要完全不同。
30 秒实验
MD5('')会给什么?空字符串也是合法输入吗?MD5('123456 ')与MD5('123456')一样吗?多一个空格会怎样?- 把盐换成
MD5(RAND())生成,会有什么问题?
常见误区
- 用
md5存用户密码。 同一密码永远同一摘要,彩虹表一查就出来,弱密码等于明文。 - 全库共用一个盐。 等于没加盐——攻击者只要针对那一个盐重建一次表。
- 以为 md5 能防篡改。 它只能发现「传坏了」,能改内容的人也能顺手改掉 md5。
自测三问(点开对答案)
1. `md5` 和 `sha1` 的输出各多少位? —— `md5` 32 位十六进制(128 位),`sha1` 40 位(160 位)。 2. 为什么裸存 md5 等于没加密? —— 同一输入永远同一输出,常见密码的 md5 可以预先算成表(彩虹表)反查;而且 md5 算得极快。 3. 加盐要满足哪几个条件? —— 每个用户单独一个、随机生成、足够长(16 字节以上)。全库共用一个等于没加。加密讲完,下一篇是查环境的系统函数——它们不查数据,查的是「我连到了哪儿」。
- ☐ (本篇完,下一篇 6.7 系统函数)