|
| 1 | +--- |
| 2 | +title: " [學習筆記] Express.js middleware auth 的業界標準(res.locals)" |
| 3 | +date: 2025/07/25 11:17:04 |
| 4 | +--- |
| 5 | + |
| 6 | +## 前言 |
| 7 | + |
| 8 | +在 Express.js 認證系統中,常見的做法是在每個需要驗證的路由中直接解析 JWT token。 |
| 9 | +但有一個更好的實作方式:使用中介軟體將認證結果存放在 `res.locals` 中。 |
| 10 | +這不僅是官方建議的做法,也避免了重複解析 token 的效能問題。 |
| 11 | + |
| 12 | +## TL;DR |
| 13 | + |
| 14 | +使用 `res.locals` 處理認證是 Express.js 的標準實作: |
| 15 | + |
| 16 | +- 避免在每個路由重複解析 JWT token |
| 17 | +- Auth.js、Passport.js 等主流函式庫都採用這種模式 |
| 18 | +- Express.js 官方文檔明確支持這種用法 |
| 19 | + |
| 20 | +## 直接解析 JWT 的缺點 |
| 21 | + |
| 22 | +### 重複工作的效能問題 |
| 23 | + |
| 24 | +如果每個路由都直接解析 JWT,會造成不必要的重複運算: |
| 25 | + |
| 26 | +```javascript |
| 27 | +// ❌ 在每個路由重複解析 |
| 28 | +app.get('/profile', (req, res) => { |
| 29 | + const token = req.headers.authorization?.split(' ')[1] |
| 30 | + const user = jwt.verify(token, JWT_SECRET) // 重複解析 |
| 31 | + res.json({ user }) |
| 32 | +}) |
| 33 | + |
| 34 | +app.get('/orders', (req, res) => { |
| 35 | + const token = req.headers.authorization?.split(' ')[1] |
| 36 | + const user = jwt.verify(token, JWT_SECRET) // 又解析一次 |
| 37 | + res.json({ orders: getUserOrders(user.id) }) |
| 38 | +}) |
| 39 | +``` |
| 40 | +
|
| 41 | +### 程式碼重複與維護困難 |
| 42 | +
|
| 43 | +每個需要認證的路由都要寫類似的 token 驗證邏輯,違反了 DRY 原則,也增加了維護成本。 |
| 44 | +
|
| 45 | +### 錯誤處理不一致 |
| 46 | +
|
| 47 | +不同路由可能有不同的 token 驗證錯誤處理方式,造成 API 回應不一致。 |
| 48 | +
|
| 49 | +## 什麼是 res.locals? |
| 50 | +
|
| 51 | +根據 Express.js 官方文檔,`res.locals` 是一個物件,用來存放**請求範圍內的區域變數**,這些變數只在當前請求-回應週期中可用。 |
| 52 | +
|
| 53 | +```javascript |
| 54 | +// Express.js 官方範例 |
| 55 | +app.use((req, res, next) => { |
| 56 | + res.locals.user = req.user |
| 57 | + res.locals.authenticated = !req.user |
| 58 | + next() |
| 59 | +}) |
| 60 | +``` |
| 61 | +
|
| 62 | +## 主流函式庫的標準實作 |
| 63 | +
|
| 64 | +### Auth.js 官方範例 |
| 65 | +
|
| 66 | +Auth.js 官方文檔明確展示了使用 `res.locals` 的標準模式: |
| 67 | +
|
| 68 | +```javascript |
| 69 | +import { getSession } from "@auth/express" |
| 70 | + |
| 71 | +export function authSession(req, res, next) { |
| 72 | + res.locals.session = await getSession(req) |
| 73 | + next() |
| 74 | +} |
| 75 | + |
| 76 | +app.use(authSession) |
| 77 | + |
| 78 | +// 在路由中使用 |
| 79 | +app.get("/", (req, res) => { |
| 80 | + const { session } = res.locals |
| 81 | + res.render("index", { user: session?.user }) |
| 82 | +}) |
| 83 | +``` |
| 84 | +
|
| 85 | +### Passport.js 的實作模式 |
| 86 | +
|
| 87 | +Passport.js 在認證成功時會設置 `req.user` 屬性,許多開發者會將此資訊複製到 `res.locals` 以便在視圖中使用: |
| 88 | +
|
| 89 | +```javascript |
| 90 | +// 認證中介軟體 |
| 91 | +app.use((req, res, next) => { |
| 92 | + res.locals.user = req.isAuthenticated() ? req.user : null |
| 93 | + res.locals.isAuthenticated = req.isAuthenticated() |
| 94 | + next() |
| 95 | +}) |
| 96 | +``` |
| 97 | +
|
| 98 | +## 為什麼這是好實作? |
| 99 | +
|
| 100 | +### 1. 官方支持的標準 |
| 101 | +
|
| 102 | +Express.js 官方文檔說明 `res.locals` 就是用來存放請求級別的資訊,如認證用戶、用戶設定等。這不是 hack 或 workaround,而是設計用途。 |
| 103 | +
|
| 104 | +### 2. 生態系統共識 |
| 105 | +
|
| 106 | +主流的認證函式庫都採用類似模式: |
| 107 | +
|
| 108 | +- Auth.js 直接在文檔中展示 `res.locals.session` |
| 109 | +- Passport.js 社群普遍使用 `res.locals.user` |
| 110 | +- 許多教學都展示將認證狀態存放在 `res.locals` 的模式 |
| 111 | +
|
| 112 | +### 3. 分離關注點 |
| 113 | +
|
| 114 | +```javascript |
| 115 | +// 認證中介軟體 - 只負責認證 |
| 116 | +function authMiddleware(req, res, next) { |
| 117 | + const token = req.headers.authorization?.split(' ')[1] |
| 118 | + |
| 119 | + if (token) { |
| 120 | + const decoded = jwt.verify(token, JWT_SECRET) |
| 121 | + res.locals.user = decoded |
| 122 | + } |
| 123 | + |
| 124 | + next() |
| 125 | +} |
| 126 | + |
| 127 | +// 路由處理器 - 只負責業務邏輯 |
| 128 | +app.get('/profile', authMiddleware, (req, res) => { |
| 129 | + const { user } = res.locals |
| 130 | + if (!user) { |
| 131 | + return res.status(401).json({ error: 'Unauthorized' }) |
| 132 | + } |
| 133 | + res.json({ profile: user }) |
| 134 | +}) |
| 135 | +``` |
| 136 | +
|
| 137 | +### 4. 視圖整合優勢 |
| 138 | +
|
| 139 | +在使用模板引擎時,`res.locals` 的資料會自動傳遞給視圖: |
| 140 | +
|
| 141 | +```javascript |
| 142 | +// 中介軟體設定 |
| 143 | +app.use((req, res, next) => { |
| 144 | + res.locals.currentUser = req.user |
| 145 | + next() |
| 146 | +}) |
| 147 | + |
| 148 | +// 在 EJS/Pug 模板中直接使用 |
| 149 | +// <%= currentUser.name %> |
| 150 | +``` |
| 151 | +
|
| 152 | +## 常見的反模式 |
| 153 | +
|
| 154 | +### 避免的做法 |
| 155 | +
|
| 156 | +有些開發者認為過度使用 `res.locals` 會讓除錯變困難,但這通常是因為: |
| 157 | +
|
| 158 | +1. **濫用中介軟體模式** - 把業務邏輯也放在中介軟體裡 |
| 159 | +2. **過度耦合** - 多個中介軟體互相依賴 `res.locals` 的順序 |
| 160 | +
|
| 161 | +### 正確的使用原則 |
| 162 | +
|
| 163 | +中介軟體應該用於所有 HTTP 請求共通的事項,且不包含業務邏輯: |
| 164 | +
|
| 165 | +```javascript |
| 166 | +// ✅ 好的做法 - 純粹的認證檢查 |
| 167 | +function authenticate(req, res, next) { |
| 168 | + const token = getTokenFromRequest(req) |
| 169 | + const user = validateToken(token) |
| 170 | + res.locals.user = user |
| 171 | + next() |
| 172 | +} |
| 173 | + |
| 174 | +// ✅ 好的做法 - 授權檢查 |
| 175 | +function requireAuth(req, res, next) { |
| 176 | + if (!res.locals.user) { |
| 177 | + return res.status(401).json({ error: 'Unauthorized' }) |
| 178 | + } |
| 179 | + next() |
| 180 | +} |
| 181 | + |
| 182 | +// ❌ 避免的做法 - 在中介軟體中處理業務邏輯 |
| 183 | +function badMiddleware(req, res, next) { |
| 184 | + const user = res.locals.user |
| 185 | + const orders = getUserOrders(user.id) // 業務邏輯不應該在這裡 |
| 186 | + res.locals.orders = orders |
| 187 | + next() |
| 188 | +} |
| 189 | +``` |
| 190 | +
|
| 191 | +## 實務上的最佳實作 |
| 192 | +
|
| 193 | +### 標準認證中介軟體 |
| 194 | +
|
| 195 | +```javascript |
| 196 | +const jwt = require('jsonwebtoken') |
| 197 | + |
| 198 | +function authMiddleware(req, res, next) { |
| 199 | + try { |
| 200 | + const token = req.headers.authorization?.replace('Bearer ', '') |
| 201 | + |
| 202 | + if (token) { |
| 203 | + const decoded = jwt.verify(token, process.env.JWT_SECRET) |
| 204 | + res.locals.user = decoded |
| 205 | + res.locals.isAuthenticated = true |
| 206 | + } else { |
| 207 | + res.locals.user = null |
| 208 | + res.locals.isAuthenticated = false |
| 209 | + } |
| 210 | + |
| 211 | + next() |
| 212 | + } catch (error) { |
| 213 | + res.locals.user = null |
| 214 | + res.locals.isAuthenticated = false |
| 215 | + next() |
| 216 | + } |
| 217 | +} |
| 218 | + |
| 219 | +// 授權檢查中介軟體 |
| 220 | +function requireAuth(req, res, next) { |
| 221 | + if (!res.locals.isAuthenticated) { |
| 222 | + return res.status(401).json({ error: 'Authentication required' }) |
| 223 | + } |
| 224 | + next() |
| 225 | +} |
| 226 | + |
| 227 | +// 使用方式 |
| 228 | +app.use(authMiddleware) |
| 229 | +app.get('/profile', requireAuth, (req, res) => { |
| 230 | + const { user } = res.locals |
| 231 | + res.json({ user }) |
| 232 | +}) |
| 233 | +``` |
| 234 | +
|
| 235 | +### 與 req 自定義屬性的比較 |
| 236 | +
|
| 237 | +雖然也可以使用 `req.user` 或 `req.data` 等自定義屬性,但 `res.locals` 有幾個優勢: |
| 238 | +
|
| 239 | +1. **語意清晰** - 明確表示這是給視圖用的資料 |
| 240 | +2. **自動傳遞** - 模板引擎會自動取用 `res.locals` 的資料 |
| 241 | +3. **標準化** - 社群共識,維護性更好 |
| 242 | +
|
| 243 | +## 結論 |
| 244 | +
|
| 245 | +`res.locals` 在認證系統中的使用確實是標準實作: |
| 246 | +
|
| 247 | +1. **官方支持** - Express.js 和 Auth.js 官方文檔都展示這種用法 |
| 248 | +2. **生態系統標準** - Passport.js 等主流函式庫採用相同模式 |
| 249 | +3. **效能考量** - 避免重複解析 JWT 或查詢資料庫 |
| 250 | +4. **開發體驗** - 控制器可以直接存取用戶資訊 |
| 251 | +
|
| 252 | +所以下次有人說使用 `res.locals` 不是好實作時,可以拿出這些官方文檔來證明這確實是被廣泛接受的標準做法。 |
| 253 | +
|
| 254 | +## 參考資料 |
| 255 | +
|
| 256 | +- [Express.js Using middleware](https://expressjs.com/en/guide/using-middleware.html) |
| 257 | +- [Auth.js Express Integration](https://authjs.dev/reference/express) |
| 258 | +- [Passport.js Middleware Documentation](https://www.passportjs.org/concepts/authentication/middleware/) |
| 259 | +- [Express.js res.locals API Reference](https://expressjs.com/en/api.html#res.locals) |
| 260 | +
|
| 261 | +(fin) |
0 commit comments