
在 Authorization 相關的系統中,近幾年來始終都是講求無狀態的方式處理授權相關的問題
最常見的做法就是 jwt,在早期 jwt 出現之前,大部分的系統要碼是以 cookies 配合 Session
的方式去處理授權相關問題,因為 jwt 是無狀態的,所以在授權有效之前還是會需要一個緩存處理
當然隨著系統的增大,jwt 作為微服務,基本上已經算是一個基本標配
但同時我們需要 Refresh Token 去處理相關問題,
Session 與 Jwt 並不是互斥的事情,相反混合架構可以使用兩者的優點去實現對系統的保障
我們為何需要做混合架構?以下我用一張圖表來展示相關議題
| 特性 | 純 JWT | 純 Session | JWT + Session 混合 |
|---|---|---|---|
| 無狀態驗證 | ✅ 快 | ❌ 需查 DB/Redis | ✅ Access Token 免查詢 |
| 可即時撤銷 | ❌ 難以撤銷 | ✅ 刪除即失效 | ✅ Refresh Token 可撤銷 |
| 跨服務驗證 | ✅ 適合微服務 | ❌ 需共享 Session Store | ✅ Access Token 可跨服務 |
| Token 外洩風險 | 高(長效期) | 低(可即時清除) | 低(Access Token 短效期) |
| 實作複雜度 | 低 | 中 | 中高 |
大部分現在的工程師普遍以 jwt 作為短效期授權的解決方案,但要考慮 Refresh token 進行長效期的
解決方案可能較為生疏,今天我們就談及混合方案的部分
在開始實作前,我們需要了解一下整個藍圖(如下圖)要做什麼,以便我們去把整個部分做出來
Access token (Jwt) : 短效期的 token 我們只用於存在記憶體或是 Authorization header 中
用來驗證 api 不查資料庫,
Refresh token : 長效型 token, 存在資料庫或是 redis 並且綁定 httpOnly cookies,用來換新的
Access Token,且可以被 server 主動撤銷(登出或是被盜用時強制下線)
這樣的模式是有專有名詞的,就是所謂的 Rotating Refresh Token 或是 Token Rotation
我們融合前面所有的知識點,bun 有內建的 sqlite 所以我們用 sqlite 去處理相關的,
我們建立兩張資料表 : users 還有 sessions 並且去關聯 user_id
並且優化一些查詢的設定
可以參考以下的 code
import { Database } from 'bun:sqlite';
export const db = new Database('auth.sqlite', { create: true });
db.run(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
user_agent TEXT,
ip_address TEXT,
expires_at INTEGER NOT NULL,
revoked_at INTEGER,
created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
`)
試跑看看結果

bun 最威猛的就是他內部就有提供雜湊密碼的方法,以下提供 :
(這裡我們用 ./src/password.ts)
// 這個是雜湊用的
const hashPassword = async (input: string): Promise<string> => {
return Bun.password.hash(input, {
algorithm: 'argon2d',
memoryCost: 19456, // 19 MB OWASP 建議值
timeCost: 2,
});
}
// 這個是驗證用的
const verifyPassword = async (input: string, hashInput: string): Promise<boolean> => {
return Bun.password.verify(input, hashInput);
}
我們簡單驗證一下結果

目前已經完成的部分
下一天我們接續往 jwt 走!Go!Go!Go