2026 暑訓 / COURSE 02.07

Day 5 - CMS 教學 3

Authentication 讓系統知道你是誰;Authorization 則決定你能做什麼。

INSTRUCTOR王朝威UPDATED2026.08.23

完成 Articles、Users 與 Tags 的基本功能後,接下來要讓 CMS 真正具備「使用者登入」與「權限控制」。

這一天會分成兩個核心主題:

  • Authentication(認證):確認「你是誰」
  • Authorization(授權):確認「你可以做什麼」

最後,我們會讓使用者只能編輯或刪除自己建立的文章,並將相同的授權概念延伸到 Users 與 Tags。


1. Authentication 與 Authorization 的差別

這兩個概念常常會被混在一起。

可以先這樣理解:

Authentication
→ 你是誰?
→ Login / Logout

Authorization
→ 你可以做什麼?
→ Add / Edit / Delete / Ownership

Authentication 負責確認使用者身分。

Authorization 則是在已經知道「你是誰」之後,再判斷你是否有權限執行某個操作。


2. 安裝 Authentication Plugin

CakePHP 的登入功能可以透過官方 Authentication Plugin 完成。

使用 Composer 安裝:

composer require "cakephp/authentication:^4.0"

接下來會完成:

  • 密碼雜湊
  • Login
  • Logout
  • Session Authentication
  • Form Authentication
  • Registration

3. 為密碼加入 Hash

如果直接使用 Bake 建立 Users CRUD:

bin/cake bake all users

一開始可能會發現密碼直接以明文儲存。

這在實際系統中是不可接受的,因此必須先進行 Password Hashing。

在 CakePHP 中:

Table
→ 負責操作一組資料

Entity
→ 代表單筆資料

密碼屬於單一 User 的資料,因此 Hash Logic 適合放在:

src/Model/Entity/User.php

建立 Password Setter

在 src/Model/Entity/User.php 中加入:

use Authentication\PasswordHasher\DefaultPasswordHasher;

接著建立:

protected function _setPassword(string $password): ?string
{
    if (mb_strlen($password) > 0) {
        return (new DefaultPasswordHasher())
            ->hash($password);
    }

    return null;
}

CakePHP 會依照 Convention,在設定:

$user->password

時自動呼叫:

_setPassword()

因此不論新增或修改 User,只要設定 Password,就會自動進行雜湊。


bcrypt

Authentication Plugin 預設使用 bcrypt 處理 Password Hash。

因此同樣的密碼:

123456

每次 Hash 後得到的結果都可能不同。

這是正常現象。

Password Hash 並不是加密後再解密,而是用來進行密碼比對。

也就是:

User Input Password
↓
Hash / Verify
↓
Database Hash

系統不需要知道使用者原本的明文密碼。


4. Authentication

Authentication Plugin 主要由幾個部分組成。

Application

負責提供:

AuthenticationService

以及將 Authentication Middleware 加入 Application。

AuthenticationService

負責定義:

登入位置
使用哪些欄位登入
使用哪些 Authenticator
使用哪種 Identifier

AuthenticationMiddleware

在 Request 進入 Controller 前執行 Authentication。

流程大致是:

Browser
↓
Request
↓
AuthenticationMiddleware
↓
AuthenticationService
↓
Controller

所以 Authentication 發生在 Controller 之前。


修改 Application

在 src/Application.php 中加入:

use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Identifier\PasswordIdentifier;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;

接著讓 Application 實作 AuthenticationServiceProviderInterface:

class Application extends BaseApplication
    implements AuthenticationServiceProviderInterface
{

    // 其他程式碼...
}

加入 AuthenticationMiddleware

在 middleware() 中加入:

$middlewareQueue
    ->add(new RoutingMiddleware($this))
    ->add(new BodyParserMiddleware())
    ->add(new AuthenticationMiddleware($this));

順序很重要。

AuthenticationMiddleware 必須在 Routing 等必要 Middleware 之後加入,才能取得正確的 Request Context。


建立 AuthenticationService

接著在 src/Application.php 加入:

public function getAuthenticationService(
ServerRequestInterface $request
)
    : AuthenticationServiceInterface 
{
    $service = new AuthenticationService();

    $service->setConfig([
        'unauthenticatedRedirect' => [
            'prefix' => false,
            'plugin' => null,
            'controller' => 'Users',
            'action' => 'login',
        ],
        'queryParam' => 'redirect',
    ]);

    $fields = [
        PasswordIdentifier::CREDENTIAL_USERNAME => 'email',
        PasswordIdentifier::CREDENTIAL_PASSWORD => 'password',
    ];
    
    $service->loadAuthenticator(
'Authentication.Session'
);
    $service->loadAuthenticator(
'Authentication.Form',
 [
        'fields' => $fields,
        'loginUrl' => [
            'prefix' => false,
            'plugin' => null,
            'controller' => 'Users',
            'action' => 'login',
        ],
        'identifier' => [
            'className' => 'Authentication.Password',
            'fields' => $fields,
        ],
    ]
);
    
    return $service;
}

