就糟糕啦~
簡述
上次在 利用 Cookie 實作登入機制 中已經實作出一個基本的登入功能,但文末有提過這樣會有 cookie 被竄改的問題。這篇文章就來解釋一下是什麼問題?以及該如何解決?
假冒別人身分
既然 cookie 值可以任我改,那只要知道其他人的 username
是什麼就可以假冒身分了:
補充一下「新增留言」的程式邏輯,不然會看不太懂:
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
| <?php require_once('./conn.php'); require_once('./utils.php'); $username = $_COOKIE['username']; $user = getUser($username); $nickname = $user['nickname']; $content = $_POST['content'];
if (empty($content)) { header('Location: index.php?errorCode=1'); die(); }
$sql = sprintf( "INSERT INTO comments(`nickname`, `content`) VALUES ('%s', '%s')", $nickname, $content );
try { $result = $conn->query($sql); header('Location: index.php'); } catch (Exception $e) { echo '執行失敗:' . $e->getMessage() . '<br>'; echo '錯誤代碼:' . $conn->errno; } ?>
|
簡單來說,當 request 送出去時,handle_add_comments.php
會從 cookie 的內容找出對應的 user,取得使用者資訊後再把內容寫到資料庫裡。也就是說 cookie 寫誰就代表誰 的意思,因此最後留言的才會是無慘而不是煉獄。
解決辦法
前面這種「把資訊都存在瀏覽器的 Cookie」的做法其實叫做「Cookie-based session」,但現在要換個思維,我們可以改成「把資訊存在 Server 端」來處理,這種作法叫「Session Identifier」。
意思是說,要存在 cookie 的東西不是資訊本身,而是一個類似「通行證」的東西。就好像你去好市多購物要檢查會員卡一樣,「會員卡」就是要用來放在 cookie 裡面給 Server 識別的東西。
話說回來,剛剛說把要儲存的資訊放在 Server 端,但實際上是指哪裡?
當然就是 資料庫 囉!
我們可以新開一個叫做 tokens
的資料庫,專門放每個 token 對應到哪個 user,像這樣:
現在還沒存任何東西所以欄位都是空的。
總之呢,登入這一段要做的事情很簡單:
- 當登入成功後,產生一個 token
- 把 token 寫到 cookie,並且把 token 對應到的 user 寫到資料庫中
1 2 3 4 5 6 7 8 9 10 11
| <?php function generateToken() { $s = ''; for ($i=0; $i<15; $i++) { $s .= chr(rand(65,90)); } return $s; } >
|
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
| <?php require_once('./conn.php'); require_once('./utils.php'); $username = $_POST['username']; $password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username='$username' AND password='$password'"; $result = $conn->query($sql);
if ($result->num_rows > 0) { $token = generateToken(); $sql = "INSERT INTO tokens(`token`, username) VALUE('$token', '$username')"; $conn->query($sql); $expire = time() + 3600 * 24 * 30; setcookie('token', $token, $expire); header('Location: ./index.php'); } else { header('Location: ./login.php?errorCode=2'); } ?>
|
接下來的 index.php
的流程也差不多,只是稍微多幾個步驟:
- 根據 cookie 中的 token 去
tokens
(table) 找到對應的 username
- 拿 username 去
users
(table) 找到對應的 user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
<?php require_once("./conn.php"); require_once("./utils.php"); $username = Null; if (!empty($_COOKIE['token'])) { $token = $_COOKIE['token']; $user = getUserFromToken($token); $username = $user['username']; $nickname = $user['nickname']; } ?>
|
一樣附上 getUserFromToken($token)
的程式邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php require_once('./conn.php'); function getUserFromToken($token) { global $conn; $sql = "SELECT username FROM tokens WHERE token='$token'"; $result = $conn->query($sql); if ($result->num_rows === 0) { return 'Failed'; } $row = $result->fetch_assoc(); $username = $row['username']; $sql = "SELECT * FROM users WHERE username='$username'"; $result = $conn->query($sql); return $result->fetch_assoc(); } ?>
|
最後一樣就完成登入機制了,但跟剛剛不同的是現在沒辦法再竄改 cookie 來假冒別人身分:
一樣附上「新增留言」的程式邏輯:
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
| <?php require_once('./conn.php'); require_once('./utils.php');
$token = $_COOKIE['token']; $user = getUserFromToken($token); $nickname = $user['nickname']; $content = $_POST['content'];
if (empty($content)) { header('Location: index.php?errorCode=1'); die(); }
$sql = sprintf( "INSERT INTO comments(`nickname`, `content`) VALUES ('%s', '%s')", $nickname, $content );
try { $result = $conn->query($sql); header('Location: index.php'); } catch (Exception $e) { echo '執行失敗:' . $e->getMessage() . '<br>'; echo '錯誤代碼:' . $conn->errno; } ?>
|
以上就是解決 cookie 被竄改的應對方法,以及 Session Identifier 的概念,把資料存在 Server,Cookie 只存 Token(或叫 Session ID)。
最後補充一下,登出的時候不要忘了把資料庫中的 token 給清掉:
1 2 3 4 5 6 7 8 9 10 11
| <?php require_once('./conn.php'); $token = $_COOKIE['token']; $sql = "DELETE FROM tokens WHERE token='$token'"; $conn->query($sql); setcookie('token', '', time() - 3600); header('Location: ./index.php'); ?>
|
後記
其實 PHP 裡面就有內建的 session 機制,用法可以參考這篇:PHP 內建的 session 方法
雖然內建方法用起來簡單,而且也比較穩定,但自己手工實作一遍確實會對「Session Identifier」的概念有更深的理解,所以才會想寫篇文章來做紀錄。