此处简单介绍密码如何保存,或者说其他敏感信息如何保存的方案。不局限于密码,其他敏感信息也可以参考一下,比如用户身份证信息、用户照片等,此类信息一旦验证通过是不能直接保存原信息的,涉及到用户隐私这是不合法的,所以一般方案是验证通过之后加密保存,或者不保存,标记该用户已通过身份验证即可。如要更换信息只能重新验证,跟找回密码类似。下面以密码为例进行说明
密码如何保存
既然是密码,那就不能保存明文,这不用过多解释为什么。
不能保存明文,那只能是密文,加密成密文又分为多种加密方案,如对称加密、非对称加密、散列算法加密等。总的来说,根据密文和明文的可互转换分为两种:加密之后可解密的和加密之后不可解密的。因为密码为了保证不泄露,一旦加密存储之后是不需要再查看它本来密码明文的,如果忘记了只能重置或者找回密码,这是一个意思,都是不能获取到原来密码的方案。所以加密密码基本上采用散列算法加密。
散列算法
散列,又叫哈希(hash),常见的哈希算法有 MD 家族的 MD4、MD5,SHA 家族的 SHA-1、SHA-224、SHA-256、SHA-384,和SHA-512 等。
散列算法的优点:
- 生成定长字符,无论多长的字符串经过哈希算法之后都能获得定长的字符,如经过 MD5 加密的都能得到 32 位长度的字符串;
- 密文不可逆,就是根据加密的密文无法反推出明文,保证了即使知道密文,也不可能获取明文。但是可以用对比密文暴力破解,下面介绍。
散列算法的缺点:
- 哈希碰撞是哈希算法不可避免的缺点。因为是不论多长的输入都能得到定长的输出,所以不可避免的会出现不同的输入会得到相同的输出,打个比方:abc 和 abcd 散列之后都输出 123,当然实际情况没有这么简单。好的哈希算法都是几十万亿、几百万亿才出现碰撞,用世界上最好的计算机计算也要几年时间,比如 SHA1 会有1x10 ^ 48分之一的机率出现相同的密文,这样破解了密码也无意思了,所以这种不考虑。
如何用哈希算法加密密码
直接用保存经过哈希算法得到的字符串是不是就可以了呢?其实真实情况这样是有问题的,经过哈希算法的字符串确实是不可逆推出明文,但是这样还是存在攻击漏洞。下面介绍几种密码攻击的方法,这样才能更好的理解为何要这样而不是那样保存密码
密码攻击
- 猜密码,此种方法虽然可以用设置复杂的密码来防止,但是很多用户还是喜欢那种简单常用的密码,比如 123456、admin123、admin888,或者一些以手机号、身份证后几位等作为密码的,这种就很容易被猜出密码从而进行非法操作,此方法是不知道加密方法的情况下可以用,完全靠猜;
- 字典攻击法,跟猜密码类似,首先准备好一张数据表,里面存一些常用密码、单词等,然后遍历比较经过哈希算法处理的字符,比较成功之后此密码就是用户密码。此方法也是不知道加密方法的情况下可以用;
- 暴力破解,或称为穷举法,将密码进行逐个推算直到找出真正的密码为止,这种方法需要大量的运算,理论上没有破解不了的密码,只是时间太长没有实际意义,一般需要借助工具进行暴力攻击;
- 彩虹表攻击,彩虹表是一个用于加密散列函数逆运算的预先计算好的表, 为破解密码的散列值(或称哈希值、微缩图、摘要、指纹、哈希密文)而准备。一般主流的彩虹表都在100G以上。 这样的表常常用于恢复由有限集字符组成的固定长度的纯文本密码。这是空间/时间替换的典型实践, 比每一次尝试都计算哈希的暴力破解处理时间少而储存空间多,但却比简单的对每条输入散列翻查表的破解方式储存空间少而处理时间多。使用加 salt(一般说法叫盐值) 的KDF函数可以使这种攻击难以实现。
暴力破解与彩虹表攻击的区别:
暴字典攻击、力破解和彩虹表攻击宽泛来说都属于口令破解,但他们使用的场景完全是不一样的。
暴力破解、字典攻击:完全不知道密码----通过系统提供的认证接口不断认证----获取原始口令----处于进入系统前;
彩虹表攻击:已知密码hash值(用户密码密文已泄露的情况)----通过自己的工具及彩虹表不断查找----获取原始口令----处理进入系统后。
那么直接用哈希算法加密密码不可靠,该用什么方法呢?下面介绍一种方案:加盐值加密
加盐值
上面介绍了采用单一的哈希算法加密是不可靠的,所以要加点 “盐” 一起加密。
加盐加密是一种对系统登录口令的加密方式,它实现的方式是将每一个口令同一个叫做”盐“(salt)的n位随机数相关联。无论何时只要口令改变,随机数就改变。随机数以未加密的方式存放在口令文件中,这样每个人都可以读。不再只保存加密过的口令,而是先将口令和随机数连接起来然后一同加密,加密后的结果放在口令文件中。
加盐加密的好处就是,就算用户密码相同,最后得到的密码也不会相同,因为每个用户的盐值是唯一的。可以防止类似彩虹表的攻击,或增加其攻击成本。
PHP 代码演示
下面是一个简单用户表
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增',
`user_name` varchar(10) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '用户名',
`password` char(40) CHARACTER SET utf8 DEFAULT '' COMMENT '密码密文',
`salt` char(32) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '加密盐值',
`create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '注册时间,时间戳',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COMMENT='用户表';
PHP 接收到前端注册用户的信息,做加密之后入库处理
<?php
//假设收到的用户名密码
$userName = 'test';
$pwd = 'Aa123456';
//确保盐值唯一,用 MD5 加密,得到定长字符好存储
$salt = md5(mt_rand(100000,999999) . uniqid());
//采用sha1加密
$password = sha1($pwd . $salt);
$data = [
'user_name' => $userName,
'password' => $password,
'salt' => $salt,
'create_time' => time(),
];
//TODO 入库操作
这是注册,之后登录每次接受到用户密码,先用用户名去获取盐值,拿到盐值之后再和密码按相同算法加密,之后比对密文,相同则登录成功。
其他
单单加密还是保证不了密码的安全,应该结合其他方面一起做处理才能使用户密码更安全,此处介绍其他一些处理
- 密码长度格式设计:比如密码不少于8位、要有大小写字母、数字、符号等,还有用户输错了密码之后不直接提示密码错误,而应该提示:用户名密码不匹配,不提示太过具体的信息,防止攻击者通过枚举法知道用户的登录名,从而在使用猜密码的方式进行破解。
- 或不采取密码方式,采用验证码方式更安全,用户也不需要记登录密码。采用这种方案应记住用户的登录有效期,频繁的获取验证码登录影响用户体验。
- 登录时增加一些干扰信息,比如增加图片验证码、拖动之类,防止机器暴力破解。
- 用户登录有指定时间内输错密码多次之后锁定该用户一天这类机制,也能预防攻击者通过枚举的方法攻击的目的。
- 记录用户登录真实IP,当IP变动时通知用户异地登录。
这里简单介绍下 PHP + Redis 登录密码输错的情况锁定的实现
<?php
$num = 5;//设置错误次数
//假设收到的用户名密码
$userName = 'test';
$pwd = 'Aa123456';
//实例化redis数据库
$redis= new \Redis();
$redis->connect('127.0.0.1', 6379);
//用天作为key,好判断一天之内该用户是否输入错误密码多次
$key = date('Ymd'). ':' . $userName;
//判断是否已经错误5次
if($redis->get($key) >= $num){
//当天错误5次,锁定该用户一天,或者可以锁定10分钟之类,这里锁定一天,并发短信通知用户是不是本人操作
echo '密码错误超过5次,账号已锁定,请明天再试';
exit();
}
//查询数据库,查询该用户不存在,提示:用户名和密码不匹配,不要提示用户不存在
if (!isset($userInfo) || empty($userInfo)){
echo '用户名和密码不匹配';
exit;
}
//模拟查询到用户在数据库的信息
$userInfo = array('userName'=>'test','password'=>'c76161d9e15f9983fe0d8ae18104ddbb82176f70','salt' => 'fe699dfb3b5f4e31502d9addf46d02ea');
//按注册的加密方法加密。比对密码是否正确
$password = sha1($pwd . $userInfo['salt']);
//判断密码是否正确
if($password != $userInfo['password']){
//将错误次数加 1
$redis->incr($key);
//设置一天有效期,一天之后自动注销该key
$redis->expire($key, 86400);
//当天已错误次数
$errorNum = $redis->get($key);
echo '密码不正确,当天已错误 '. $errorNum . ' 次,超过 '. $num . ' 次之后账号被锁定一天';
exit();
}else{
echo '成功登录';
}
密码加密基本上就上面这些,敏感信息的加密,要根据自己的实际业务选择加密方案。也是分为加密和加密之后解密不解密的问题,一些需要解密的,因为是站内加密解密,密码无需传输,可采用对称加密算法 AES,AES加密安全且高效。