這裡定義:

Username Field
→ email

Password Field
→ password

也就是使用 Email + Password 登入。


Session 與 Form Authenticator

接著載入兩個 Authenticator:

$service->loadAuthenticator(
'Authentication.Session'
);

以及:

$service->loadAuthenticator(
'Authentication.Form',
 [
    'fields' => $fields,
    'loginUrl' => [
        'prefix' => false,
        'plugin' => null,
        'controller' => 'Users',
        'action' => 'login',
    ],
    'identifier' => [
        'className' => 'Authentication.Password',
        'fields' => $fields,
    ],
]
);

Session 必須優先載入。

因為如果使用者已經登入,系統應優先透過 Session 確認身分,而不是每次都重新讀取 Login Form。

可以理解成:

Request
↓
Session Authentication
↓
如果沒有登入
↓
Form Authentication

載入 AuthenticationComponent

修改 src/Controller/AppController.php 加入:

public function initialize(): void
{
    parent::initialize();

    $this->loadComponent('Flash');

    $this->loadComponent(
'Authentication.Authentication'
);
}

之後所有 Controller 都可以透過:

$this->Authentication

操作 Authentication。


Authentication Result

Authentication 完成後,結果會放入 Request。

Controller 中可以透過:

$this->request
    ->getAttribute('authentication');

取得 Authentication Result。

例如可以判斷:

是否登入成功
是否登入失敗
目前是否有已驗證使用者

如果使用者尚未登入,又存取需要 Authentication 的頁面,就會被導向:

/users/login

建立 Login Action

在 src/Controller/UsersController.php 加入:

public function beforeFilter(
\Cake\Event\EventInterface $event
): void 
{
    parent::beforeFilter($event);

    $this->Authentication
->allowUnauthenticated(['login']);
}

這非常重要。

因為 Login Page 本身不能要求使用者先登入。

否則會形成:

未登入
↓
導向 /users/login
↓
/users/login 又要求登入
↓
再次導向 /users/login
↓
Infinite Redirect

login()

建立:

public function login()
{
    $result = $this->Authentication
->getResult();

    if ($result && $result->isValid()) {

        $target =
 $this->Authentication
->getLoginRedirect()
 ?? [
                'controller' => 'Articles',
                'action' => 'index',
        ];

        return $this->redirect($target);
    }

    if ($this->request->is('post')) {
        $this->Flash->error(
__('無效的帳號或密碼。')
);
    }
}

流程可以整理成:

Login Form
↓
POST email + password
↓
AuthenticationMiddleware
↓
AuthenticationService
↓
Password Identifier
↓
成功
→ Redirect

失敗
→ Flash Error

建立 Login Template

建立 templates/Users/login.php:

<div class="users form">
    <?= $this->Flash->render() ?>
    <h3>登入</h3>
    <?= $this->Form->create() ?>

    <fieldset>
        <legend>
<?= __('請輸入您的帳號與密碼') ?>
</legend>

        <?= $this->Form->control(
'email',
 ['required' => true]
) ?>
        <?= $this->Form->control(
'password',
 ['required' => true]
) ?>
    </fieldset>

    <?= $this->Form->submit(
__('登入')
) ?>
    <?= $this->Form->end() ?>
    <?= $this->Html->link(
'新增使用者',
['action' => 'add']
) ?>
</div>

Form 使用:

email
password

必須與 AuthenticationService 中的 Field Mapping 一致。


開放公開頁面

我們不希望所有頁面都要求登入。

例如:

index
view

通常可以讓未登入使用者瀏覽。

因此在 src/AppController.php 加入:

public function beforeFilter(
\Cake\Event\EventInterface $event
): void 
{
    parent::beforeFilter($event);

    $this->Authentication
->allowUnauthenticated([
        'index',
        'view'
    ]);
}

代表所有 Controller 中名稱為:

index
view

的 Action 都不需要登入。


Logout

在 src/Controller/UsersController.php 中加入:

public function logout()
{
    $this->Authentication->logout();

    return $this->redirect([
        'controller' => 'Users',
        'action' => 'login'
    ]);
}

Logout 主要就是清除 Authentication Session。


開放註冊

如果 /users/add 也要求登入,就沒辦法讓新使用者註冊。

因此改成:

