Learning Objectives
- 說明表單送出時,瀏覽器把資料放在網址(GET)或請求本體(POST)送到伺服器
- 用 $_GET、$_POST 讀取資料,用 $_SERVER['REQUEST_METHOD'] 判斷是不是送出表單
- 驗證使用者輸入:必填、長度、格式(filter_var),把錯誤訊息顯示給使用者並保留已填的內容
- 說明為什麼輸出使用者輸入時一定要用 htmlspecialchars 跳脫(XSS)
- 用 PRG(Post/Redirect/Get)模式避免重新整理時重複送出
表單:讓使用者把資料送到伺服器
登入、留言、報名、搜尋,都是表單(form):使用者在網頁上填資料,按下按鈕,瀏覽器把資料送到伺服器, 伺服器上的 PHP 讀取資料、處理,再產生一個新的網頁回傳。
<form method="post" action="register.php">
<input name="name"> <!-- name 屬性就是送到伺服器的欄位名稱 -->
<input name="email">
<button>送出</button>
</form>
action:資料送到哪個網址;省略時送回目前這一頁method:送的方式,get或post(省略時是 get)- 每個欄位的
name屬性,就是 PHP 讀取時用的鍵;沒有name的欄位不會被送出
PHP 收到請求時,已經把資料整理成超全域變數(superglobals):在程式任何地方、包括函式裡,都可以直接使用。
| 變數 | 內容 |
|---|---|
$_GET |
網址查詢字串(? 後面)的資料 |
$_POST |
用 POST 送出的表單資料 |
$_SERVER |
這次請求的資訊:REQUEST_METHOD、REQUEST_URI、HTTP_HOST… |
$_COOKIE、$_SESSION |
第 16 課介紹 |
本課的範例模擬一次 HTTP 請求執行 PHP;逐步執行動畫的全域框架會顯示這次請求帶來的 $_GET、$_POST。
GET 與 POST
| GET | POST | |
|---|---|---|
| 資料放在哪裡 | 網址:/search.php?q=php&page=2 |
請求的本體(body),網址看不到 |
| PHP 讀取 | $_GET['q'] |
$_POST['q'] |
| 可以加入書籤、分享連結 | 可以 | 不行 |
| 重新整理 | 直接再送一次 | 瀏覽器會問要不要「重新傳送表單」 |
| 適合 | 搜尋、篩選、分頁:只讀取資料 | 登入、報名、留言:會修改伺服器上的資料 |
密碼不要用 GET 送:網址會留在瀏覽紀錄、伺服器紀錄檔和書籤裡。不過 POST 也只是「網址看不到」,資料一樣是明文傳送, 保護傳輸內容要靠 HTTPS。
讀取網址的查詢字串
這個範例模擬瀏覽器請求 /index.php?name=Amy&lang=php。全域框架從第一步就有 $_GET,右邊可以看到它的內容。網址裡沒有的欄位(page)用 ?? 給預設值,避免「Undefined array key」警告。
讀取與驗證 POST 資料
永遠不要相信使用者送來的資料。欄位可能是空的、格式不對、長度超過,甚至根本沒有送(有人直接用程式發送請求)。 處理表單的固定步驟:
- 用
$_SERVER['REQUEST_METHOD'] === 'POST'判斷是不是送出表單(第一次打開頁面是 GET,不用檢查) - 用
?? ''讀取欄位,欄位不存在也不會出現警告;用trim()去掉前後空白 - 逐一檢查,把錯誤訊息放進一個陣列
- 沒有錯誤才真正處理(存進資料庫、寄信…);有錯誤就重新顯示表單和錯誤訊息
常用的檢查:
| 檢查 | 寫法 |
|---|---|
| 必填 | $name === '' |
| 長度(中文) | mb_strlen($name) > 20 |
| Email 格式 | filter_var($email, FILTER_VALIDATE_EMAIL),不合格回傳 false |
| 整數與範圍 | filter_var($age, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]) |
| 只能是某幾個值 | in_array($color, ['red', 'green'], true) |
filter_var 驗證成功時回傳轉換後的值(例如整數 20),失敗回傳 false,所以判斷時要用 === false:
年齡填 0 的時候,if (!$age) 會把合法的 0 也當成失敗。
驗證表單欄位
模擬的 POST 資料是:姓名 " 小明 "(前後有空白)、Email ming@、年齡 "20"。注意 $_POST 裡的值都是字串;filter_var 驗證整數成功後,$age 變成整數 20。
想一想:GET、POST 與驗證
使用者在搜尋框輸入「資料庫」,表單是 <form method="get" action="search.php">、欄位 name="kw"。PHP 要怎麼讀到這個值?
模擬 POST 送出 qty=0,下面的程式會印出什麼?
<?php
$qty = filter_var($_POST['qty'], FILTER_VALIDATE_INT);
if (!$qty) {
echo '數量不合法';
} else {
echo "數量 $qty";
}
輸出時一定要跳脫:XSS
PHP 產生的是 HTML。如果把使用者的輸入原封不動放進 HTML,輸入裡的 <script> 就會被瀏覽器當成程式執行,
這種攻擊叫 XSS(Cross-Site Scripting,跨網站指令碼):攻擊者在留言板留下一段程式,每個看留言的人都會執行它,
可能被偷走登入狀態、被導到釣魚網站。
解法是輸出前用 htmlspecialchars() 把 HTML 的特殊字元換成實體(entity),瀏覽器就只會把它們當成文字顯示:
| 字元 | 變成 |
|---|---|
& |
& |
<、> |
<、> |
" |
" |
' |
' |
原則:輸入時驗證、輸出時跳脫。資料存進資料庫時保留原樣,輸出到 HTML 的那一刻才跳脫。 第 18 課會再整理其他常見的攻擊與防護。
有跳脫與沒有跳脫
模擬有人在留言欄位送出 <script>alert("hi")</script>。看最後的輸出:第一行把它原樣放進 HTML,瀏覽器會執行這段程式;第二行跳脫之後只是一段文字。
多選欄位:名稱加上 []
勾選框(checkbox)可以勾好幾個。欄位名稱寫成 hobby[],PHP 會把勾選的值收集成一個陣列:
<input type="checkbox" name="hobby[]" value="閱讀"> 閱讀
<input type="checkbox" name="hobby[]" value="運動"> 運動
<input type="checkbox" name="hobby[]" value="程式"> 程式
沒有勾選的勾選框不會被送出:一個都沒勾時,$_POST 裡根本沒有 hobby,所以要寫 $_POST['hobby'] ?? []。
送來的值也可能被竄改,用 array_intersect 只留下允許的選項。
勾選框送出的陣列
模擬勾選了「閱讀」和「程式」。右邊可以看到 $_POST['hobby'] 本身就是一個陣列。
PRG 模式:處理完就轉址
POST 處理成功後,如果直接在同一頁顯示「報名成功」,使用者按下重新整理,瀏覽器會再送一次同樣的 POST, 報名就重複了。常見的做法是 PRG(Post/Redirect/Get):
- 瀏覽器 POST 表單
- PHP 處理成功,回應
header('Location: thanks.php');並exit;,狀態碼是 302(轉址) - 瀏覽器自動用 GET 請求 thanks.php
使用者最後看到的是一個 GET 請求的頁面,重新整理也只會再 GET 一次。
header() 必須在任何輸出之前呼叫(包括 <?php 前面的空白與 HTML),否則會出現
「Cannot modify header information - headers already sent」警告,第 16 課會再說明。
header() 之後一定要 exit;,否則程式會繼續執行後面的內容。
想一想:跳脫與轉址
模擬 GET 請求 ?name=<b>Amy</b>,下面的程式會輸出什麼 HTML?
<?php
echo 'Hi ' . htmlspecialchars($_GET['name']);
下面的程式處理 POST 之後轉址,但少寫了 exit;。模擬 POST 送出時,回應的內容會是什麼?
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: done.php');
}
echo '這裡是表單頁';
動手做:驗證密碼設定表單
模擬使用者用 POST 送出 user=amy、pw=abc123、pw2=abc124。請完成驗證:帳號 3 到 12 個字元、密碼至少 8 個字元、兩次輸入的密碼要相同。依序檢查,每個錯誤印一行;全部通過才印「設定完成:帳號」。
重點回顧
- 表單欄位的
name就是 PHP 讀取的鍵;GET 的資料在網址、用$_GET讀,POST 的資料在請求本體、用$_POST讀 - 只讀取資料(搜尋、分頁)用 GET;會修改資料(登入、報名)用 POST
- 用
$_SERVER['REQUEST_METHOD'] === 'POST'判斷是不是送出表單;讀欄位寫$_POST['x'] ?? '',再trim() - 永遠驗證使用者輸入:必填、長度用
mb_strlen、格式用filter_var(判斷失敗要用=== false) - 輸出使用者輸入到 HTML 之前一定要
htmlspecialchars(),避免 XSS:輸入時驗證、輸出時跳脫 - 多選欄位名稱寫
hobby[],PHP 收到陣列;沒有勾選時欄位不存在 - POST 處理成功後用
header('Location: …'); exit;轉址(PRG),避免重新整理時重複送出