[安全設計] 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 推動的下一代認證標準,目標是徹底取代密碼。它的原理是:

  1. 使用者的裝置(手機、電腦)生成一對公私鑰
  2. 公鑰存在伺服器,私鑰留在裝置上
  3. 認證時,伺服器發出挑戰(Challenge),裝置用私鑰簽名回應
  4. 伺服器用公鑰驗證簽名

這意味著:伺服器上完全沒有密碼或密碼雜湊,就算資料庫被入侵,攻擊者也拿不到任何可以用來登入的東西。

// 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 的完整架構確實比較適合中大型組織。但它的核心精神——「每次存取都驗證」——不管系統多小都適用。

對小團隊來說,至少做到以下三點:

  1. 每個 API 端點都有認證檢查(不要有「匿名可存取的內部 API」)
  2. 管理員帳號啟用 MFA
  3. 不要因為「在內網」就省略認證

這三件事不需要買任何工具,花一天就能做到。

Q3:RBAC 的角色太多了怎麼辦?

角色爆炸通常有兩個原因:

  1. 把「權限」當成「角色」:比如你建了「可匯出報表的客服」、「不可匯出報表的客服」兩個角色,其實應該是一個「客服」角色加上「報表匯出」這個獨立權限。

  2. 把「組織結構」塞進角色:比如「台北區主管」、「高雄區主管」。組織結構應該用 ABAC 的屬性來處理,不是建新角色。

如果角色超過 15 個,建議重新審視角色設計,看看是否需要引入 ABAC 來處理動態條件。

Q4:這些跟台灣的法規有什麼關係?

台灣個資法要求「適當安全維護措施」,認證與授權就是最核心的安全措施之一。具體來說:

  • 個資法 §27:要有適當的存取控制機制(就是授權)
  • 金融資安行動方案:要求電子交易必須有多因素驗證
  • 資通安全管控指引:上市櫃公司需要有帳號管理與存取控制機制

做好認證與授權,不只是安全最佳實踐,也是法規遵循的基本要求。


九、結語:認證是門鎖,授權是房間鑰匙,Zero Trust 是每道門都上鎖

回到蓋房子的比喻。一棟安全的建築,不會只在大門口放一個保全就覺得萬事大吉。每一層樓有門禁、每一間辦公室有獨立的鑰匙、金庫還需要雙人同時開啟——這就是認證、授權、Zero Trust 在建築中的體現。

軟體系統也是一樣。認證確認「你是誰」,授權決定「你能做什麼」,Zero Trust 確保「每一次存取都經過驗證」。這三者不是三個獨立的系統,而是層層遞進的安全架構。

很多開發者會說:「這些東西好複雜,我們先上線再說。」但認證和授權是系統的骨架——就像承重牆一樣,事後要改的成本極高。如果你在設計階段就把認證和授權的架構想清楚,後續的開發反而會更順暢,因為你不需要一直回頭修補。

就像這個系列一直在說的:安全不是恐懼,而是創造的基礎。 堅固的認證與授權架構,不是限制你的創造力,而是讓你可以安心地在上面建構更豐富、更有價值的功能。

下一篇,我們會進入安全設計的另一個重要主題——API 安全設計:保護你的資料入口。認證授權的架構有了,接下來要看看怎麼把 API 這個對外大門守好。


延伸閱讀