$this->Authentication
->allowUnauthenticated([
'login',
 'add'
]);

這表示:

login
add

不需要通過 Authentication。


Authentication 完成

到這裡,我們已經完成:

Password Hash
Login
Logout
Session
Registration
Protected Actions
Public Actions

但目前還有一個問題。

只要是已登入使用者,就可能編輯其他人的 Article。

所以接下來要進入:

Authorization。


5. 安裝 Authorization Plugin

使用 Composer:

composer require "cakephp/authorization:^3.0"

接著在 src/Application.php 的 bootstrap() 中加入:

$this->addPlugin('Authorization');

6. Authorization

在 src/Application.php 加入:

use Authorization\AuthorizationService;
use Authorization\AuthorizationServiceInterface;
use Authorization\AuthorizationServiceProviderInterface;
use Authorization\Middleware\AuthorizationMiddleware;
use Authorization\Policy\OrmResolver;

接著讓 Application 實作:

AuthorizationServiceProviderInterface

因此最後會呈現:

class Application extends BaseApplication
    implements
        AuthenticationServiceProviderInterface,
        AuthorizationServiceProviderInterface
{

    // 其他程式碼...
}

AuthorizationMiddleware

在 Middleware Queue 中:

$middlewareQueue->add(
    new AuthorizationMiddleware($this)
);

Authorization 應該放在 Authentication 後面。

因為系統必須先知道「你是脽」才能判斷「你可以做什麼」。

因此概念順序是:

AuthenticationMiddleware
↓
AuthorizationMiddleware
↓
Controller

建立 AuthorizationService

在 src/Application.php 加入:

public function getAuthorizationService(
ServerRequestInterface $request
)
    : AuthorizationServiceInterface 
{
    $resolver = new OrmResolver();
    return new AuthorizationService(
$resolver
);
}

OrmResolver 負責根據 ORM Resource 找到對應的 Policy。

例如:

Article Entity
↓
ArticlePolicy

載入 AuthorizationComponent

在 src/Controller/AppController.php 加入:

$this->loadComponent(
'Authorization.Authorization'
);

接下來每個 Controller Action 都需要明確選擇 authorize() 或 skipAuthorization()。


skipAuthorization()

例如:

login
logout
add user

這些 Action 不需要 Authorization Check。

因此可以在 Action 中加入:

$this->Authorization
->skipAuthorization();

這表示:

這個 Action 是刻意略過 Authorization,而不是忘記檢查。


建立 ArticlePolicy

使用 Bake:

bin/cake bake policy --type entity Article

會建立:

src/Policy/ArticlePolicy.php

Policy 負責定義:

Identity
+
Resource
+
Action
↓
Allow / Deny

canAdd()

public function canAdd(
IdentityInterface $user,
 Article $article
): bool 
{
    return true;
}

所有已登入使用者都可以新增 Article。

canEdit()

public function canEdit(
IdentityInterface $user,
 Article $article
): bool 
{
    return $this->_isAuthor(
$user, 
$article
);
}

使用者只有在自己是 Author 時,才能 Edit。

canDelete()

public function canDelete(
IdentityInterface $user,
 Article $article
): bool 
{
    return $this->_isAuthor(
$user,
 $article
);
}

使用者只有在自己是 Author 時,才能 Delete。

判斷 Article Owner

protected function _isAuthor(
IdentityInterface $user,
 Article $article
): bool 
{
    return $article->user_id
 === $user->getIdentifier();
}

概念就是:

Article.user_id
 ===
 Current User ID

成立:Allow

不成立:Deny


在 Controller 執行 authorize()

Policy 只是定義 Rule。

Controller 還必須真正呼叫:

$this->Authorization
->authorize($article);

add()

public function add()
{
    $article =
 $this->Articles
->newEmptyEntity();
    $this->Authorization
->authorize($article);

    // ...
}

AuthorizationComponent 會自動找 canAdd()。

edit():

$article = $this->Articles
    ->findBySlug($slug)
    ->contain('Tags')
    ->firstOrFail();

$this->Authorization
->authorize($article);

AuthorizationComponent 會自動找 canEdit()。

delete():

$article = $this->Articles
    ->findBySlug($slug)
    ->firstOrFail();

$this->Authorization
->authorize($article);

AuthorizationComponent 會自動找 candelete()。


authorize() 與 Policy Method

可以整理成:

add()
→ canAdd()

edit()
→ canEdit()

delete()
→ canDelete()

也可以手動指定:

$this->Authorization
->authorize(
$article,
 'update'
);

此時會改用指定的 Authorization Action。


公開 Article Action

像:

index
view
tags

是公開功能。

所以加入:

