[安全設計] 003 認證與授權架構設計實戰指南:從密碼到 Zero Trust|身份驗證、RBAC、ABAC 與 Node.js 程式碼範例
「門鎖決定你能不能進門;門禁卡的權限,決定你進得了哪幾個房間。
認證是驗身分,授權是給權限——搞混了,就會把不該進的人放進去。」
— SSDLC by 飛飛
一、認證與授權是什麼?為什麼你不能只靠一組帳密打天下?
在 SSDLC 的「蓋房子」旅程中,我們已經走過了安全需求定義(確認房子要防什麼)、威脅建模(找出小偷從哪進來)、安全設計原則(建築師的防禦哲學)。現在,我們要進入安全設計階段最實戰的主題——認證(Authentication)與授權(Authorization)。
先用一個生活場景來分清楚這兩件事。
想像你走進一棟辦公大樓。大廳的保全要你刷員工證——這是認證,確認「你是誰」。你刷了卡,保全確認你是合法員工,讓你進入大樓。但這不代表你可以走進每間辦公室。你能進自己部門的樓層,卻不能刷進財務部的金庫——這是授權,決定「你能做什麼」。
在軟體系統中也一樣:
| 概念 | 英文 | 回答的問題 | 生活比喻 |
|---|---|---|---|
| 認證 | Authentication(AuthN) | 你是誰? | 刷員工證進大樓 |
| 授權 | Authorization(AuthZ) | 你能做什麼? | 只能進自己部門的樓層 |
這兩件事看似簡單,卻是 OWASP Top 10 中排名前三的重災區。A01:2021 — Broken Access Control(權限控制失效) 連續多年高居榜首,而 A07:2021 — Identification and Authentication Failures(認證失效) 也名列前茅。換句話說,全世界最常見的資安漏洞,就是「搞不清楚誰是誰」和「管不住誰能做什麼」。
飛飛觀點:
很多開發者花了大量時間防 SQL Injection 和 XSS,卻忘了最基本的事:你的系統真的確認了「這個請求是誰發的」嗎?他真的有權限做這件事嗎?把認證和授權做對,比擋住任何單一攻擊都重要。
二、認證機制選擇:從「你知道什麼」到「你是什麼」
認證的本質是回答一個問題:「你怎麼證明你是你?」
根據認證因子(Authentication Factor)的不同,可以分成三大類:
| 認證因子 | 英文 | 說明 | 範例 |
|---|---|---|---|
| 你知道的東西 | Something you know | 只有你知道的秘密 | 密碼、PIN 碼、安全問題 |
| 你擁有的東西 | Something you have | 你持有的實體或數位物品 | 手機 OTP、硬體金鑰、智慧卡 |
| 你本身的特徵 | Something you are | 你的生物特徵 | 指紋、臉部辨識、虹膜掃描 |
單獨使用任何一種因子都不夠安全。密碼會被猜到、手機會被偷、指紋甚至可以被複製。所以現代系統的最佳實踐是多因素認證(MFA, Multi-Factor Authentication)——至少使用兩種不同類別的因子。
2.1 密碼:最古老也最脆弱的認證方式
密碼是最普遍的認證方式,也是最容易出問題的。以下是開發者在處理密碼時最常犯的錯誤:
| 常見錯誤 | 為什麼危險 | 正確做法 |
|---|---|---|
| 密碼明碼存資料庫 | 資料庫被入侵 = 所有密碼外洩 | 使用 Argon2id 或 bcrypt 雜湊 |
| 限制密碼長度上限為 8 碼 | 8 碼密碼可在數小時內被暴力破解 | 最低 12 碼,上限至少 64 碼 |
| 要求定期換密碼(每 90 天) | 使用者只會在舊密碼後加 1、2、3 | NIST 建議不再強制定期更換(SP 800-63B) |
| 限制不能用特殊符號 | 縮小密碼空間,降低破解難度 | 允許所有 Unicode 字元 |
| 自己寫密碼雜湊邏輯 | 幾乎肯定有漏洞 | 使用成熟的函式庫 |
Node.js 密碼雜湊範例:
const argon2 = require('argon2');
// ✅ 註冊時:雜湊密碼
async function hashPassword(plainPassword) {
return await argon2.hash(plainPassword, {
type: argon2.argon2id, // 使用 Argon2id 變體(抵抗 GPU 和 side-channel 攻擊)
memoryCost: 65536, // 64 MB 記憶體
timeCost: 3, // 迭代次數
parallelism: 4 // 平行度
});
}
// ✅ 登入時:驗證密碼
async function verifyPassword(plainPassword, hashedPassword) {
return await argon2.verify(hashedPassword, plainPassword);
}
// ✅ 完整的登入流程
async function login(email, password) {
const user = await db.query(
'SELECT id, password_hash, login_attempts, locked_until FROM users WHERE email = $1',
[email]
);
// 不管帳號存不存在,都要做雜湊比對(防止 Timing Attack)
if (!user) {
await argon2.hash('dummy-password'); // 花費相同時間
return { success: false, message: '帳號或密碼錯誤' };
}
// 檢查帳號是否被鎖定
if (user.locked_until && user.locked_until > new Date()) {
return { success: false, message: '帳號已鎖定,請稍後再試' };
}
const isValid = await verifyPassword(password, user.password_hash);
if (!isValid) {
// 累加失敗次數,5 次後鎖定 15 分鐘
await incrementLoginAttempts(user.id);
await logSecurityEvent('LOGIN_FAILED', { userId: user.id, ip: req.ip });
return { success: false, message: '帳號或密碼錯誤' }; // 不透露是帳號錯還是密碼錯
}
// 登入成功,重置失敗次數
await resetLoginAttempts(user.id);
await logSecurityEvent('LOGIN_SUCCESS', { userId: user.id, ip: req.ip });
return { success: true, token: generateJWT(user) };
}
2.2 多因素認證(MFA):不要把所有雞蛋放在密碼這個籃子裡
密碼再強也可能被釣魚、被社交工程、被資料庫外洩。MFA 的價值在於:即使密碼被盜,攻擊者還需要突破另一道關卡。
常見的 MFA 方式比較:
| MFA 方式 | 安全性 | 便利性 | 成本 | 適合場景 |
|---|---|---|---|---|
| 簡訊 OTP | ⭐⭐ | ⭐⭐⭐⭐ | 低 | 一般會員系統(但注意 SIM Swap 攻擊) |
| TOTP(如 Google Authenticator) | ⭐⭐⭐ | ⭐⭐⭐ | 免費 | 內部系統、開發者工具 |
| 推播通知(如 Duo、Line Notify) | ⭐⭐⭐ | ⭐⭐⭐⭐ | 中 | 企業內部系統 |
| FIDO2/WebAuthn(硬體金鑰、Passkey) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 高 | 高安全需求(金融、管理員帳號) |
| Email OTP | ⭐⭐ | ⭐⭐ | 免費 | 低風險操作的二次確認 |
飛飛觀點:
簡訊 OTP 很常見,但它並不像你以為的那麼安全。SIM Swap 攻擊(打電話給電信商假裝是你,把你的門號轉到攻擊者的 SIM 卡)在台灣已經有實際案例。如果你的系統處理金流或敏感資料,建議至少用 TOTP,最好支援 FIDO2/Passkey。
2.3 Passkey 與 FIDO2:密碼的終結者
Passkey 是 FIDO Alliance 推動的下一代認證標準,目標是徹底取代密碼。它的原理是:
- 使用者的裝置(手機、電腦)生成一對公私鑰
- 公鑰存在伺服器,私鑰留在裝置上
- 認證時,伺服器發出挑戰(Challenge),裝置用私鑰簽名回應
- 伺服器用公鑰驗證簽名
這意味著:伺服器上完全沒有密碼或密碼雜湊,就算資料庫被入侵,攻擊者也拿不到任何可以用來登入的東西。
// Passkey 註冊流程(使用 @simplewebauthn/server)
const { generateRegistrationOptions, verifyRegistrationResponse }
= require('@simplewebauthn/server');
// 步驟一:產生註冊選項
app.post('/api/auth/passkey/register-options', async (req, res) => {
const user = req.user;
const options = await generateRegistrationOptions({
rpName: '我的台灣電商', // 你的服務名稱
rpID: 'myshop.com.tw', // 你的網域
userID: user.id,
userName: user.email,
attestationType: 'none', // 一般應用不需要硬體認證
authenticatorSelection: {
residentKey: 'preferred', // 支援 Passkey(Discoverable Credential)
userVerification: 'preferred' // 偏好生物辨識驗證
}
});
// 暫存 challenge,等驗證時比對
await saveChallenge(user.id, options.challenge);
res.json(options);
});
// 步驟二:驗證註冊結果
app.post('/api/auth/passkey/register-verify', async (req, res) => {
const user = req.user;
const expectedChallenge = await getChallenge(user.id);
const verification = await verifyRegistrationResponse({
response: req.body,
expectedChallenge,
expectedOrigin: 'https://myshop.com.tw',
expectedRPID: 'myshop.com.tw'
});
if (verification.verified) {
// 儲存公鑰,之後登入驗證用
await savePasskey(user.id, verification.registrationInfo);
res.json({ success: true });
}
});
2.4 身份驗證機制選擇指南
面對這麼多認證方式,怎麼選?以下是根據不同場景的建議:
| 場景 | 建議認證方式 | 理由 |
|---|---|---|
| 一般電商會員 | 密碼 + Email OTP(敏感操作時) | 平衡安全與便利 |
| 網路銀行 / 金融 App | 密碼 + TOTP/推播 + 生物辨識 | 金管會要求,高風險場景 |
| 內部管理後台 | SSO + MFA(TOTP 或硬體金鑰) | 管理員帳號一旦被盜,影響全系統 |
| API 服務對接 | API Key + mTLS | 機器對機器,不需要人類互動 |
| 微服務之間 | JWT + Service Mesh mTLS | 服務間信任建立 |
| 高安全需求(政府、軍事) | FIDO2 硬體金鑰 + 智慧卡 | 最高安全等級 |
三、Token 機制:認證之後,怎麼「記住」你?
使用者通過認證後,系統需要一種方式來「記住」他已經登入了,不用每次操作都重新輸入密碼。這就是 Token 的角色。
3.1 Session vs. JWT:兩種主流方案
| 比較維度 | Session(伺服器端) | JWT(客戶端) |
|---|---|---|
| 狀態儲存 | 伺服器(記憶體或 Redis) | 客戶端(Cookie 或 LocalStorage) |
| 擴展性 | 需要共享 Session Store(如 Redis) | 天然適合分散式架構 |
| 撤銷機制 | 簡單(直接刪除 Session) | 困難(需要額外的黑名單機制) |
| 適合場景 | 傳統 Web 應用、單體架構 | SPA、行動 App、微服務架構 |
| 安全考量 | CSRF 攻擊 | Token 過期管理、儲存安全 |
JWT 的正確使用方式:
const jwt = require('jsonwebtoken');
// ✅ JWT 簽發:只放必要資訊,不放敏感資料
function generateTokens(user) {
const accessToken = jwt.sign(
{
sub: user.id, // 使用者 ID
role: user.role, // 角色(但授權時要再到 DB 確認!)
type: 'access'
},
process.env.JWT_SECRET,
{
expiresIn: '15m', // Access Token 短效期:15 分鐘
issuer: 'myshop.com.tw',
audience: 'myshop-api'
}
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET, // 用不同的密鑰!
{ expiresIn: '7d' } // Refresh Token 較長:7 天
);
return { accessToken, refreshToken };
}
// ✅ JWT 驗證中間件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (!token) {
return res.status(401).json({ error: '未提供認證 Token' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'myshop.com.tw',
audience: 'myshop-api'
});
// ⚠️ 重要:不要只信任 Token 裡的角色資訊
// 要去資料庫確認使用者是否仍然有效
req.userId = decoded.sub;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token 已過期,請重新登入' });
}
return res.status(403).json({ error: '無效的 Token' });
}
}
飛飛觀點:
JWT 最常見的錯誤就是「把太多東西塞進 Token 裡,然後完全信任 Token 的內容」。記住:JWT 是「簽名」不是「加密」——任何人都可以 decode 看到裡面的內容。絕對不要在 JWT 裡放密碼、信用卡號、或任何敏感資料。而且,Token 裡的角色和權限只是「快取」,真正做授權決策時,還是要到資料庫確認最新狀態。
3.2 Token 儲存安全
Token 存在哪裡,直接決定它的安全性:
| 儲存位置 | XSS 風險 | CSRF 風險 | 建議 |
|---|---|---|---|
| <code>localStorage</code> | ❌ 高(JS 可讀) | ✅ 無 | 不建議存敏感 Token |
| <code>sessionStorage</code> | ❌ 高(JS 可讀) | ✅ 無 | 不建議存敏感 Token |
| <code>HttpOnly Cookie</code> | ✅ 低(JS 不可讀) | ❌ 需要 CSRF 防護 | 推薦方案 |
| <code>HttpOnly Cookie</code> + SameSite | ✅ 低 | ✅ 低 | 最佳實踐 |
// ✅ 最佳實踐:用 HttpOnly + Secure + SameSite Cookie 存 Token
res.cookie('access_token', accessToken, {
httpOnly: true, // JavaScript 無法讀取
secure: true, // 只在 HTTPS 下傳送
sameSite: 'Strict', // 阻擋跨站請求(防 CSRF)
maxAge: 15 * 60 * 1000, // 15 分鐘
path: '/api' // 只在 API 路徑下傳送
});
四、授權模型:RBAC vs. ABAC,選哪個?
認證解決了「你是誰」,接下來要解決「你能做什麼」。這就是授權模型的工作。
4.1 RBAC(Role-Based Access Control):基於角色的存取控制
RBAC 是最常見的授權模型,核心邏輯是:使用者 → 角色 → 權限。
用辦公室比喻:你的工作職稱(角色)決定你能進哪些房間(權限)。經理可以進會議室和辦公區,實習生只能進辦公區。
台灣電商平台的 RBAC 設計:
角色定義:
├── customer(一般會員)
│ ├── 瀏覽商品
│ ├── 下訂單
│ ├── 查看自己的訂單
│ └── 修改自己的個資
├── seller(賣家)
│ ├── 管理自己的商品(CRUD)
│ ├── 查看自己的訂單
│ ├── 回覆評論
│ └── 查看自己的銷售報表
├── customer_service(客服)
│ ├── 查看訂單(所有)
│ ├── 修改訂單狀態
│ └── 查看會員基本資料(遮蔽敏感欄位)
├── admin(管理員)
│ ├── 管理使用者帳號
│ ├── 管理系統設定
│ └── 查看所有報表
└── super_admin(超級管理員)
└── 所有權限 + 管理管理員帳號
Node.js RBAC 實作:
// 權限定義:每個角色有哪些權限
const ROLE_PERMISSIONS = {
customer: [
'product:read',
'order:create',
'order:read:own', // 只能讀自己的
'profile:read:own',
'profile:update:own'
],
seller: [
'product:read',
'product:create:own',
'product:update:own',
'product:delete:own',
'order:read:own',
'review:reply:own',
'report:read:own'
],
customer_service: [
'product:read',
'order:read:all',
'order:update:status',
'user:read:basic' // 只能看基本資料,敏感欄位遮蔽
],
admin: [
'user:read:all',
'user:update:all',
'user:delete:all',
'system:config:read',
'system:config:update',
'report:read:all'
]
};
// 授權中間件
function authorize(...requiredPermissions) {
return async (req, res, next) => {
// ⚠️ 從資料庫即時查詢角色,不只依賴 Token
const user = await db.query(
'SELECT role, status FROM users WHERE id = $1',
[req.userId]
);
if (!user || user.status !== 'active') {
return res.status(403).json({ error: '帳號已停用或不存在' });
}
const userPermissions = ROLE_PERMISSIONS[user.role] || [];
const hasPermission = requiredPermissions.every(
perm => userPermissions.includes(perm)
);
if (!hasPermission) {
await logSecurityEvent('AUTHORIZATION_DENIED', {
userId: req.userId,
role: user.role,
requiredPermissions,
path: req.path
});
return res.status(403).json({ error: '權限不足' });
}
req.userRole = user.role;
next();
};
}
// 使用範例
app.get('/api/orders',
authenticateToken,
authorize('order:read:all'),
async (req, res) => { /* ... */ }
);
app.delete('/api/users/:id',
authenticateToken,
authorize('user:delete:all'),
async (req, res) => { /* ... */ }
);
4.2 ABAC(Attribute-Based Access Control):基於屬性的存取控制
RBAC 簡單好用,但面對複雜的權限需求時會顯得力不從心。比如:
- 「賣家只能在營業時間修改商品」——這涉及時間屬性
- 「客服只能查看自己負責區域的訂單」——這涉及地理屬性
- 「只有通過 KYC 驗證的使用者才能購買高單價商品」——這涉及使用者屬性
這些場景用 RBAC 很難表達,因為你需要為每種組合建立一個角色,角色會爆炸性增長(「台北區日班客服」、「台北區夜班客服」、「高雄區日班客服」…)。
ABAC 的解法是:不只看角色,還看環境、資源、動作的各種屬性來做決策。
ABAC 決策公式:
允許?= f(主體屬性, 資源屬性, 動作屬性, 環境屬性)
主體屬性:角色、部門、KYC 狀態、信用等級...
資源屬性:資料分類、擁有者、敏感等級...
動作屬性:讀取、寫入、刪除、匯出...
環境屬性:時間、IP 位址、裝置、地理位置...
Node.js ABAC 實作範例:
// ABAC 策略引擎
class PolicyEngine {
constructor() {
this.policies = [];
}
addPolicy(policy) {
this.policies.push(policy);
}
evaluate(context) {
// 預設拒絕(Deny by Default)
let decision = 'DENY';
for (const policy of this.policies) {
const result = policy.evaluate(context);
if (result === 'DENY') return 'DENY'; // 任何明確拒絕 → 直接拒絕
if (result === 'ALLOW') decision = 'ALLOW';
}
return decision;
}
}
// 定義策略
const policyEngine = new PolicyEngine();
// 策略一:賣家只能在營業時間(08:00-22:00)修改商品
policyEngine.addPolicy({
name: 'seller-business-hours',
evaluate: (ctx) => {
if (ctx.subject.role !== 'seller') return 'NOT_APPLICABLE';
if (ctx.action !== 'update' || ctx.resource.type !== 'product') return 'NOT_APPLICABLE';
const hour = new Date().getHours();
if (hour >= 8 && hour < 22) return 'ALLOW';
return 'DENY'; // 非營業時間拒絕修改
}
});
// 策略二:只有通過 KYC 的使用者才能購買 NT$50,000 以上的商品
policyEngine.addPolicy({
name: 'high-value-purchase-kyc',
evaluate: (ctx) => {
if (ctx.action !== 'purchase') return 'NOT_APPLICABLE';
if (ctx.resource.price < 50000) return 'NOT_APPLICABLE';
if (ctx.subject.kycVerified) return 'ALLOW';
return 'DENY';
}
});
// 策略三:客服只能查看自己負責區域的訂單
policyEngine.addPolicy({
name: 'cs-regional-access',
evaluate: (ctx) => {
if (ctx.subject.role !== 'customer_service') return 'NOT_APPLICABLE';
if (ctx.action !== 'read' || ctx.resource.type !== 'order') return 'NOT_APPLICABLE';
if (ctx.subject.region === ctx.resource.region) return 'ALLOW';
return 'DENY';
}
});
// 在 API 中使用
app.put('/api/products/:id',
authenticateToken,
async (req, res) => {
const product = await getProduct(req.params.id);
const user = await getUser(req.userId);
const decision = policyEngine.evaluate({
subject: { role: user.role, region: user.region, kycVerified: user.kycVerified },
resource: { type: 'product', ownerId: product.sellerId, price: product.price },
action: 'update',
environment: { time: new Date(), ip: req.ip }
});
if (decision !== 'ALLOW') {
return res.status(403).json({ error: '目前無法執行此操作' });
}
// 執行更新...
}
);
4.3 RBAC vs. ABAC:怎麼選?
| 比較維度 | RBAC | ABAC |
|---|---|---|
| 複雜度 | 低,容易理解和維護 | 高,策略多了容易混亂 |
| 彈性 | 低,角色固定 | 高,可依各種屬性動態決策 |
| 適合場景 | 角色明確、權限規則簡單 | 權限需要考慮多種條件 |
| 效能 | 快(查表即可) | 較慢(每次要評估多條策略) |
| 稽核 | 容易(誰有什麼角色一目了然) | 複雜(需要追蹤哪條策略允許了什麼) |
| 台灣常見場景 | 一般電商、CMS、內部管理系統 | 金融業、醫療、多租戶 SaaS |
飛飛觀點:
90% 的系統用 RBAC 就夠了。不要為了追求技術的先進性而導入 ABAC——除非你的業務需求真的需要。如果你發現 RBAC 的角色數量開始爆炸(超過 15 個角色),那就是該考慮 ABAC 的時候了。一個務實的做法是:先用 RBAC 打底,對少數複雜場景補充 ABAC 策略。
五、從城堡到公路:Zero Trust 架構入門
5.1 傳統模型的問題:城堡與護城河
傳統的網路安全模型就像中世紀的城堡——一旦你通過護城河(防火牆)和城門(VPN),進入城堡內部後就自由了。這叫做邊界安全(Perimeter Security)。
但這個模型有致命的問題:
- 攻擊者一旦突破邊界(釣魚、VPN 漏洞、內鬼),就可以在內網橫向移動
- 雲端和遠端工作讓「邊界」變得模糊——你的員工在咖啡廳用公共 Wi-Fi 連 VPN,邊界在哪?
- 微服務架構下,服務之間的通訊不再只在「城堡內」
5.2 Zero Trust 的核心原則:「永不信任,持續驗證」
Zero Trust 不是一個產品,而是一種架構設計哲學。它的核心前提是:不管你在哪裡(公司內網還是咖啡廳)、不管你是誰(員工還是合作夥伴),每一次存取都要被驗證和授權。
用蓋房子比喻:傳統模型是「社區大門有門禁,但進了社區每戶都不鎖門」;Zero Trust 是「每一道門都有獨立的門鎖和監視器,每次進入都要刷卡」。
Zero Trust 的三大原則:
| 原則 | 說明 | 實踐方式 |
|---|---|---|
| 永不信任 | 不因為在內網就自動信任 | 所有請求都需要認證和授權 |
| 持續驗證 | 不是登入一次就永遠信任 | 持續評估裝置狀態、行為異常、Token 效期 |
| 最小權限 | 只給完成當下任務所需的最小權限 | 動態權限調整、Just-in-Time Access |
5.3 在 Node.js 微服務中實踐 Zero Trust
// 微服務之間的 Zero Trust 驗證中間件
async function zeroTrustMiddleware(req, res, next) {
// 1. 驗證呼叫者身份(服務對服務認證)
const serviceToken = req.headers['x-service-token'];
if (!verifyServiceToken(serviceToken)) {
return res.status(401).json({ error: '未認證的服務呼叫' });
}
// 2. 檢查呼叫者是否有權限存取此端點
const callerService = decodeServiceToken(serviceToken).serviceName;
const allowedCallers = getEndpointPolicy(req.path, req.method);
if (!allowedCallers.includes(callerService)) {
await logSecurityEvent('ZERO_TRUST_DENIED', {
caller: callerService,
path: req.path,
reason: 'SERVICE_NOT_ALLOWED'
});
return res.status(403).json({ error: '此服務無權存取此端點' });
}
// 3. 檢查呼叫頻率是否異常
const callRate = await getServiceCallRate(callerService, req.path);
if (callRate > THRESHOLD) {
await logSecurityEvent('ZERO_TRUST_ANOMALY', {
caller: callerService,
callRate,
threshold: THRESHOLD
});
return res.status(429).json({ error: '呼叫頻率異常' });
}
// 4. 如果請求攜帶使用者 Token,也要驗證
if (req.headers['authorization']) {
try {
const userToken = req.headers['authorization'].split(' ')[1];
const decoded = jwt.verify(userToken, process.env.JWT_SECRET);
req.userId = decoded.sub;
} catch (err) {
return res.status(401).json({ error: '使用者 Token 無效' });
}
}
// 5. 記錄所有存取行為(稽核日誌)
await logAccess({
caller: callerService,
userId: req.userId || 'system',
path: req.path,
method: req.method,
timestamp: new Date()
});
next();
}
// 服務間的端點存取策略
function getEndpointPolicy(path, method) {
const policies = {
'GET /api/users/:id': ['order-service', 'notification-service'],
'POST /api/payments': ['order-service'],
'PUT /api/orders/:id/status': ['delivery-service', 'payment-service'],
// 每個端點明確列出哪些服務可以呼叫
};
return policies[<code class="kb-btn">${method} ${path}</code>] || [];
}
5.4 Zero Trust 落地路線圖
不可能一夜之間變成 Zero Trust。以下是分階段導入的務實建議:
階段一:盤點與基礎建設(1-3 個月)
- 盤點所有系統、服務、使用者、資料流
- 為所有內部服務加上認證(不再依賴「內網就安全」)
- 導入集中式身份管理(如 Keycloak、Auth0)
階段二:強化存取控制(3-6 個月)
- 實施最小權限原則,盤點並收回過大的權限
- 所有管理員帳號啟用 MFA
- 導入 API Gateway 統一管理存取
階段三:持續監控與動態信任(6-12 個月)
- 建立行為異常偵測機制
- 導入裝置信任評估(裝置是否更新、是否有防毒)
- 實施 Just-in-Time 權限(需要時申請,用完自動撤銷)
六、實戰案例:台灣線上訂餐平台的認證與授權設計
讓我們用一個完整的案例,把本篇所有觀念串起來。
場景
你的團隊正在為一間台灣連鎖餐飲集團設計線上訂餐平台。系統有四種角色:消費者、店家、外送員、總部管理員。
認證架構設計
消費者:Email/密碼 + 手機 OTP(首次登入新裝置時)
支援 Line Login 社群登入(OAuth 2.0)
支援 Passkey(可選)
店家: Email/密碼 + TOTP(Google Authenticator)
必須啟用 MFA
外送員:手機號碼 + 簡訊 OTP(每次登入)
GPS 位置驗證(確認在營業區域內)
管理員:SSO(公司 AD 整合)+ FIDO2 硬體金鑰
IP 白名單(只能從辦公室網路登入)
授權架構設計(RBAC + 部分 ABAC)
// RBAC 基礎權限
const ROLE_PERMISSIONS = {
customer: [
'menu:read',
'order:create',
'order:read:own',
'order:cancel:own', // 只能取消自己的訂單
'review:create',
'payment:create:own'
],
store: [
'menu:manage:own', // 只能管理自家菜單
'order:read:own_store', // 只能看到自家的訂單
'order:update:status:own', // 只能更新自家訂單狀態
'review:reply:own'
],
delivery: [
'order:read:assigned', // 只能看被指派的訂單
'order:update:delivery', // 只能更新配送狀態
'location:update:own' // 只能更新自己的位置
],
admin: [
'user:manage:all',
'store:manage:all',
'order:read:all',
'report:read:all',
'system:config:manage'
]
};
// ABAC 補充策略
const abacPolicies = [
// 消費者只能在訂單建立後 5 分鐘內取消
{
name: 'order-cancel-time-limit',
evaluate: (ctx) => {
if (ctx.action !== 'cancel' || ctx.resource.type !== 'order') return 'NOT_APPLICABLE';
const minutesSinceCreated = (Date.now() - ctx.resource.createdAt) / 60000;
return minutesSinceCreated <= 5 ? 'ALLOW' : 'DENY';
}
},
// 店家只能在營業時間接受新訂單
{
name: 'store-business-hours',
evaluate: (ctx) => {
if (ctx.subject.role !== 'store') return 'NOT_APPLICABLE';
if (ctx.action !== 'accept') return 'NOT_APPLICABLE';
const now = new Date();
const hour = now.getHours();
return (hour >= ctx.subject.openHour && hour < ctx.subject.closeHour)
? 'ALLOW' : 'DENY';
}
},
// 外送員只能在被指派的配送區域內操作
{
name: 'delivery-zone-restriction',
evaluate: (ctx) => {
if (ctx.subject.role !== 'delivery') return 'NOT_APPLICABLE';
return ctx.subject.assignedZone === ctx.resource.deliveryZone
? 'ALLOW' : 'DENY';
}
}
];
Token 策略
Access Token: 15 分鐘,HttpOnly Cookie,存使用者 ID 和角色
Refresh Token: 消費者 30 天、店家 7 天、管理員 1 天
密碼重設 Token:30 分鐘,一次性使用
Email 驗證 Token:24 小時,一次性使用
所有 Token 簽發時記錄:IP、User-Agent、裝置指紋
Token 被使用時,如果 IP 或裝置指紋與簽發時不同 → 觸發 MFA 重新驗證
七、認證與授權設計 Checklist
以下是一份可以直接用於專案的檢核清單:
## 認證設計 Checklist
### 密碼安全
- [ ] 密碼使用 Argon2id 或 bcrypt 雜湊(不是 MD5 / SHA-256)
- [ ] 最低密碼長度 ≥ 12 碼
- [ ] 不限制密碼字元類型(允許 Unicode)
- [ ] 登入失敗 5 次鎖定帳號 15 分鐘
- [ ] 錯誤訊息統一為「帳號或密碼錯誤」
- [ ] 密碼重設連結 30 分鐘過期,一次性使用
- [ ] 密碼重設後撤銷所有已簽發的 Token
### 多因素認證
- [ ] 管理員帳號強制啟用 MFA
- [ ] 敏感操作(改密碼、改 Email、大額交易)要求 MFA
- [ ] 新裝置首次登入要求額外驗證
- [ ] 提供多種 MFA 選項(不只有簡訊 OTP)
### Token 管理
- [ ] Access Token 效期 ≤ 30 分鐘
- [ ] Refresh Token 有適當的效期
- [ ] Token 存在 HttpOnly + Secure + SameSite Cookie
- [ ] JWT 不包含敏感資料(密碼、信用卡號)
- [ ] 有 Token 撤銷機制(登出、密碼變更時)
### 登入安全
- [ ] 所有登入行為(成功/失敗)都有日誌記錄
- [ ] 日誌中不包含密碼(含雜湊值)
- [ ] 有異常登入偵測(異地登入、短時間大量嘗試)
- [ ] 社群登入(OAuth)有 state 參數防 CSRF
## 授權設計 Checklist
### 存取控制
- [ ] 預設拒絕(Deny by Default),明確授予權限
- [ ] 每個 API 端點都有授權檢查(不依賴前端隱藏)
- [ ] 授權決策在伺服器端,不信任前端傳來的角色資訊
- [ ] 資源存取檢查所有權(使用者只能存取自己的資料)
- [ ] 權限變更即時生效(不等 Token 過期)
### RBAC 設計
- [ ] 角色定義清楚,職責不重疊
- [ ] 管理員權限分級(不是只有一個 admin 角色)
- [ ] 角色指派有審核機制(不能自己升級自己的角色)
- [ ] 定期盤點權限,移除不再需要的角色指派
### 日誌與稽核
- [ ] 所有授權失敗都記錄日誌
- [ ] 權限變更(角色指派、權限調整)都有稽核紀錄
- [ ] 異常存取行為有即時告警
八、常見問題 FAQ
Q1:JWT 和 Session 我到底該選哪個?
如果你的架構是傳統的 Server-Side Rendering(如 Express + EJS),或者是單體架構,用 Session 就好——簡單、撤銷方便、不需要處理 Token 過期的複雜邏輯。
如果你的架構是 SPA + API、行動 App、或微服務,JWT 是更自然的選擇——無狀態、適合分散式系統、跨服務認證方便。
但記住:不管選哪個,安全原則都一樣——短效期、安全儲存、可撤銷。
Q2:我的系統很小,需要考慮 Zero Trust 嗎?
Zero Trust 的完整架構確實比較適合中大型組織。但它的核心精神——「每次存取都驗證」——不管系統多小都適用。
對小團隊來說,至少做到以下三點:
- 每個 API 端點都有認證檢查(不要有「匿名可存取的內部 API」)
- 管理員帳號啟用 MFA
- 不要因為「在內網」就省略認證
這三件事不需要買任何工具,花一天就能做到。
Q3:RBAC 的角色太多了怎麼辦?
角色爆炸通常有兩個原因:
-
把「權限」當成「角色」:比如你建了「可匯出報表的客服」、「不可匯出報表的客服」兩個角色,其實應該是一個「客服」角色加上「報表匯出」這個獨立權限。
-
把「組織結構」塞進角色:比如「台北區主管」、「高雄區主管」。組織結構應該用 ABAC 的屬性來處理,不是建新角色。
如果角色超過 15 個,建議重新審視角色設計,看看是否需要引入 ABAC 來處理動態條件。
Q4:這些跟台灣的法規有什麼關係?
台灣個資法要求「適當安全維護措施」,認證與授權就是最核心的安全措施之一。具體來說:
- 個資法 §27:要有適當的存取控制機制(就是授權)
- 金融資安行動方案:要求電子交易必須有多因素驗證
- 資通安全管控指引:上市櫃公司需要有帳號管理與存取控制機制
做好認證與授權,不只是安全最佳實踐,也是法規遵循的基本要求。
九、結語:認證是門鎖,授權是房間鑰匙,Zero Trust 是每道門都上鎖
回到蓋房子的比喻。一棟安全的建築,不會只在大門口放一個保全就覺得萬事大吉。每一層樓有門禁、每一間辦公室有獨立的鑰匙、金庫還需要雙人同時開啟——這就是認證、授權、Zero Trust 在建築中的體現。
軟體系統也是一樣。認證確認「你是誰」,授權決定「你能做什麼」,Zero Trust 確保「每一次存取都經過驗證」。這三者不是三個獨立的系統,而是層層遞進的安全架構。
很多開發者會說:「這些東西好複雜,我們先上線再說。」但認證和授權是系統的骨架——就像承重牆一樣,事後要改的成本極高。如果你在設計階段就把認證和授權的架構想清楚,後續的開發反而會更順暢,因為你不需要一直回頭修補。
就像這個系列一直在說的:安全不是恐懼,而是創造的基礎。 堅固的認證與授權架構,不是限制你的創造力,而是讓你可以安心地在上面建構更豐富、更有價值的功能。
下一篇,我們會進入安全設計的另一個重要主題——API 安全設計:保護你的資料入口。認證授權的架構有了,接下來要看看怎麼把 API 這個對外大門守好。