19 KiB
tags, create time
| tags | 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 才是本质区别
// 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 传大数据(虽然不规范)。考试中考查的是协议层的事实差异,而非设计哲学。
延伸对比
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 中间件最关键的两个方法,作用完全相反:
// ✅ 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() 底层实现
// 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()检测到这个标志就停止遍历。
💡 深入理解:中间件的"洋葱模型"
Gin 中间件执行可以用经典的"洋葱模型"来理解:
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(按逆序,栈的行为)
- 这就是为什么日志中间件既能记录请求开始时间,也能记录响应耗时。
单12 & 单17:Gin 全局中间件注册与上下文共享
[!note] 这两题你答对了 ✅,放在一起巩固:
单12:全局注册中间件
- 正确写法:
router.Use(middleware) - Gin 没有
RegisterMiddleware、AttachMiddleware、Middleware这些方法
单17:中间件之间共享数据
- 正确方式:
c.Set(key, val)/c.Get(key)存入 Context 的Keysmap - 绝对不要用全局变量!每个请求的 Context 是隔离的
// 中间件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 解析三步法
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 结构速记
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 能安全、隔离地拿到用户信息,不会互相干扰
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] 此题你答对了 ✅,巩固关键知识点:
// 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] 此题你答对了 ✅。
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/跨域问题调试 — 跨域排查全流程