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

19 KiB
Raw Blame History

tags, create time
tags create time
Go
后端开发
武科大
Week03
错题精讲
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 的 Keys map
  • 绝对不要用全局变量!每个请求的 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 管理

项目 内容
✅ 正确答案 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 挡在前面,后端躲在后面"

关联笔记