Skip to content

Commit b76f4a6

Browse files
committed
新增 Express.js 認證中介軟體標準實務筆記,詳細說明使用 res.locals 的好處與最佳實作
1 parent 54d0a4f commit b76f4a6

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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

Comments
 (0)