🦏 第6章 内置函数
📄 一句话概括

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 秒实验

  1. MD5('') 会给什么?空字符串也是合法输入吗?
  2. MD5('123456 ') 与 MD5('123456') 一样吗?多一个空格会怎样?
  3. 把盐换成 MD5(RAND()) 生成,会有什么问题?

常见误区

  1. 用 md5 存用户密码。 同一密码永远同一摘要,彩虹表一查就出来,弱密码等于明文。
  2. 全库共用一个盐。 等于没加盐——攻击者只要针对那一个盐重建一次表。
  3. 以为 md5 能防篡改。 它只能发现「传坏了」,能改内容的人也能顺手改掉 md5。
自测三问(点开对答案) 1. `md5` 和 `sha1` 的输出各多少位? —— `md5` 32 位十六进制(128 位),`sha1` 40 位(160 位)。 2. 为什么裸存 md5 等于没加密? —— 同一输入永远同一输出,常见密码的 md5 可以预先算成表(彩虹表)反查;而且 md5 算得极快。 3. 加盐要满足哪几个条件? —— 每个用户单独一个、随机生成、足够长(16 字节以上)。全库共用一个等于没加。

加密讲完,下一篇是查环境的系统函数——它们不查数据,查的是「我连到了哪儿」。

相关笔记