106 lines
3.1 KiB
Markdown
106 lines
3.1 KiB
Markdown
|
|
---
|
|||
|
|
tags: [后端, Go, Gin, 中间件, JWT]
|
|||
|
|
create time: 2026-04-27 12:51
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# JWT 中间件:认证失败后 Context 数据安全性
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
回答父文档中提出的思考题:JWT 认证中间件中,如果认证失败并调用了 `c.Abort()`,之前设置的 `userID` 等用户信息是否会被后续 handler 读到?
|
|||
|
|
|
|||
|
|
## 正文
|
|||
|
|
|
|||
|
|
### 问题描述
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func jwtAuth() gin.HandlerFunc {
|
|||
|
|
return func(c *gin.Context) {
|
|||
|
|
token := c.GetHeader("Authorization")
|
|||
|
|
if token == "" {
|
|||
|
|
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "missing token"})
|
|||
|
|
c.Abort()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
claims, err := parseJWT(token)
|
|||
|
|
if err != nil {
|
|||
|
|
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid token"})
|
|||
|
|
c.Abort()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 把用户信息存入 Context
|
|||
|
|
c.Set("userID", claims.UserID)
|
|||
|
|
c.Next()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问:** 如果认证失败(`c.Abort()`),那 `userID` 还会被后面的 handler 读到吗?为什么?
|
|||
|
|
|
|||
|
|
### 答案:不会
|
|||
|
|
|
|||
|
|
#### 原因一:代码层面 — `return` 阻断了执行流
|
|||
|
|
|
|||
|
|
认证失败分支中,`c.Abort()` 之后紧跟 `return`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
if token == "" {
|
|||
|
|
c.JSON(...) // ① 写入 401 响应体
|
|||
|
|
c.Abort() // ② 标记终止链
|
|||
|
|
return // ③ 函数直接退出
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
c.Set("userID", ...) // ← ④ 永远不会执行到
|
|||
|
|
c.Next() // 永远不会执行到
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`c.Set()` 和 `c.Next()` 在认证通过的分支之后,一旦进入失败分支就会通过 `return` 提前返回,这两行代码根本无法执行。
|
|||
|
|
|
|||
|
|
#### 原因二:机制层面 — `c.Abort()` 阻断中间件链
|
|||
|
|
|
|||
|
|
即使忘记写 `return`,Gin 的中间件调度器也会自动阻止后续 handler 执行:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// gin/context.go 核心逻辑
|
|||
|
|
func (c *Context) Next() {
|
|||
|
|
c.index++
|
|||
|
|
for ; c.index < int8(len(c.handlers)); c.index++ {
|
|||
|
|
if c.IsAborted() { // ← Abort() 会将 IsAborted 置为 true
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
c.handlers[c.index](c)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`c.Abort()` 的作用是让 `IsAborted()` 返回 `true`,导致 `Next()` 中的循环立即终止。
|
|||
|
|
|
|||
|
|
### 完整的执行链路
|
|||
|
|
|
|||
|
|
| 步骤 | 动作 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 1 | `c.JSON(401)` | 写入错误响应体 |
|
|||
|
|
| 2 | `c.Abort()` | 设置内部标志位 `isAborted = true` |
|
|||
|
|
| 3 | `return` | 中间件函数直接退出 |
|
|||
|
|
| 4 | — | `c.Set()` 未执行 → Context 中无 `userID` |
|
|||
|
|
| 5 | — | `c.Next()` 未调用 → 后续所有 handler 跳过 |
|
|||
|
|
|
|||
|
|
### 设计启示
|
|||
|
|
|
|||
|
|
这个模式体现了一个重要的工程原则:**先失败、快速返回,成功才继续**。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
验证输入 → 失败? → 快速返回 ────→ 不会污染后续状态
|
|||
|
|
↓通过
|
|||
|
|
写入上下文 → 交给下游
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这种 **"Guard Clause"** 风格天然保证了敏感信息(用户身份)只会在验证通过后才被注入到 Context,不会因为代码顺序倒置而意外泄露。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[GIN/3-middleware]] — 中间件完整机制
|
|||
|
|
- [[middleware-abort]] — `c.Abort()` 终止机制详解
|