Files
cs-note/hzh/EXAM/Week03.md
T

527 lines
19 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
tags: [Go, 后端开发, 武科大, Week03, 错题精讲]
create time: 2026-05-02 14:30
---
# 金山考试 - 武科大服务端 - 第三次课 · 错题精讲
## 概述
本次测试涵盖 **HTTP 协议基础、Gin 框架核心机制、CORS 跨域、JWT 认证、Nginx 反向代理** 五大模块,共 30 道单选题。其中 **26 道答对,4 道错题**,集中在 HTTP 基础认知、Gin 中间件执行控制、以及 JWT 解析流程三个知识面上。本笔记逐一拆解错误原因,给出正解辨析和代码验证。
> [!tip] 使用建议
> 先看错题本身的「为什么错 → 为什么对」,再做对照表的自测检查,最后用「高频考点速览」串联记忆薄弱点。
---
## 一、HTTP 协议基础:GET/POST 与默认端口
### 单1:GET 和 POST 的主要区别
| 项目 | 内容 |
|------|------|
| ❌ 你的答案 | A(GET 请求数据,POST 只提交数据) |
| ✅ 正确答案 | **B(GET 数据附加在 URL 之后,POST 数据放在请求体内)** |
> [!error] 你的误区根源
> 你选了 **A**,描述的是一个"规范层面"的说法——从 RESTful 最佳实践来说 GET 应该用于获取资源、POST 用于创建资源。但题目问的是**主要区别**,而两者最根本的、决定性的差异在于**数据传输位置**。
#### 为什么 B 才是本质区别
```go
// GET — 参数拼在 URL 后面 ?key=value&name=xxx
GET /api/users?page=1&limit=10 HTTP/1.1
Host: example.com
// Body 为空
// POST — 参数放在 Body 中
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
page=1&limit=10
```
| 维度 | GET | POST |
|------|-----|------|
| 数据位置 | URL 查询字符串(`?key=value`) | 请求体(Body) |
| 数据长度 | 受 URL 长度限制(通常 ~2KB) | 理论上无限制 |
| 缓存行为 | 可被浏览器缓存、收藏 | 不被缓存 |
| 幂等性 | ✅ 幂等(多次请求结果一致) | ❌ 非幂等(可能重复创建资源) |
> [!warning] ⭐ A 选项为什么不选
> "GET 只获取、POST 只提交"是 RESTful **设计规范**,不是 HTTP 协议的硬性约束。实际上你可以 POST 一个纯查询接口,也可以用 GET 传大数据(虽然不规范)。考试中考查的是协议层的事实差异,而非设计哲学。
#### 延伸对比
```mermaid
flowchart LR
subgraph GET["GET 请求结构"]
G1["Method: GET"] --> G2["URL: /api?q=hello"]
G2 --> G3["Header: ..."]
G3 --> G4["Body: (empty)"]
end
subgraph POST["POST 请求结构"]
P1["Method: POST"] --> P2["URL: /api"]
P2 --> P3["Header: Content-Type"]
P3 --> P4["Body: key=value&foo=bar"]
end
style G4 fill:#f99
style P4 fill:#9f9
```
### 单5:HTTP 默认端口号
| 项目 | 内容 |
|------|------|
| ❌ 你的答案 | D(8080) |
| ✅ 正确答案 | **B(80)** |
> [!memory] 常用端口速记表
| 端口 | 协议 | 说明 |
|------|------|------|
| **80** | HTTP | 超文本传输协议标准端口 ✅ |
| **443** | HTTPS | HTTP over TLS/SSL |
| **21** | FTP | 文件传输协议 |
| **22** | SSH | 安全远程登录 |
| **3306** | MySQL | 数据库 |
| **5432** | PostgreSQL | 数据库 |
| **8080** | HTTP(替代) | 开发/代理常用端口,非标准 |
| **8443** | HTTPS(替代) | HTTPS 的常见替代端口 |
> [!question] 💡 思考
> 如果 Go 程序监听 80 端口报错 `permission denied`,怎么办?
>
> Linux/macOS 下 1024 以下的端口需要 root 权限。解决方法:① 用 `sudo` 运行;② 将 Nginx 设为反向代理转发到 8080;③ 使用 `authbind` 或 `setcap` 授予特定端口权限。这也是生产环境推荐 Nginx 前置的原因之一。
---
## 二、Gin 中间件核心机制
### 单13:Gin 中间件中继续执行后续逻辑的方法
| 项目 | 内容 |
|------|------|
| ❌ 你的答案 | B(`c.Abort()`) |
| ✅ 正确答案 | **A(`c.Next()`)** |
> [!error] 核心混淆点
> 你把 `c.Next()` 和 `c.Abort()` 的作用搞反了。这是 Gin 中间件最关键的两个方法,作用完全相反:
```go
// ✅ c.Next() — 暂停当前中间件,去执行后续所有中间件 + handler
// 执行完后回来继续运行 Next() 之后的代码
func loggingMiddleware(c *gin.Context) {
start := time.Now() // ① 前置:记录开始时间
c.Next() // ② 去执行后面的中间件和 handler...
// ③ 执行完回到这里
fmt.Println(time.Since(start)) // ④ 后置:输出耗时
}
// ❌ c.Abort() — 立即终止整个中间件链,后续的 middleware + handler 全都不执行
func authMiddleware(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.Abort() // ← 后面的所有内容都不再执行
return // ⚠️ Abort() 后建议紧跟 return
}
c.Next() // 认证通过,才放行
}
```
> [!summary] c.Next() vs c.Abort() 对比
| 方法 | 行为 | 类比 |
|------|------|------|
| `c.Next()` | 把请求交给下一个 handler,执行完回来继续 | **"过安检,过了继续走"** |
| `c.Abort()` | 直接拦截,不再往下传递 | **"拦下来,不能进"** |
| `c.AbortWithStatus(401)` | 终止 + 返回指定状态码 | **"不让你进,还给你个理由"** |
| `c.AbortWithError(500, err)` | 终止 + 返回状态码 + 附带错误信息 | **"不让你进,还告诉你出了什么错"** |
> [!abstract] c.Next() 底层实现
>
> ```go
> // gin/context.go 简化版
> func (c *Context) Next() {
> c.index++ // 移动到下一个 handler
> for ; c.index < int8(len(c.handlers)); c.index++ {
> if c.IsAborted() { // 如果前面有调用 Abort()
> return // 循环终止
> }
> c.handlers[c.index](c) // 执行当前 handler
> }
> }
> ```
>
> `c.Abort()` 只是设置了一个内部标志位 `isAborted = true`,当 `Next()` 检测到这个标志就停止遍历。
<details>
<summary>💡 深入理解:中间件的"洋葱模型"</summary>
Gin 中间件执行可以用经典的"洋葱模型"来理解:
```mermaid
flowchart TD
A["M1 前置处理"] --> B["M1.Next()"]
B --> C["M2 前置处理"]
C --> D["M2.Next()"]
D --> E["Handler 执行"]
E --> F["M2 后置处理"]
F --> G["M1 后置处理"]
style E fill:#FFD700
style A fill:#90EE90
style B fill:#90EE90
style C fill:#90EE90
style D fill:#90EE90
style F fill:#FFB6C1
style G fill:#FFB6C1
```
- **进入时**:M1 → M2 → Handler(按注册顺序)
- **退出时**:Handler → M2 → M1(按逆序,栈的行为)
- 这就是为什么日志中间件既能记录请求开始时间,也能记录响应耗时。
</details>
### 单12 & 单17:Gin 全局中间件注册与上下文共享
> [!note] 这两题你答对了 ✅,放在一起巩固:
**单12:全局注册中间件**
- 正确写法:`router.Use(middleware)`
- Gin 没有 `RegisterMiddleware`、`AttachMiddleware`、`Middleware` 这些方法
**单17:中间件之间共享数据**
- 正确方式:`c.Set(key, val)` / `c.Get(key)` 存入 Context 的 `Keys` map
- 绝对不要用全局变量!每个请求的 Context 是隔离的
```go
// 中间件1:设置用户信息
func authMiddleware(c *gin.Context) {
userID := parseToken(c.GetHeader("Authorization"))
c.Set("userID", userID) // 存入 Context
c.Set("role", getRole(userID))
c.Next()
}
// 中间件2/Handler:读取用户信息
func listUsers(c *gin.Context) {
userID := c.GetString("userID") // 安全获取
role := c.MustGet("role").(string) // 断言获取(确保 key 存在)
_ = role
}
```
### 单14 & 单15 & 单27 & 单28:Gin 参数解析与绑定
> [!note] 这四题你全部答对了 ✅,汇总关键 API:
| 场景 | Gin API | 示例 |
|------|---------|------|
| JSON body → Struct | `c.ShouldBindJSON(&obj)` | `POST /api/user` 接收 JSON |
| 路径参数 `:id` | `c.Param("id")` | `GET /user/:id` → `id="123"` |
| 查询参数 `?key=` | `c.Query("key")` | `GET /search?q=hello` |
| Form 表单 | `c.PostForm("field")` | `POST` x-www-form-urlencoded |
| 动态路由定义 | `router.GET("/user/:id", h)` | Gin 用 `:` 前缀,不是 `{}` |
| JSON 响应 | `c.JSON(200, gin.H{...})` | 返回标准 JSON |
| 未认证状态码 | 401 Unauthorized | 400 是请求格式错,401 是未认证 |
---
## 三、JWT 认证:解析步骤与上下文传递
### 单19:Golang 中解析 JWT 的正确步骤
| 项目 | 内容 |
|------|------|
| ❌ 你的答案 | C |
| ✅ 正确答案 | **A(解码 → 验证签名 → 检查声明)** |
> [!error] 你的误区
> 你可能凭直觉先"检查声明"(比如看看过期时间),但这样做是有安全隐患的:**在没有验证签名的前提下检查声明,等于信任了可能被篡改的数据。**
#### 正确的 JWT 解析三步法
```go
import (
"github.com/golang-jwt/jwt/v5"
)
func parseJWT(tokenString string) (*jwt.Claims, error) {
// Step 1: 解码(Decoding)— 把 Base64Url 编码的三个部分拆出来
// 不验签名,只看格式是否合法
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, fmt.Errorf("token format invalid: %w", err)
}
// Step 2: 验证签名(Signature Verification)— 用密钥重新计算 HMAC SHA256
// 确保 token 没有被第三方篡改
token, err = jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("your-secret-key"), nil // 必须用同一个密钥
})
if err != nil || !token.Valid {
return nil, fmt.Errorf("signature invalid: %w", err)
}
// Step 3: 检查声明(Claims Validation)— 此时才能相信数据
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid claims type")
}
// 检查过期时间
if time.Unix(int64(claims["exp"].(float64)), 0).Before(time.Now()) {
return nil, fmt.Errorf("token expired")
}
return &claims, nil
}
```
> [!warning] ⭐ 高频考点:为什么顺序不能乱?
>
> ```
> 错误的做法:先读 exp → 发现过期 → 告诉客户端"过期了"
> ↑ 黑客可以伪造 exp = 未来时间来绕过检查
>
> 正确的做法:先验签名 → 确认数据未被篡改 → 再读 exp → 确实是 2024-01-01
> ```
>
> 签名验证是**信任锚**——只有签名正确,里面的所有声明才可信。
#### JWT 结构速记
```mermaid
flowchart LR
A["JWT Token"] --> B["Header<br/>Base64Url(JSON)"]
A --> C["Payload / Claims<br/>Base64Url(JSON)"]
A --> D["Signature<br/>HMACSHA256<br/>base64UrlHeader.payload + secret"]
B -->|"alg + typ"| C
C -->|"签名算法对<br/>Header+Payload<br/>加上 Secret"| D
style D fill:#9f9
```
三部分用 `.` 连接:`xxxxx.yyyyy.zzzzz`
> [!memory] JWT 解析三步口诀
> **"先拆开看格式 → 再验身保真 → 最后读内容"**
>
> 解码 → 验签名 → 查声明
### 单10 & 单11:JWT 解决的核心痛点与最佳实践
> [!note] 这两题你答对了 ✅,补充完整理解:
**单10:JWT 解决的核心痛点**
- 传统 Session:服务器内存/Redis 存储会话,集群环境下需要同步共享
- JWT:**无状态**,服务端无需存储会话记录即可验证有效性
- 注意:JWT 并没有加密用户密码,字符串也比 SessionID **更长**(多了 Header + Payload)
**单11:JWT 验证后的最佳实践**
- 中间件就像"查票员",查验通过后通过 `c.Set()` 把用户信息存入当前请求的 Context
- 这样后续 Handler 能安全、隔离地拿到用户信息,不会互相干扰
```go
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
return
}
claims, err := parseJWT(tokenString)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return
}
// ✅ 最佳实践:存入 Context,供下游使用
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
```
---
## 四、Cookie 安全与 Session 管理
### 单7:防止 XSS 攻击的 Cookie 防护
| 项目 | 内容 |
|------|------|
| ✅ 正确答案 | **C(httpOnly 设置为 true)** |
> [!note] 此题你答对了 ✅,巩固关键知识点:
```go
// Set-Cookie 的关键属性
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionValue,
Path: "/",
MaxAge: 3600, // 有效期(秒),0 表示浏览器关闭即失效
HttpOnly: true, // ✅ 防 XSS —— JS 无法 document.cookie 读取
Secure: true, // ✅ 防窃听 —— 仅 HTTPS 传输
SameSite: http.SameSiteStrictMode, // ✅ 防 CSRF —— 严格同源策略
})
```
| 属性 | 作用 | 防御目标 |
|------|------|---------|
| `HttpOnly` | 禁止 JavaScript 读取 | XSS 窃取 Cookie |
| `Secure` | 仅 HTTPS 传输 | 中间人窃听 |
| `SameSite` | 限制跨站携带 Cookie | CSRF 攻击 |
| `Max-Age` | 控制有效期 | 延长攻击窗口 |
### 单9:Gin 与 Session
| 项目 | 内容 |
|------|------|
| ✅ 正确答案 | **C(Session 本质是后端存状态,前端只保存 SessionID)** |
> [!note] 此题你答对了 ✅。核心要点:
> - Gin 官方库**不自带** Session,需第三方中间件(如 `gorilla/sessions`)
> - Session **数据在服务端**,不在客户端(那是 Cookie 的做法)
> - 关闭浏览器不等于删除服务端 Session(取决于 `Max-Age` 或服务端清理策略)
### 单21:Session ID 的存储方式
| 项目 | 内容 |
|------|------|
| ✅ 正确答案 | **B(Set-Cookie)** |
> [!note] 此题你答对了 ✅。首次访问时服务端在 HTTP 响应的 Headers 里写入 `Set-Cookie: session_id=abc123`,浏览器自动保存,后续请求自动带上。
---
## 五、CORS 跨域问题专题
### 单22 ~ 单26:跨域全链路
> [!note] 这 5 道题你全部答对了 ✅,涵盖完整的跨域知识体系。整理如下:
**单22:生产环境跨域的标准解决方案**
- Nginx 反向代理统一域名 / CORS 中间件精确配置
**单23:为什么要用 Nginx 做反向代理**
- 静态文件分发、隐藏后端 IP、负载均衡、限流
**单24:OPTIONS 预检请求的本质**
- CORS Preflight:复杂请求前先问后端"我的自定义头你能接受吗?"
**单25:Vite Proxy 的开发阶段原理**
- 前端请求发给 Node.js 开发服务器 → 服务器转发给真实后端 → 原样返回
**单26:CORS 报错的根本原因**
- 浏览器的同源策略(Same-Origin Policy):防止恶意网站窃取敏感数据
> [!abstract] 同源策略判定
```
同源 = 协议相同 && 域名相同 && 端口相同
http://localhost:3000 → http://localhost:8080 ❌ 端口不同,跨域!
https://app.example.com → https://api.example.com ❌ 域名不同,跨域!
```
详见 `[[DEV/跨域问题调试]]` 的详细排查流程。
---
## 六、HTTP 状态码与语义
### 单3、单6、单29:HTTP 基础概念
| 题号 | 考查点 | ✅ 答案 | 解析 |
|------|--------|---------|------|
| 单3 | "未修改"状态码 | **304** | 浏览器发 `If-Modified-Since` / `If-None-Match`,服务端返回 304 让浏览器用本地缓存 |
| 单6 | 资源未被修改的状态码 | **304** | 和单3 考同一个知识点,换了一种问法 |
| 单29 | HTTP "无状态"的含义 | **每次请求独立,服务器不记住之前请求** | 这意味着需要 Cookie/Session/JWT 来维护用户状态 |
> [!memory] 与缓存相关的状态码
| 状态码 | 含义 | 触发条件 |
|--------|------|---------|
| **200** | 正常成功 | 首次请求 / 缓存失效 |
| **304** | 未修改 | 缓存命中,服务端通知浏览器直接用本地副本 |
| **206** | 部分内容 | Range 请求(视频断点续传等场景) |
---
## 七、其他知识点
### 单30:Git 临时保存工作进度
| 项目 | 内容 |
|------|------|
| ✅ 正确答案 | **C(git stash)** |
> [!note] 此题你答对了 ✅。
```bash
git stash # 保存当前工作区为"栈顶"
git stash pop # 恢复最近一次 stash(同时删除它)
git stash list # 查看所有 stash
git stash apply # 恢复但不删除(留在栈里)
```
---
## 错题自测表
> [!summary] 遮住右侧,自己检验是否真正理解了
| 题目 | 考查点 | ❌ 你的原答案 | ✅ 正确答案 | 关键概念 |
|------|--------|-------------|------------|---------|
| 单1 | GET vs POST 的本质区别 | A | **B** | URL 参数 vs Body,是事实差异而非设计规范 |
| 单5 | HTTP 默认端口 | D (8080) | **B (80)** | 80 是标准端口,8080 是常见替代 |
| 单13 | Gin 中间件继续执行的方法 | B (Abort) | **A (Next)** | Next() 继续执行,Abort() 中断链 |
| 单19 | JWT 解析正确顺序 | C | **A** | 解码 → 验签名 → 查声明(签名是信任锚) |
---
## 高频考点速览
> [!tip] 考前快速过一遍
| 类别 | 考点 | 一句话口诀 |
|------|------|-----------|
| GET vs POST | 数据位置不同(URL vs Body) | **"GET 挂在 URL 上,POST 装在 Body 里"** |
| HTTP 默认端口 | 80 | **"HTTP=80,HTTPS=443"** |
| HTTP 无状态 | 每次请求独立 | **"喝醉就忘,下次重来"** |
| 304 状态码 | 资源未修改,走缓存 | **"没变就用老的"** |
| Gin c.Next() | 继续执行后续中间件 + handler | **"过桥费,交了才能走"** |
| Gin c.Abort() | 中断中间件链 | **"栏杆放下,不让过"** |
| Gin 全局中间件 | router.Use(middleware) | Use,不是 Register / Attach |
| Gin 共享数据 | c.Set() / c.Get() | Context 里存取,别用全局变量 |
| Gin 路径参数 | `/user/:id` | Gin 用 `:` 而不是 `{}` |
| Gin JSON 绑定 | c.ShouldBindJSON(&obj) | 从请求体映射到 Struct |
| 401 状态码 | 未认证/缺少凭证 | **"401 = 你是谁?"** |
| Cookie httpOnly | 防 XSS 读取 | **"JS 看不见,只有浏览器带"** |
| Session 本质 | 后端存状态,前端拿 SessionID | **"服务器记账,前端拿号码牌"** |
| JWT 解析三步 | 解码 → 验签名 → 查声明 | **"先看皮相,再验身份证,最后读内容"** |
| CORS 根因 | 浏览器同源策略 | **"不同源的请求,浏览器不放心"** |
| OPTIONS 预检 | 复杂请求前的"询问" | **"我先问问你能不能干"** |
| Git stash | 临时保存工作进度 | **"先收起来,干完别的再放回"** |
| Nginx 前置 | 反向代理隐藏后端、负载均衡 | **"Nginx 挡在前面,后端躲在后面"** |
---
## 关联笔记
- [[hzh/EXAM]]
- [[GIN/3-middleware]] — 中间件完整机制(含 c.Next() / c.Abort() 详解)
- [[GIN/4-context-lifecycle]] — Context 生命周期与方法速查
- [[GIN/3-middleware/jwt-auth-qa]] — JWT 中间件安全性
- [[DEV/跨域问题调试]] — 跨域排查全流程