[ 紀錄 ] 實戰練習 - 留言版 (以 php 實作前端 + 後端)


Posted by krebikshaw on 2020-08-18

確認需求

  1. 會員註冊、登入、登出功能
  2. 會員個人資料修改功能
  3. 留言新增、編輯、刪除功能
  4. 留言顯示有分頁功能
  5. 管理員權限功能,以及只有管理員進得去的後台管理頁面,可以看到所有使用者,而管理員可以調整使用者的身份,也可以編輯各項身份的權限(註1)
  6. 資訊安全防護功能 (註2)

註1:

  • 彈性的後台權限設計,可以任意組合「新增文章」、「刪除自己的文章」、「刪除任意文章」、「編輯自己的文章」、「編輯任意文章」這些權限(舉例來說,管理員要可以更改遭停權使用者這個身份的權限,可以變成只能編輯文章不能刪除等等。)

註2:

  • 解決明碼問題、SQL Injection 攻擊防護以及 XSS 攻擊防護

資料庫規劃

紀錄所需要的 table 及內容

user :

  • id :會員 id
  • nickname : 會員暱稱
  • username : 會員帳號 (獨一)
  • password : 會員密碼
  • role : 會員權限 (關聯 roles.id )
  • created_at : 註冊時間
  • updated_at : 最後更新時間

comments :

  • id : 留言 id
  • user_id : (關聯 user.id)
  • content : 留言內容
  • is_deleted : 是否為刪除狀態
  • created_at : 新增時間
  • updated_at : 最後更新時間
  • deleted_at : 刪除時間

roles :

  • id : 權限 id
  • name : (獨一) 權限名稱
  • read : 讀取文章
  • create : 新增文章
  • update_oneself : 編輯自己的文章
  • update_all : 編輯全部的文章
  • delete_oneself : 刪除自己的文章
  • delete_all : 刪除全部的文章
  • isAdmin:是否可進入後台

檔案路由規劃

index.php (首頁)

  • 註冊按鈕 -> 導到 register.php (註冊頁面)
  • 登入按鈕 -> 導到 login.php (登入頁面)
  • 登出按鈕 -> 執行 handle_logout.php 登出程式 -> 導回 index.php
  • 編輯會員資料按鈕 -> 導到 update_user.php (編輯會員資料頁面)
  • 送出留言按鈕 -> 執行 handle_add_comment.php 新增留言程式 -> 導回 index.php
  • 編輯留言按鈕 -> 導到 update_comment.php (編輯留言頁面)
  • 刪除留言按鈕 -> 執行 handle_delete_comment.php 刪除留言程式 -> 導回 index.php
  • 後台管理按鈕 -> 導到 backstage.php (後台管理頁面)

register.php (註冊頁面)

  • 送出按鈕 -> 執行 handle_register.php 註冊程式 -> 導回 index.php

login.php (登入頁面)

  • 送出按鈕 -> 執行 handle_login.php 登入程式 -> 導回 index.php

update_user.php (編輯會員資料頁面)

  • 送出按鈕 -> 執行 handle_update_user.php 更改會員資料程式 -> 導回 index.php

update_comment.php (編輯留言頁面)

  • 送出按鈕 -> 執行 handle_update_comment.php 編輯留言程式 -> 導回 index.php

backstage.php (後台管理頁面)

  • 權限設定按鈕 -> 導到 role_manage.php (權限設定頁面)
  • 送出按鈕 -> 執行 handle_user_role.php 權限變更程式 -> 導回 index.php

role_manage.php (權限設定頁面)

  • 送出按鈕 -> 執行 handle_role_manage.php 權限設定程式 -> 導回 backstage.php

頁面細節規劃

把 index.php 頁面中需處理資料的部分獨立成各別檔案做處理

navbar.php (功能列)

  • 判斷登入狀態
  • 判斷登入者權限

form.php (表單列)

  • 判斷登入狀態
  • 判斷登入者權限

comment.php (留言列)

  • 判斷登入狀態
  • 判斷登入者權限

page.php (分頁列)

  • 處理分頁資料

功能細部規劃

把相關功能分類並區分成不同的檔案

DB_conn.php

  • 資料庫連線功能
  • 發送 SQL 指令功能
  • response 除錯功能

utils_general.php

  • 取得 user 資料功能
  • 權限驗證功能

utils_post.php

  • 留言新增功能
  • 留言編輯功能
  • 留言刪除功能

utils_user.php

  • 會員註冊功能
  • 會員登入功能
  • 會員登出功能
  • 會員編輯資料功能

utils_roles.php

  • 新增權限功能
  • 編輯權限功能
  • 設定使用者權限功能

開發流程規劃

  1. index.php 切版
    • 「建立」顯示留言功能
    • 「建立」新增留言功能
  2. login.php 切版
    • 「建立」會員登入功能
    • 「建立」會員登出功能
  3. register.php 註冊頁面切版
    • 「建立」帳號註冊功能
  4. 設定 SESSION 判斷登入狀態
    • 「修改」新增留言功能 - 非登入狀態不可使用
  5. update_user.php 切版
    • 「建立」編輯會員資料功能
  6. update_comment.php 切版
    • 「建立」編輯留言功能
    • 「建立」刪除留言功能
    • 「建立」留言顯示分頁功能
  7. backstage.php 切版
    • 「建立」取得所有使用者資料功能
    • 「建立」設定使用者權限功能
  8. role_manage.php 切版
    • 「建立」新增權限功能
    • 「建立」編輯權限功能
    • 「修改」新增留言功能 - 加入權限驗證
    • 「修改」編輯留言功能 - 加入權限驗證
    • 「修改」刪除留言功能 - 加入權限驗證

