標籤:
本節將使用PHP和Redis實現使用者註冊登入功能,下面分模組來介紹具體實現方法。
1.註冊
需求描述:使用者註冊時需要提交郵箱、登入密碼和暱稱。其中郵箱是使用者的唯一標識,每個使用者的郵箱不能重複,但允許使用者修改自己的郵箱。
我們使用散列類型來儲存使用者的資料,鍵名為user:使用者ID。其中使用者ID是一個自增的數字,之所以使用 ID 而不是郵箱作為使用者的標識是因為考慮到在其他鍵中可能會通過使用者的標識與使用者物件相關聯,如果使用郵箱作為使用者的標識的話在使用者修改郵箱時就不得不同時需要修改大量的鍵名或索引值。為了儘可能地減少要修改的地方,我們只把郵箱作為該散列鍵的一個欄位。為此還需要使用一個散列類型的鍵email.to.id來記錄郵箱和使用者ID間的對應關係以便在登入時能夠通過郵箱獲得使用者的ID。
使用者填寫並提交註冊表單後首先需要驗證使用者輸入,我們在項目目錄中建立一個register.php檔案來實現使用者註冊的邏輯。驗證部分的代碼如下:
// 設定Content-type以使瀏覽器可以使用正確的編碼顯示提示資訊,
// 具體的編碼需要根據檔案實際編碼選擇,此處是utf-8。
header("Content-type: text/html; charset=utf-8");
if(!isset($_POST[‘email‘]) ||
!isset($_POST[‘password‘]) ||
!isset($_POST[‘nickname‘])) {
echo ‘請填寫完整的資訊。‘;
exit;
}
$email = $_POST[‘email‘];
// 驗證使用者提交的郵箱是否正確
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo ‘郵箱格式不正確,請重新檢查‘;
exit;
}
$rawPassword = $_POST[‘password‘];
// 驗證使用者提交的密碼是否安全
if(strlen($rawPassword) < 6) {
echo ‘為了保證安全,密碼長度至少為6。‘;
exit;
}
$nickname = $_POST[‘nickname‘];
//不同的網站對使用者暱稱有不同的要求,這裡不再做檢查,即使是空也可以。
// 而後我們需要判斷使用者提交的郵箱是否被註冊了:
$redis = new Predis\Client();
if($redis->hexists(‘email.to.id‘, $email)) {
echo ‘該郵箱已經被註冊過了。‘;
exit;
}
驗證通過後接下來就需要將使用者資料存入Redis中。在儲存的時候要記住使用散列函數處理使用者提交的密碼,避免在資料庫中儲存純文字密碼。原因是如果資料庫中資料泄露(外部原因或內部原因都有可能),攻擊者也無法獲得使用者的真實密碼,也便無法正常地登入進系統。更重要的是考慮到使用者很可能在其他網站中也使用了同樣的密碼,所以純文字密碼泄露還會給使用者造成額外的損失。
除此之外,還要避免使用速度較快的散列函數處理密碼以防止攻擊者使用窮舉法破解密碼,並且需要為每個使用者產生一個隨機的“鹽”(salt)以避免攻擊者使用彩虹表破解。這裡作為樣本,我們使用Bcrypt演算法來對密碼進行散列。PHP 5.3中提供的crypt函數支援Bcrypt演算法,我們可以實現一個函數來隨機產生鹽並調用crypt函數獲得散列後的密碼:
function bcryptHash($rawPassword, $round = 8)
{
if ($round < 4 || $round > 31) $round = 8;
$salt = ‘$2a$‘ . str_pad($round, 2, ‘0‘, STR_PAD_LEFT) . ‘$‘;
$randomValue = openssl_random_pseudo_bytes(16);
$salt .= substr(strtr(base64_encode($randomValue), ‘+‘, ‘.‘), 0, 22);
return crypt($rawPassword, $salt);
}
提示 openssl_random_pseudo_bytes函數需要安裝OpenSSL擴充。
之後使用如下代碼獲得散列後的密碼:
$hashedPassword = bcryptHash($rawPassword);
儲存使用者資料就很簡單了,所有命令都在第3章介紹過了。代碼如下:
require ‘./predis/autoload.php‘;
$redis = new Predis\Client();
// 首先擷取一個自增的使用者ID
$userID = $redis->incr(‘users:count‘);
// 儲存使用者資訊
$redis->hmset("user:{$userID}", array(
‘email‘ => $email,
‘password‘ => $hashedPassword,
‘nickname‘ => $nickname
));
// 記得記錄下郵箱和使用者ID的對應關係
$redis->hset(‘email.to.id‘, $email, $userID);
// 提示使用者註冊成功
echo ‘註冊成功!‘;
大部分情況下在註冊時我們需要驗證使用者的郵箱,不過這部分的邏輯與忘記密碼部分相似,所以在這裡不做更多的介紹。
2.登入
需求描述:使用者登入時需要提交郵箱和登入密碼,如果正確則輸出“登入成功”,否則輸出“使用者名稱或密碼錯誤”。
當使用者提交郵箱和登入密碼後首先通過email.to.id鍵獲得使用者ID,然後將使用者提交的登入密碼使用同樣的鹽進行散列並與資料庫儲存的密碼比對,如果一樣則表示登入成功。我們建立一個login.php檔案來處理使用者的登入,處理該邏輯的部分代碼如下:
header("Content-type: text/html; charset=utf-8");
if(!isset($_POST[‘email‘]) ||
!isset($_POST[‘password‘])) {
echo ‘請填寫完整的資訊。‘;
exit;
}
$email = $_POST[‘email‘];
$rawPassword = $_POST[‘password‘];
require ‘./predis/autoload.php‘;
$redis = new Predis\Client();
// 獲得使用者的ID
$userID = $redis->hget(‘email.to.id‘, $email);
if(!$userID) {
echo ‘使用者名稱或密碼錯誤。‘;
exit;
}
$hashedPassword = $redis->hget("user:{$userID}", ‘password‘);
現在我們得到了之前儲存過的經過散列後的密碼,接著定義一個函數來對使用者提交的密碼進行散列處理。bcryptHash函數中返回的密碼中已經包含了鹽,所以只需要直接將散列後的密碼作為crypt函數的第二個參數,crypt函數會自動地提取出密碼中的鹽:
function bcryptVerify($rawPassword, $storedHash)
{
return crypt($rawPassword, $storedHash) == $storedHash;
}
之後就可以使用此函數進行比對了:
if(!bcryptVerify($rawPassword, $hashedPassword)) {
echo ‘使用者名稱或密碼錯誤。‘;
exit;
}
echo ‘登入成功!‘;
3.忘記密碼
需求描述:當使用者忘記密碼時可以輸入自己的郵箱,系統會發送一封包含更改密碼的連結的郵件,使用者單擊該連結後會進入密碼修改頁面。該模組的訪問頻率限制為1分鐘10次以防止惡意使用者通過此模組向某個郵箱地址大量發送垃圾郵件。
當使用者在忘記密碼的頁面輸入郵箱後,我們的程式需要做兩件事。
(1)進行訪問頻率限制。這裡使用4.2.3節介紹的方法以郵箱為標示符對發送修改密碼郵件的過程進行訪問頻率限制。當使用者提交了郵箱地址後首先驗證郵箱地址是否正確,如果正確則檢查訪問頻率是否超限:
$keyName = "rate.limiting:{$email}";
$now = time();
if($redis->llen($keyName) < 10) {
$redis->lpush($keyName, $now);
} else {
$time = $redis->lindex($keyName, -1);
if($now - $time < 60) {
echo ‘訪問頻率超過了限制,請稍後再試。‘;
exit;
} else {
$redis->lpush($keyName, $now);
$redis->ltrim($keyName, 0, 9);
}
}
一般在全站中還會有針對IP地址的訪問頻率限制,原理與此類似。
(2)發送修改密碼郵件。使用者通過訪問頻率限制後我們會為其產生一個隨機的驗證碼,並將驗證碼通過郵件發送給使用者。同時在程式中要把使用者的郵箱地址存入名為retrieve.password.code:散列後的驗證碼的字串類型鍵中,然後使用EXPIRE命令為其設定一個存留時間(如1個小時)以提供安全性並且保證及時釋放儲存空間。由於忘記密碼需要的安全等級與使用者註冊登入相同,所以我們依然使用Bcrypt演算法來對驗證碼進行散列,具體的演算法同上這裡不再詳述。
redis實踐:使用者註冊登入功能