《現代 PHP》學習筆記(十四):密碼

前言

本文為《現代 PHP》一書的學習筆記。

環境

  • Windows 10
  • Wnmp 3.1.0

密碼

密碼安全性非常重要,安全的管理、雜湊並儲存使用者密碼是開發者的職責之一。幸運的是,PHP 提供了內建的工具讓密碼安全性管理變得簡單許多。

絕對不要知道使用者密碼

開發者永遠不應該有辦法得知使用者的密碼,如果應用程式的資料庫被入侵,將帶來成堆的法律問題,名譽也將受損。

絕對不要限制使用者密碼

如果要求使用者地密碼符合特定格式,等同於提供一個好用的指南讓惡意駭客侵入應用程式。如果非得限制使用者的密碼,最好就是限制最短長度。

絕對不要以電子郵件寄送使用者密碼

如果藉由電子郵件寄送使用者的密碼,使用者將會知道他的密碼可能是不安全的,而且是可以被知道的。

相反,開發者應該使用電子郵件發送一個 URL,讓使用者在按下「忘記密碼」的連結後,被導向一個可以重新設定密碼的表單頁面。

使用 bcrypt 雜湊

確保使用者的密碼被雜湊而不是被加密。「加密」與「雜湊」並非同義字,前者是雙向的演算法,被加密的資料可以被解密;而後者是單向的,被雜湊的資料不能被倒回為原本的形式,而相同的資料永遠會產生出相同的雜湊碼。

當使用者密碼被雜湊後儲存於資料庫,入侵的駭客只會看到無意義的密碼雜湊碼,需要花費大量的時間和資源來破解。許多雜湊演算法都是開放使用的(例如 MD5SHA1bcryptscrypt),而速度與安全性是使用時需要考慮的要素。

現今被檢驗最安全的雜湊演算法是 bcrypt,它被設計成十分緩慢,但最終的雜湊碼非常的安全。雜湊疊代的次數稱為「工作係數」(work factor),工作係數和雜湊碼被破解的難度呈指數性上升幅度。

密碼雜湊 API

PHP 原生的密碼雜湊 API 提供了易用的函式,簡化了密碼雜湊的程序,預設是使用 bcrypt 雜湊演算法。

使用者註冊

假想一個應用程式接收到一個 HTTPPOST 請求,夾帶著有效的電子郵件以及至少八個字元的密碼,就允許建立一個使用者密碼。

範例 5-7:使用者註冊腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
try {
// 驗證電子郵件地址
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if (!$email) {
throw new Exception('Invalid email');
}

// 驗證密碼
$password = filter_input(INPUT_POST, 'password');
if (!$password || mb_strlen($password) < 8) {
throw new Exception('Password must contain 8+ characters');
}

// 建立密碼雜湊値
$passwordHash = password_hash(
$password,
PASSWORD_DEFAULT,
['cost' => 12]
);
if ($passwordHash === false) {
throw new Exception('Password hash failed');
}

// 建立使用者帳戶
// $user = new User();
// $user->email = $email;
// $user->password_hash = $passwordHash;
// $user->save();

// 重新導向至登入頁面
header('HTTP/1.1 302 Redirect');
header('Location: /login.php');
} catch (Exception $e) {
// 回報錯誤
header('HTTP/1.1 400 Bad request');
echo $e->getMessage();
}

使用者登入

假想一個應用程式接收到一個 HTTPPOST 請求,夾帶著電子郵件以及密碼,用來認證登入使用者。

範例 5-7:使用者登入腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
session_start();
try {
// 從請求中取得電子郵件地址
$email = filter_input(INPUT_POST, 'email');

// 從請求中取得密碼
$password = filter_input(INPUT_POST, 'password');

// 用電子郵件地址找到使用者
$user = User::findByEmail($email);

// 用密碼雜湊値驗證密碼
if (password_verify($password, $user->password_hash) === false) {
throw new Exception('Invalid password');
}

// 必要時重新雜湊密碼
$currentHashAlgorithm = PASSWORD_DEFAULT;
$currentHashOptions = array('cost' => 15);
$passwordNeedsRehash = password_needs_rehash(
$user->password_hash,
$currentHashAlgorithm,
$currentHashOptions
);
if ($passwordNeedsRehash === true) {
// 儲存新的密碼雜湊値
$user->password_hash = password_hash(
$password,
$currentHashAlgorithm,
$currentHashOptions
);
$user->save();
}

// 儲存登入狀態到 Session
$_SESSION['user_logged_in'] = 'yes';
$_SESSION['user_email'] = $email;

// 重新導向到個人資料頁面
header('HTTP/1.1 302 Redirect');
header('Location: /user-profile.php');
} catch (Exception $e) {
header('HTTP/1.1 401 Unauthorized');
echo $e->getMessage();
}
  • password_needs_rehash() 函式用來檢驗密碼雜湊値是否和指定的選項匹配。
  • password_verify() 函式用來驗證密碼是否和密碼雜湊値匹配。

重新雜湊密碼

為什麼應當建立一個新的密碼雜湊値?假設應用程式是兩年前建立的,當時使用的 bcrypt 工作係數是 10,而現在是使用 20,因此需要確保密碼雜湊値被更新。

參考資料

  • Josh Lockhart(2015)。現代 PHP。台北市:碁峯資訊。