Files
cs-note/hzh/EXAM/Week03.md
T
2026-05-24 11:42:38 +08:00

527 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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/跨域问题调试]] — 跨域排查全流程