引入

在数据库的user表中我们通常可以看到一个叫做salt的字段,salt大家很容易就知道意思是盐,大家一开始接触可能感觉很奇怪,为何用户要有一个“盐”属性?我们可以上维基查一下定义:

盐(Salt)

在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。

以上这段话是维基百科上中对于 Salt 的定义,但是仅凭这句话还是很难理解什么叫 Salt,以及它究竟在用户表里面会起到什么样的作用。接下来我们就以用户表的设计开始来揭示“salt”的必要性。

数据表设计

直接设计

我们在设计用户表时,首当其冲需要设计用户名(或账号)以及密码,这两个字段可以说是用户表的核心,一个表示用户的唯一身份,一个表示和唯一身份相匹配的登录凭证。所以我们的用户表至少要设计成这样:

字段名 类型 是否为空 说明
…… …… …… ……
username varchar(50) no 用户名
password varchar(100) no 密码
…… …… …… ……

PS:这里设计的数据表默认为MySQL中的表,下同

在这样的数据表中,我们可以直接存储用户名和密码,在进行验证时也可以直接根据用户名匹配密码即可。但是随之带来了安全问题,例如在数据库中存储的是如下数据:

…… username password ……
…… zhangsan 123456 ……
…… lisi lisi ……

一旦数据中的数据被违法获取或者因为机制泄漏,那么用户的密码就形同虚设,数据盗用者可以利用用户名和密码进行各种操作,损害用户的权益。

看到这里,读者想必也能意识到在数据库存储明文密码的危害了。

转化密文

可能有读者会想,明文不行,我用加密算法(下面演示采用md5加密)加密成密文再存不就行了,那我们继续来看:

稍稍对表进行修改,

字段名 类型 是否为空 说明
…… …… …… ……
username varchar(50) no 用户名
pwd_hash varchar(100) no hash运算之后的密码
…… …… …… ……

然后其中的数据变成了:

…… username pass_hash ……
…… zhangsan e10adc3949ba59abbe56e057f20f883e ……
…… lisi dc3a8f1670d65bea69b7b65048a0ac40 ……
…… wangwu e10adc3949ba59abbe56e057f20f883e ……

这样是不是看起来比原来存的密文是不是安全多了,不过细心的读者可能发现了,用户zhangsan和wangwu进行md5运算之后的密码是一样的,代表他们原来的密码也是一样的。那么如果数据盗用者有一本小本本,记录着很多很多的加密前的密码和加密后的值的对应记录,现在计算机的运行速度那么快,简单的密码在被盗取后,也就比原来明文的获取多了一点破解时间而已。这样看来单纯的明文还是不能保证用户的密码安全性。

“加盐”

正如炒菜的味道不够需要加盐一样,原来简单的加密不再能保证100%的安全了,毕竟谁也不知道你的密码没有别人用过或者没有以前被人破解记录过。于是salt应运而生,正如上述维基百科中所述“通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符”,这句话可能还是不太好理解,直接上例子:

字段名 类型 是否为空 说明
…… …… …… ……
username varchar(50) no 用户名
pwd_hash varchar(100) no hash运算之后的密码
salt varchar(30) no 加密用盐值
…… …… …… ……

Salt 可以是任意字母、数字、或是字母或数字的组合,但必须是随机产生的,每个用户的 Salt 都不一样,用户注册的时候,数据库中存入的不是明文密码,也不是简单的对明文密码进行散列,而是 加密(明文密码 + Salt),salt所在的位置任意,例如:

1
2
3
MD5('123456' + '2blfq0jgqus6ki13mkf81xt7dg') = 'a69b7f5a9c65a35eb3d9b826d3bf05b2'
MD5('lisi' + 't5s198thbqveyjactdt7h91bwa') = 'e6e21083351279c61bbe742e536525f4'
MD5('123456' + 'dflhsgyu5flnmwn2qpzoa2mmwq') = '8da7d8f7f313b8adbc82d99bfb98528c'

这种方法相对就已经很保险了,因为数据盗用者无从得知你的盐值到底加在何处,你甚至可以在服务器代码上自定义序列,然后在对应序列位置交错穿插盐值(序列的第n位就是密码第n位插入盐值的位置),例如序列为斐波那契数列{1,1,2,3,5,8,13,……},那么按照密码长度将其穿插进盐值,例如:

1
MD5(2[12]b[3]lf[4]q0[5]jgq[6]us6ki13mkf81xt7dg) = 'c13e8cb039e818b933d4b30e7c348330'

程序设计时应根据业务需求灵活调整,以上序列穿插法仅供参考。

这样一来,破解难度就大大提升,最大程度的保证了用户的密码安全性。

总结

相信读者看到这里,大概心里已经对用户表中salt的存在已经掌握,但是笔者在这里还是要提个醒,虽然进行更严密更复杂的加密手段可以保护用户密码,但是随之而来还有进行解密时的复杂性,会造成用户登录的时间影响,所以建议笔者还是根据实际需求,在验证时间和加密程度中做一个平衡。

参考资料

本文部分内容参考 密码学中的“盐值 Salt”