權限驗證 - 規劃

驗證流程:

  1. 先確認使用者是否有進入此頁面的權限,沒有就導回
  2. 確認使用者是否有操作此行為的權限,沒有就導回
  3. 確認此篇留言是否屬於此使用者(若目前使用者有權限管理所有留言,則不檢驗此項目)
  4. 三項驗證皆符合,方可執行後續程式

執行方法:

  • 所需獲取的資料:
    • 目前的使用者帳號
    • 目前頁面所要操作的行為
    • 目前的文章編號
  • 資料處理流程:
    1. 利用帳號,取得使用者的權限編號
    2. 利用此權限編號,取得所有行為的權限
    3. 利用文章編號,獲取文章的作者的 user_id
  • 開始進行驗證:
    1. 若此頁為後台頁面,先判斷是否為管理員身份,不是就導回 (若此頁面非後台頁面,則跳過此步驟)
    2. 判斷是否具備此行為之權限
    3. 判斷此篇留言是否屬於此使用者(若目前使用者有權限管理所有留言,則不檢驗此項目)

權限驗證 - 使用方式:

  • 先 new 一個物件準備做權限驗證
    • $cp = new CheckPermission($username, 'create', '9');
  • 若是後台頁面要確認使用者是否為管理員,可以用 isAdmin() 驗證
    • if (!$cp->isAdmin()) { exit; }
  • 若是跟文章相關相關,則直接用 hasPermission() 驗證
    • if (!$cp->hasPermission) { exit; }

實際程式碼 - 權限驗證

<?php
  class CheckPermission {
    public function __construct($username, $action, $comment_id) {
        $this->username = $username;
        $this->action = $action;
        $this->comment_id = $comment_id;
        $this->init();
    }

    private function init() {
        // 先把需要的資料都先準備好
        $this->userData = getUser($this->username);
        $this->roleData = getRole($this->userData['role']);
        $this->commentData = getComment($this->comment_id);
    }

    public function isAdmin() {
        // 驗證是否有權限進入後台頁面,若非管理員身份,回傳 false
        return !empty($this->roleData['is_admin']);
    }

    public function hasPermission() {
        // 按照順序做權限驗證,全數通過才會回傳 true
        if ( !$this->checkAction()) return false ;
        if ( !$this->checkBelonging()) return false ;
        return true;
    }

    private function checkAction() {
        // 判斷使用者是否具備此行為的權限
        if ($this->action === 'create') {
            return !empty($this->roleData['create_comment']);
        }
            if ($this->action === 'update') {
                return !empty($this->roleData['update_oneself']) || !empty($this->roleData['update_all']);
        }
        if ($this->action === 'delete') {
            return !empty($this->roleData['delete_oneself']) || !empty($this->roleData['delete_all']);
        }       
    }

    private function checkBelonging() {
        // 若是 comment_id 沒有值或者 action 為新增文章,則跳過此步驟
        // 判斷使用者是否有權限管理別人的文章,有權限則回傳 true
        // 判斷此使用者是否為文章所有人
        if (empty($this->comment_id) || $this->action === 'create') return true ;
        if ($this->action === 'update' && !empty($this->roleData['update_all'])) return true ;
        if ($this->action === 'delete' && !empty($this->roleData['delete_all'])) return true ;
        return ($this->userData['id'] === $this->commentData['user_id']);
    }
  }

$cp = new CheckPermission($username, 'create', '9') ;
?>

實際程式碼 - 資料庫連線 DB_conn.php

<?php
  require_once('DB_config.php');

  class Db {
    public function __construct($servername, $username, $password, $db_name) {
      $this->server = $servername;
      $this->user = $username;
      $this->pass = $password;
      $this->db = $db_name;
      $this->init();
    }

    private function init() {
      $this->conn = mysqli_connect($this->server, $this->user, $this->pass, $this->db);
      if ($this->conn->connect_error) {
        die('資料庫連線錯誤:' . $this->conn->connect_error);
      }
      mysqli_query($this->conn, "SET NAMES utf8");
      mysqli_query($this->conn, 'SET time_zone = "+8:00"');
    }

    public function sqlQuery($str, $type = 's', ...$List) {
      $this->stmt = mysqli_prepare($this->conn, $str);
      mysqli_stmt_bind_param($this->stmt, $type, ...$List);
      $this->stmt_result = mysqli_stmt_execute($this->stmt);
      $this->checkResult();
      $this->result = $this->stmt->get_result();
    }

    private function checkResult() {
      if (!$this->stmt_result) {
        echo 'Failed' . $this->conn->error;
        exit;
      }
    }

    public function getResult() {
      return $this->result ? $this->result->fetch_assoc() : ''; 
    }
  }

  $db = new Db($servername, $username, $password, $db_name);
?>

參考資料

PHP中require和include路徑問題詳解


#comments #PHP







Related Posts

JavaScript ES6

JavaScript ES6

NLP 簡介

NLP 簡介

API 留言板練習-Part1 : 打造後端 API

API 留言板練習-Part1 : 打造後端 API


Comments