$this->Authorization
->skipAuthorization();

這表示這些 Action 不需要 Authorization Check。


7. 修正 Article Owner

雖然 Policy 已經限制只有 Owner 能 Edit,但還有另一個安全問題。

如果 user_id 允許 Mass Assignment,使用者可能修改 Request:

user_id = 其他使用者

進而變更 Article Owner。

因此還要修正 Add 與 Edit。


Add 時設定目前使用者

新增 Article 時,不再從 Form 接收 user_id,而是由 Server 直接設定:

// 修改 src/Controller/ArticlesController.php
$article->user_id =
$this->request
->getAttribute('identity')
->getIdentifier();

也就是:

Logged-in User
↓
Identity
↓
Identifier
↓
Article.user_id

這樣 Article Owner 就由後端決定。


Edit 時禁止修改 user_id

Edit 時使用:

$this->Articles->patchEntity(
    $article,
    $this->request->getData(),
 [
        'accessibleFields' => [
'user_id' => false
]
    ]
);

這表示:

在這次 patchEntity() 中,不允許 user_id 被批次賦值。

並且應該從 templates/Articles/edit.php 刪除 user_id Form Control。


8. Authorization 與 Mass Assignment 是兩層防護

這裡非常重要。

Policy 解決:

你有沒有資格進入 Edit / Delete?

Mass Assignment Restriction 解決:

進入 Edit 之後,你能不能偷偷修改敏感欄位?

兩者不是同一件事。

可以理解成:

Authorization
→ 控制「能不能做」

Accessible Fields
→ 控制「哪些資料可以被修改」

9. Exercise:完善 Users 與 Tags 的授權控制

目前我們只替 Articles 完成完整的 Authorization。

接下來請把相同概念延伸到:

Users
Tags

作業目標

請完成:

  1. 為 User 與 Tag 建立對應 Policy。
  2. 在 UsersController 與 TagsController 中加入授權檢查。
  3. 需要 Authorization 的 Action 使用:

    $this->Authorization->authorize(...);
  4. 公開 Action 使用:

    $this->Authorization->skipAuthorization();
  5. 使用者只能 Edit / Delete 自己的 User。
  6. Tags 可以公開瀏覽,但修改操作需要適當授權。
  7. 權限判斷必須放在 Backend,而不是只靠隱藏按鈕或 Form。
  8. 測試:
    • 未登入
    • 已登入但無權限
    • 已登入且有權限

Policy 思考方式

可以參考:

ArticlePolicy
├── canAdd()
├── canEdit()
├── canDelete()
└── _isAuthor()

接著自行思考:

UserPolicy
→ 誰可以新增?
→ 誰可以查看?
→ 誰可以編輯?
→ 誰可以刪除?

TagPolicy
→ 誰可以新增?
→ 誰可以編輯?
→ 誰可以刪除?
→ 哪些頁面可以公開?

不要把權限寫死在 Controller

例如避免大量:

if ($user->id === ...)

散落在 Controller。

比較好的做法是把 Rule 集中在 src/Policy/。

例如:

ArticlePolicy
UserPolicy
TagPolicy

Controller 只負責:

Request
Application Flow
Resource Loading
authorize()

而 Policy 專門負責:

Authorization Rules

10. Day 5 重點整理

這一天完成了兩個非常重要的 Web Application Security Concept。

Authentication

Password Hash
Session
Login
Logout
Registration
AuthenticationMiddleware
AuthenticationService

負責:

Who are you?

Authorization

AuthorizationMiddleware
AuthorizationService
Policy
authorize()
skipAuthorization()
Ownership

負責:

What are you allowed to do?

整個流程可以整理成:

Browser
↓
Request
↓
AuthenticationMiddleware
↓
確認 Identity
↓
AuthorizationMiddleware
↓
Controller
↓
authorize(Resource)
↓
Policy
↓
Allow / Deny
↓
Model / View
↓
Response

結語

到這裡,我們已經建立了一套具備基本內容管理與使用者權限控制的 CakePHP CMS。

系統目前可以:

使用者註冊
登入 / 登出
建立文章
編輯自己的文章
刪除自己的文章
建立 Tags
依 Tags 瀏覽文章
管理 Article Ownership

同時也實際使用了:

ORM
FormHelper
Authentication
Authorization
Middleware
Policy
Mass Assignment

這些概念不只適用於 CMS,也是日後開發會員系統、後台管理系統與其他 CakePHP Application 時非常重要的基礎。

COURSE FILES

附件下載

FILE2026 CakePHP-Day5 CakePHP CMS 教學3.pdf下載 ↓
NEXT COURSE / 03VTK&PCL →