# 北科 GDSC Web Backend 講義 2023/11/28 這次的進度:請先把[這個](https://mailntustedutw-my.sharepoint.com/:u:/g/personal/b11015020_mail_ntust_edu_tw/Ef89RyOczolIo1bpYtypfA8ByBNiDdALvnWqvHhcb_NKrw?e=EsdRlK)載下來然後丟到 htdocs https://mailntustedutw-my.sharepoint.com/:u:/g/personal/b11015020_mail_ntust_edu_tw/Ef89RyOczolIo1bpYtypfA8ByBNiDdALvnWqvHhcb_NKrw?e=EsdRlK :::danger ## 重要 如果你有任何一項沒有做到,都很有可能導致你無法參與課程的實作。在任何環境遇到問題,請馬上舉手問助教。 ::: ![image](https://hackmd.io/_uploads/Byyw4HQr6.png) ## 課前要求 ### 軟體安裝與檔案放置 - 上一次網頁課交的網頁,不管你怎麼做的,請到[這裡](https://mailntustedutw-my.sharepoint.com/:u:/g/personal/b11015020_mail_ntust_edu_tw/EaFPHF08_tBNkldHcoMsCwcBT1qvk_2VcEZUYf5gmI5oAA?e=25gebg)下載 - 安裝 phpstorm - 如果沒有授權,你可以使用30天試用 - 其他選項自行決定,理論上不會影響太多 - xampp 安裝好 - php 版本要是 8.0 以上的 - 不要改預設的安裝目錄(例如:Windows 用戶安裝到安裝在D:\xampp) - 將上次的網頁檔案放至下方對應作業系統的目錄下(記得解壓縮),整個資料夾丟進去 Windows: `C:\xampp\htdocs\` macOS: `Application/XAMPP/xamppfiles/htdocs/` Linux: `/opt/lampp/htdocs/` ### 在課堂開始之前,請嘗試著做這些事 #### 啟動 web server(apache), mysql server ![image](https://hackmd.io/_uploads/r1J58XzHa.png) Windows用戶啟動方式 1. 在工作列點一下搜尋按鈕, 輸入 xampp,選擇打開下圖相同的選項(選擇語言你就選英文吧) ![image](https://hackmd.io/_uploads/rJh37NzHp.png) 2. 點擊紅框中的開始按紐-Apache和MySQL ![截圖 2023-11-27 下午11.22.39](https://hackmd.io/_uploads/BybYEEzHT.png) #### 測試錯誤 1. 把上次的 index.html 檔案的副檔名改成 .php 2. 在 index.php 最上方的地方加上這些代碼 ```php= <?php echo +++; ?> ``` ## 今天要幹啥? ![image](https://hackmd.io/_uploads/HJr-MlXB6.png) 把這個東西做出來~~ 還有用 php~ ### PHP 簡介 PHP(PHP:Hypertext Preprocessor)是一種廣泛使用的開源伺服器端腳本語言,主要用在網頁開發。PHP 可以內嵌在 HTML 裡。將一個 HTML 檔案(如 `index.html`)的副檔名修改成 `.php` 就可以開始寫 PHP 了!以下是PHP語法的簡介: 1. **基本語法**:PHP 腳本以 `<?php` 開始,以 `?>` 結束。如果一個文件完全是 PHP 代碼,結束標籤可以省略。如果是要在 html 中輸出 php 中的變數,可以用 `<?= ?>` 來代替 3. **變數**:PHP 中的變數以 `$` 符號開始,後跟變數名。例如:`$name`。 4. **數據類型**:PHP 支持多種數據類型,包括整數(int)、浮點數(float)、字符串(string)、布林值(bool)、數組(array)等。 5. **條件語句**:PHP 使用 `if`、`else`、`elseif` 來執行條件判斷。 ```php if ($a > $b) { echo "a is greater than b"; } elseif ($a == $b) { echo "a is equal to b"; } else { echo "a is smaller than b"; } ``` 5. **注釋**:單行注釋使用 `//` 或 `#`,多行注釋使用 `/* ... */`。 6. **循環**:常見的循環語句包括 `for`、`foreach`、`while` 和 `do-while`。 ```php for ($i = 0; $i < 10; $i++) { echo $i; } ``` 7. **函數**:PHP 函數以 `function` 關鍵字定義。 ```php function sayHello() { echo "Hello, world!"; } ``` ## 資料庫概念與建立 CURD C: Create U: Update R: Read D: Delete ### 哪個部分 ![image](https://hackmd.io/_uploads/HkOIfxmHp.png) ### 架構圖 ![image](https://hackmd.io/_uploads/ByrtzxQBT.png) ### 實作 #### 建立新的資料庫、資料表並插入資料 - 資料庫 (Database) - ![image](https://hackmd.io/_uploads/SyeK7gQrT.png =200x) - ![image](https://hackmd.io/_uploads/B1VBNgmBa.png =400x) - 資料表 (Data table) - ![image](https://hackmd.io/_uploads/HyFuEe7ST.png =300x) - 設定資料結構(Table schema) - ![image](https://hackmd.io/_uploads/SyGp4emHT.png) - 列 (Columns) - 插入 ![image](https://hackmd.io/_uploads/rJ0xBxmBp.png) - account: admin password: 1234 permission: 1 - [關於 `utf8mb4` 和編碼的選擇](https://i30101000023b.nc.com.tw/modules/news/article.php?storyid=12) - `_ci` 的意思:不區分大小寫的意思 (e.g. admin == Admin) 所以如果帳戶裡面有這兩個使用者的話,查 admin, Admin 也有可能在搜尋結果中。 #### 連接資料庫 在專案資料夾中新建一個檔案 ![image](https://hackmd.io/_uploads/B1u5SeXST.png) 叫 `sql.php`,我打算在這邊寫資料庫連線的資訊 ![image](https://hackmd.io/_uploads/rkpy8g7rp.png) ![image](https://hackmd.io/_uploads/BkSMUg7Ba.png) ```php= <?php // sql.php $config = [ "host" => "127.0.0.1", "dbname" => "ntut-gdsc-20231128-pre", // database name "charset" => "utf8", "account" => "root", "password" => "" ]; ``` 這是一個 [PHP Array](https://www.php.net/manual/zh/language.types.array.php)。 複製、貼上,改一改。 ```php! <?php // sql.php $db = new PDO("mysql:host={$config['host']};dbname={$config['dbname']}", $config['account'], $config['password']); ``` #### 嘗試執行一個 command [說明書的範例](https://www.php.net/manual/zh/pdo.prepare.php) ![image](https://hackmd.io/_uploads/BkqvDemB6.png) --- 回到 PHPMyAdmin,點一下 `users` ![image](https://hackmd.io/_uploads/rJlpPeXBT.png) 會看到所有資料,上面那一條也會告訴你看到所有資料的query ![image](https://hackmd.io/_uploads/SkU6DlXST.png) 開寫! ```php! <?php // sql.php $query = "SELECT * FROM `users`"; $thread = $db->prepare($query); $thread->execute($param); var_dump($thread->fetchAll()); ``` 包起來 注意一下 php function 的語法。 ```php! <?php // sql.php // ... /** * @param string $query * @param array $param * @param bool $get_res * @return array|bool|PDOStatement */ function runCommand(string $query,array $param = [], bool $get_res = false): array|bool|PDOStatement { global $db; $thread = $db->prepare($query); $thread->execute($param); if ($get_res) { return $thread->fetchAll(); } return $thread; } ``` 再用看看 ```php! var_dump(runCommand("SELECT * FROM `users`", get_res: true)); ``` :::success #### 搞定 ![image](https://hackmd.io/_uploads/SyjftlQrT.png) ::: --- ## 登入區塊 ![image](https://hackmd.io/_uploads/HyNoKl7Sa.png =300x) ### 拿資料 表單的資料送去哪?(index.php) ![image](https://hackmd.io/_uploads/rJIX9eXHa.png) 前端表單的輸入匡,哪一個是哪一個呢?(index.php) ![image](https://hackmd.io/_uploads/SJgG9emHT.png) 後端怎麼拿? `$_GET['field-name']` 拿之前要確認有沒有存在 `isset(你要確認的東西)` ![image](https://hackmd.io/_uploads/BJ7jqxXBp.png) login.php ```php! <?php // isset -> 檢查要存取的東西是否存在 if (!isset($_GET['account']) || !isset($_GET['password'])) { die("no param"); // 直接強迫終止,後面的code都不會執行,括號內放輸出的字串。 } ``` ok,參數都有了,接下來是去資料庫查資料 使用資料庫要用到 `sql.php` 中的內容 所以: `require_once "sql.php";` 再加上前面寫的 runCommand ```php! <?php // login.php // ... require_once "sql.php"; $res = runCommand("SELECT * FROM `users` WHERE `account` = :account", [ "account" => $_GET['account'] ], true); ``` 查到的資料會裝在 res 裡面 但你總得先 var_dump 看一下吧 ![image](https://hackmd.io/_uploads/SJcdoxmB6.png) 正確的長怎麼樣? 錯誤的長怎麼樣? ok 知道怎麼拿資料,也知道拿到的資料長啥樣了 ### 判斷密碼是不是正確的 ```php= if ($this_user["password"] == $_GET["password"]) { ``` >Passwords should be verified using the password_verify function, which uses constant time and is timing attack safe. ok, so... ![image](https://hackmd.io/_uploads/r1umnl7Ba.png) ![image](https://hackmd.io/_uploads/BJzNnlXH6.png) ### 登入成功後,轉跳至對應的畫面 ![image](https://hackmd.io/_uploads/SyJGyB7rp.png =300x) 利用 `header("Location: ...")` ```php! <?php // login.php // ... switch ($this_user["permission"]) { case 1: // admin header("Location: admin.php"); break; case 2: // teacher header("Location: teacher.php"); break; case 3: header("Location: student.php"); break; } ``` :::danger **(我們在這)** ::: 登入失敗勒? 先放一個 flag (旗標) ```php! <?php // ... // login.php $login_success = false; // < this line if (count($res) > 0) { // user exists if (password_verify($_GET['password'], $res_user["password"])) { $login_success = true; // < this line // ... } } ``` 轉跳到對應畫面: header("Location: ...") ```php! <?php // login.php // ... if (! $login_success) { // 前面加驚嘆號是顛倒是非的意思 header("Location: login_error.html"); } ``` :::success login.php 目前進度 ```php= <?php // isset -> 檢查要存取的東西是否存在 if (!isset($_GET['account']) || !isset($_GET['password'])) { die("no param"); // 直接強迫終止,後面的code都不會執行,括號內放輸出的字串。 } require_once "sql.php"; $res = runCommand("SELECT * FROM `users` WHERE `account` = :account", [ "account" => $_GET['account'] ], true); var_dump($res); $login_success = false; // < this line if (count($res) > 0) { $this_user = $res[0]; if ($this_user["password"] == $_GET["password"]) { $login_success = true; switch ($this_user["permission"]) { case 1: // admin header("Location: admin.php"); break; case 2: // teacher header("Location: teacher.php"); break; case 3: header("Location: student.php"); break; } } } if (! $login_success) { // 前面加驚嘆號是顛倒是非的意思 header("Location: login_error.html"); } ``` ::: ### 防止猴子 ![image](https://hackmd.io/_uploads/HkgiJ-7ST.png =200x) #### set 一個 cookie 吧 login.php, 在登入成功的時候 ```php! <?php // ... setcookie("user_id", $res_user["id"]); ``` 怎麼看我設定的cookie? ![image](https://hackmd.io/_uploads/Sy44yZXSa.png) 怎麼刪掉? ```php! <?php // ... setcookie("user_id", null); ``` #### 做一個海關 :::spoiler 如果你不知道那個 runCommand 裡面的指令是什麼鬼 ![image](https://hackmd.io/_uploads/rkeYlW7Sp.png) ![image](https://hackmd.io/_uploads/BkdFgbmBa.png) ::: ```php! <?php // sql.php // ... function requireLogin(): void { if (!isset($_COOKIE["user_id"])) { die("no login!"); } } function requirePermission(int $level): void { requireLogin(); $user_id = $_COOKIE['user_id']; $this_user = runCommand("SELECT * FROM `users` WHERE `id` = :id", [ "id" => $user_id ], get_res: true); // check exists if (count($this_user) == 0) { die("not exists"); } // check right level if ($this_user[0]["permission"] != $level) { die("permission deny"); } } ``` 怎麼用?在你需要海關的地方(ex: admin.php) 加上這些 ```php! <?php require_once "sql.php"; requirePermission(1); ?> ``` ## 新增使用者 邏輯: 1. 接收($_GET)帳號、密碼、權限 2. 在資料庫查看(SELECT * FROM `users` WHERE...)有沒有重複的帳戶名稱 3. 寫進資料庫 (INSERT INTO) 4. 返回(header("Locatoin: ...")) admin.php ```htmlembedded <form action="user_add.php" method="post"> <h1>Add User</h1> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi deleniti eaque explicabo fugiat harum ipsa itaque iusto, laboriosam laudantium libero necessitatibus odit, optio porro quaerat quam repellat, vero voluptatum. Nostrum.</p> <label> 帳號 <input type="text" name="user"> </label> <label> 密碼 <input type="password" name="passwd"> </label> <label> 權限 <!-- 選單 --> <select name="permission" id=""> <option value="1">1 -> admin</option> <option value="2">2 -> teacher</option> <option value="3" selected>3 -> student</option> <!-- selected: 預選取的選項 --> </select> </label> <br><!-- 換行 --> <a href="admin.php">back</a> <br><!-- 換行 --> <div class="submit-container"> <input type="submit"> </div> </form> ``` ```php= <?php require "sql.php"; $sqlstr="Insert INTO data (account, passwd, per) VALUES ('".$_POST["user"]."','".$_POST["passwd"]."','".$_POST["permission"]."');"; $res = runCommand($sqlstr,[], true); header("Location: admin.php"); ``` ## 更改 1. (update_user.php)接收待更改的ID 2. (update_user.php)把原先使用者的資料放到畫面上 3. (update_user.php)接收($_GET)帳號、密碼、權限、用戶ID 4. (update_user_write.php)在資料庫查看(SELECT * FROM `users` WHERE...)有沒有重複的帳戶名稱 5. (update_user_write.php)寫進資料庫 (INSERT INTO) 6. 返回(header("Locatoin: ...")) admin.php ## 刪除 1. (del_user.php)接收($_GET)待更改的ID 2. 送刪除指令到SQL 3. 返回(header("Locatoin: ...")) admin.php