在數據庫中保存明文密碼是非常不明智的選擇,其危害不言而喻。
這里就不討論明文密碼的缺點了,只談談如何安全的保存密碼。
基本的安全措施如下:
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
密碼加密
<?php //$hash就是密碼加密后的字符串 $options = [ 'cost' => 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"]; }
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。