2026 暑訓 / COURSE 02.07
Day 5 - CMS 教學 3
Authentication 讓系統知道你是誰;Authorization 則決定你能做什麼。
完成 Articles、Users 與 Tags 的基本功能後,接下來要讓 CMS 真正具備「使用者登入」與「權限控制」。
這一天會分成兩個核心主題:
- Authentication(認證):確認「你是誰」
- Authorization(授權):確認「你可以做什麼」
最後,我們會讓使用者只能編輯或刪除自己建立的文章,並將相同的授權概念延伸到 Users 與 Tags。
1. Authentication 與 Authorization 的差別
這兩個概念常常會被混在一起。
可以先這樣理解:
Authentication
→ 你是誰?
→ Login / Logout
Authorization
→ 你可以做什麼?
→ Add / Edit / Delete / OwnershipAuthentication 負責確認使用者身分。
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
使用哪種 IdentifierAuthenticationMiddleware
在 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 Redirectlogin()
建立:
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.phpPolicy 負責定義:
Identity
+
Resource
+
Action
↓
Allow / DenycanAdd()
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作業目標
請完成:
- 為
User與Tag建立對應 Policy。 - 在
UsersController與TagsController中加入授權檢查。 需要 Authorization 的 Action 使用:
$this->Authorization->authorize(...);公開 Action 使用:
$this->Authorization->skipAuthorization();- 使用者只能 Edit / Delete 自己的 User。
- Tags 可以公開瀏覽,但修改操作需要適當授權。
- 權限判斷必須放在 Backend,而不是只靠隱藏按鈕或 Form。
- 測試:
- 未登入
- 已登入但無權限
- 已登入且有權限
Policy 思考方式
可以參考:
ArticlePolicy
├── canAdd()
├── canEdit()
├── canDelete()
└── _isAuthor()接著自行思考:
UserPolicy
→ 誰可以新增?
→ 誰可以查看?
→ 誰可以編輯?
→ 誰可以刪除?
TagPolicy
→ 誰可以新增?
→ 誰可以編輯?
→ 誰可以刪除?
→ 哪些頁面可以公開?不要把權限寫死在 Controller
例如避免大量:
if ($user->id === ...)散落在 Controller。
比較好的做法是把 Rule 集中在 src/Policy/。
例如:
ArticlePolicy
UserPolicy
TagPolicyController 只負責:
Request
Application Flow
Resource Loading
authorize()而 Policy 專門負責:
Authorization Rules10. 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