十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
在数据库中保存明文密码是非常不明智的选择,其危害不言而喻。
公司主营业务:成都网站建设、做网站、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。成都创新互联是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。成都创新互联推出平乐免费做网站回馈大家。
这里就不讨论明文密码的缺点了,只谈谈如何安全的保存密码。
基本的安全措施如下:
1.设置密码最小位数
2.将用户的密码加密保存
3.通过重置密码的一次性链接修改密码
4.同一IP或mac地址一天内只能获取3次重置邮件
5.用户修改密码时需输入原密码
6.用户信息被修改后发送短信/邮件提醒
当然还可以采取更安全的措施:
7.不常用设备登陆需手机短信验证(需要短信平台)
8.设置安全问答信息
9.记录错误登陆请求信息,多次错误后拒绝登陆尝试
--------------------------------------------------------------------------
第一项:密码的加密保存
通常情况下md5是最常用也是最简单的,但也是破解方法最多的一种方法。
php 5.5及以上版本中提供了Password Hashing API, 非常方便的解决了密码加密问题
http://tw2.php.net/manual/zh/ref.password.php
密码加密
10 //'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM), 不推荐手动设置盐值 ]; $hash = password_hash("123456", PASSWORD_BCRYPT, $options); ?>
注意手动设置盐值在这里不被推荐,在php7里已经废掉这个选项。
password_hash中的第二个参数是对算法的设置,有两个选项:
默认为PASSWORD_DEFAULT,现在的算法为bcrypt, 但这个算法会随着php版本的更新而更新更强的算法,建议数据库字段设置为char(255)。
PASSWORD_BCRYPT,算法也是bcrypt,(手册上写的CRYPT_BLOWFISH,其实就是crypt()使用CRYPT_BLOWFISH算法),结果永远是60个字符串,字段设置为char(60)即可。
简单的说password_hash就是把bcrypt封装了起来,而且会随着以后版本的更新而改进提升所使用的算法,就现阶段来讲,bcrypt已经足够安全了。
-------------------
顺道解释一下什么是 cost(消耗) 和 salt(盐值)
cost:消耗--是用来对付暴力破解的,随着计算机速度的不断提升,我们可以让一台计算机几十年不关机来破解一个密码,所以我们人为的加上一个消耗值,使计算机的算法变慢一点,当然变慢的这一点对单次运算影响不大,但暴力破解时间就要延长到几千上万年了。
salt:盐值--用于对付彩虹表(不知道自行百度一下),盐值作为一个干扰项,使每次hash产生的密文均不相同,抵御彩虹表破解。
-------------------
密码验证
//$hash,从数据库里读取的加密字符串 if (password_verify('password', $hash)) { //验证通过 } else { //验证错误 }
检查加密措施是否需要升级
//检查hash是否由bcrypt加密,如果不是则需要升级,返回true if (password_needs_rehash ($current_hash, PASSWORD_BCRYPT)) { $new_hash = password_hash($password, PASSWORD_BCRYPT) }
获取加密信息
password_get_info只能用于password_hash生成的hashing
-----------------------------------------------
如果你使用的是php5.5以下版本,可用以下方法(也是我现在使用的方法,原理上是一样的)替代:
class Password { private static $algo = '$2a', $cost = '$10'; public static function unique_salt() { return substr(sha1(mt_rand()),0,22); } public static function hash($password) { return crypt($password, self::$algo . self::$cost . '$' . self::unique_salt()); } public static function check_password($hash, $password) { $full_salt = substr($hash, 0, 29); $new_hash = crypt($password, $full_salt); return ($hash === $new_hash); } }
-------------------------
第二项:密码的一次性的重置链接
一次性链接有这么两特点:
在链接生成的一定时间内(比如24小时)点击有效
一旦密码被重置,链接立即失效
既然这样,就需要记录链接的是否过期,有以下几种思路:
数据库中保存链接生成时间和链接是否已被使用(考虑用一个字段记录信息)。
利用opcode缓存,需要安装xcache或其他类似工具
我现在用的便是xcahce,主要考虑到保存到库中会增加开销。
public function generate_link($username,$hash){ if (function_exists('xcache_isset')) { //将username加密作为我们的unique_id $unique_id = md5($username); //将username保存到名为unique_id的缓存中,设置缓存24小时候过期 xcache_set($unique_id, $username, 24*60*60); //将username和hash加密作为验证信息(不放心的话可以在加一个公匙在里面) $validate = md5($username.$hash); //拼接成字符串 $string = $unique_id.$validate; //生成重置密码的链接 $link = $_SERVER['SERVER_NAME']."/reset-password?p=".$string; return $link; } } //检查链接是否合法 public function check_link($p){ if (function_exists('xcache_isset')) { //获取链接中的unique_id $unique_id = substr($p, 0, 32); if(xcache_isset($unique_id)){ //通过unique_id读取username $username = xcache_get($unique_id); //通过username读取hash $hash = findHashByUsername($username); //获取链接中的验证信息 $link_md5 = substr($p,32); if($link_md5 === md5($username.$hash)){ //链接验证成功 }else{ //链接验证失败 redirect(); } }else{ redirect(); } } }
记住密码重置后要立即清除$unique_id的缓存
if(updateLoginPassword($username,$password)){ xcache_unset($unique_id); }
第三项:设置同一IP一天内的重置密码次数限制
和一次性链接很像,也有两种思路:
将ip存到数据库
将ip信息通过xcache保存
我就只给大家提供一个获取ip的函数了,其他的大家自己补充吧
public static function validip($ip) { if (!empty($ip) && ip2long($ip)!=-1) { $reserved_ips = array ( array('0.0.0.0','2.255.255.255'), array('10.0.0.0','10.255.255.255'), array('127.0.0.0','127.255.255.255'), array('169.254.0.0','169.254.255.255'), array('172.16.0.0','172.31.255.255'), array('192.0.2.0','192.0.2.255'), array('192.168.0.0','192.168.255.255'), array('255.255.255.0','255.255.255.255') ); foreach ($reserved_ips as $r) { $min = ip2long($r[0]); $max = ip2long($r[1]); if ((ip2long($ip) >= $min) && (ip2long($ip) <= $max)) return false; } return true; } else { return false; } } public static function getip() { if (self::validip($_SERVER["HTTP_CLIENT_IP"])) { return $_SERVER["HTTP_CLIENT_IP"]; } if(isset($_SERVER["HTTP_X_FORWARDED_FOR"])){ foreach (explode(",",$_SERVER["HTTP_X_FORWARDED_FOR"]) as $ip) { if (self::validip(trim($ip))) { return $ip; } } } $keys = array("HTTP_X_FORWARDED","HTTP_FORWARDED_FOR","HTTP_FORWARDED"); foreach ($keys as $key){ if (self::validip($_SERVER[$key])) { return $_SERVER[$key]; } } return $_SERVER["REMOTE_ADDR"]; }