From f6ee499fa7805d96ce915a74d8f90a95e0db9127 Mon Sep 17 00:00:00 2001 From: wonder Date: Sat, 25 Apr 2026 16:06:25 +0800 Subject: [PATCH] vault backup: 2026-04-25 16:06:25 --- 金山办公作业/Week06/bcrypt成本因子.md | 107 +++++++++++++ 金山办公作业/Week06/客户端密钥派生.md | 137 +++++++++++++++++ 金山办公作业/Week06/登录安全流程.md | 211 ++++++++++++++++++++++++++ 金山办公作业/Week06/题目.md | 4 + 金山办公作业/Week06/验收任务清单.md | 68 +++++++++ 5 files changed, 527 insertions(+) create mode 100644 金山办公作业/Week06/bcrypt成本因子.md create mode 100644 金山办公作业/Week06/客户端密钥派生.md create mode 100644 金山办公作业/Week06/登录安全流程.md create mode 100644 金山办公作业/Week06/验收任务清单.md diff --git a/金山办公作业/Week06/bcrypt成本因子.md b/金山办公作业/Week06/bcrypt成本因子.md new file mode 100644 index 0000000..00f97b0 --- /dev/null +++ b/金山办公作业/Week06/bcrypt成本因子.md @@ -0,0 +1,107 @@ +--- +tags: [go, security, bcrypt, password-hashing, cost-factor] +create time: 2026-04-25 14:45 +--- + +# bcrypt 成本因子(Cost Factor)详解 + +## 概述 + +成本因子是 bcrypt 算法的核心参数,控制哈希计算的"难度"。它是平衡安全性和性能的关键旋钮。 + +## 什么是成本因子? + +bcrypt 不是做一次哈希就完事,而是通过 **多次迭代** 来增加暴力破解的计算成本。成本因子(cost)就是这个迭代次数的对数指数: + +``` +实际迭代次数 = 2^cost +``` + +所以在 Go 中: + +```go +import "golang.org/x/crypto/bcrypt" + +// bcrypt.DefaultCost = 10 → 2^10 = 1024 次迭代 +hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) +``` + +## 不同成本因子的对比 + +| 成本值 | 迭代次数 | 单次哈希耗时 | 适用场景 | +|--------|---------|------------|---------| +| 4 | 16 | ~1ms | 本地开发、测试 | +| 8 | 256 | ~50ms | 低流量内部系统 | +| **10** | 1,024 | ~100-300ms | **生产环境默认** | +| 12 | 4,096 | ~500ms | 高安全需求 | +| 14 | 16,384 | ~2s | 极低频登录场景 | + +> 💡 **想一想**:成本因子是不是越高越好?为什么? +> +> 答案:不是。成本越高,合法用户登录时服务器也要做同等次数的计算。过高的成本会导致登录延迟,影响用户体验,也给服务器带来不必要的负载。 + +## 如何选择合适的成本? + +### 选择原则 + +``` +目标:让单次哈希耗时在 100ms ~ 300ms 之间 +``` + +这个区间意味着: +- **对合法用户**:几乎无感,登录流畅 +- **对攻击者**:暴力破解速度被大幅压制(每秒只能尝试 3~10 次) + +### 调整方式 + +```go +// 生产环境手动调高 +hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), 12) + +// 也可以手动降低(仅开发环境) +hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.MinCost) // MinCost = 4 +``` + +## 成本因子的"升级陷阱" + +一个容易被忽视的问题:当硬件变快后,原来合适的成本因子会变得不够安全。 + +```go +// 2024 年的机器比 2018 年快很多 +// 当年 cost=10 是 200ms,现在可能只要 80ms + +// 解决方案:用户登录时检查并升级哈希 +func Login(username, password string) error { + user := db.GetUser(username) + bcrypt.CompareHashAndPassword(user.Hash, []byte(password)) + + // 检测当前哈希的成本是否偏低 + currentCost, _ := bcrypt.Cost(user.Hash) + if currentCost < bcrypt.DefaultCost { + // 用更高的成本重新哈希并保存 + newHash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + db.UpdateHash(username, newHash) + } +} +``` + +> 💡 **想一想**:为什么要在登录时"顺便"升级哈希,而不是批量扫描全量用户? +> +> 答案:惰性升级(lazy upgrade)避免一次性计算压力过大,且只升级活跃用户的哈希,不活跃用户的旧哈希影响较小。 + +## 最佳实践 + +✅ **推荐** +- 生产环境默认 `bcrypt.DefaultCost`(10) +- 定期评估并逐步提升成本因子 +- 登录时惰性升级低于最低成本的哈希 + +❌ **避免** +- 在生产环境使用 `< 10` 的成本 +- 为每个用户设置不同的成本(增加复杂度且收益有限) +- 为了追求极致安全而盲目调高到 14+ + +## 关联笔记 + +- [[金山办公作业/Week06/登录安全流程]] - 登录安全全流程 +- [[CS/DB/SQL安全]] - 密码安全存储 diff --git a/金山办公作业/Week06/客户端密钥派生.md b/金山办公作业/Week06/客户端密钥派生.md new file mode 100644 index 0000000..3570617 --- /dev/null +++ b/金山办公作业/Week06/客户端密钥派生.md @@ -0,0 +1,137 @@ +--- +tags: [go, frontend, security, pbkdf2, salt-challenge, client-side-derivation] +create time: 2026-04-25 14:45 +--- + +# 客户端密钥派生:盐值挑战方案详解 + +## 概述 + +本文为「登录安全流程」的进阶篇,讲解金融类高安全场景下采用的**客户端密钥派生**方案(盐值挑战 / Salt Challenge)。核心思路:前端在发送密码之前,先用后端下发的随机盐值做 PBKDF2 派生,让传输层上永远不出现原始密码。 + +## 核心类比:餐厅里的"一次性餐巾纸" + +想象你去一家**高安全级别的餐厅**点餐——点餐的过程就等同于"登录"。 + +### 普通餐厅(标准 HTTPS 登录) + +``` +你 → 把订单写在纸条上 → 服务员拿走 → 厨房照着做 +``` + +对应到代码:用户输入密码 → 浏览器直接发 `{username, password}` → 后端验证。 + +HTTPS 就像餐厅的监控摄像头 + 玻璃隔断,服务员看不到纸条内容。但如果有坏蛋偷偷装了针孔摄像头(**HTTPS 被攻破**),他拍到的就是你**完整的订单**,可以直接拿去厨房冒充你点餐。 + +### 高级餐厅(盐值挑战方案) + +现在来看完整的盐值挑战流程——这就是高级餐厅的做法。 + +--- + +## 完整流程拆解 + +### 第一步:前端先"要盐" + +```go +func (h *Handler) GetChallenge(w http.ResponseWriter, r http.Request) { + salt := generateRandomSalt(32) // 随机生成 32 位盐值 + cache.Set("challenge:"+r.RemoteAddr, salt, 1*time.Minute) // 存到缓存,1 分钟后自动清除 + json.NewEncoder(w).Encode(LoginChallenge{ + Salt: salt, + ExpireAt: time.Now().Add(time.Minute).Unix(), // 告诉前端什么时候过期 + }) +} +``` + +**生活类比:服务员递给你一张"一次性餐巾纸"** + +- 你刚落座(`r.RemoteAddr` = 你的桌号),服务员就递给你一张**随机印了乱码的餐巾纸**(`salt`) +- 这张纸**只有效 1 分钟**(`ExpireAt`),过期作废 +- 餐厅后台会记住:哪张纸给了哪桌,什么时候过期 + +| 代码 | 类比 | 为什么重要 | +|---|---|---| +| `generateRandomSalt(32)` | 餐巾纸上随机印的乱码 | 每次都不一样,两个不同桌的人拿到的纸完全不同 | +| `cache.Set(..., 1*time.Minute)` | 1 分钟后餐厅收回这张纸 | 过期后就算有人偷了这张纸也没用 | +| `"challenge:"+r.RemoteAddr` | 按桌号记录哪张纸给了谁 | 防止有人拿别人桌的纸来冒充 | + +--- + +### 第二步:前端拿到盐后"做记号" + +```go +// 前端拿到 salt = "a7xK9mZp..." 后: +// 用 PBKDF2 算法,把 用户密码 + 盐值 混合运算 +// 得到一个派生密钥,比如 "e4f8b2d1c9a3..." +// 把这个派生密钥发给后端登录 +``` + +**生活类比:你在餐巾纸上用"暗语"写订单** + +- 你在餐巾纸的乱码旁边,**用只有你和厨房能懂的暗语**写下你要点的菜(密码) +- 你不是直接写"红烧肉",而是写暗语"8 号食材 + 3 号做法" +- 厨房拿到纸条后,用同样的暗语表**反查**,知道你点了什么 + +**PBKDF2 在这里的角色:** 是一套**单向暗语转换规则**。 +- 你(前端)用这套规则把"红烧肉"变成"8 号食材 + 3 号做法" +- 厨房(后端)用同样的规则验证这个暗语对不对 +- **但反过来——有人只看到"8 号食材 + 3 号做法",猜不出原菜名是什么**(因为 PBKDF2 是单向的) + +--- + +### 第三步:后端"对暗语" + 撕纸 + +```go +func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { + // 1. 前端先用盐值做 PBKDF2 → 得到派生密钥 + // 2. 后端用同样的盐 + 数据库里的密码做 PBKDF2 → 对比两者是否一致 + // 3. 用完这张盐,立即作废(一次性使用) +} +``` + +**生活类比:厨房验证 + 用完即撕** + +- 厨房拿到餐巾纸上的暗语 +- 同时从自己的菜单上,用同样的规则算一遍 → 看暗语是否匹配 +- **算完这张纸直接撕掉**(盐值一次性使用),哪怕有人偷了这张已经撕掉的纸也没用 + +--- + +## 全程对比 + +``` +【普通登录 — 普通餐厅】 +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ 你(用户) │────▶│ 服务员 │────▶│ 厨房 │ +│ 密码:1234 │ │ (HTTPS) │ │ 验证 │ +└──────────┘ └──────────┘ └──────────┘ + ▲ + 如果有摄像头 + 拍到完整密码 ❌ + +【盐值挑战 — 高级餐厅】 +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ 你(用户) │────▶│ 盐值纸条 │────▶│ 厨房 │ +│ 先拿盐→ │ │ (1分钟 │ │ 对暗语 │ +│ 再做变换→ │ │ 一次性) │ │ 用完撕掉 │ +│ 发密钥 │────▶│ │ │ │ +└──────────┘ └──────────┘ └──────────┘ + ▲ + 如果有摄像头 + 只拍到"暗语" + "废纸" ✅ +``` + +--- + +## 安全性总结 + +> "即使 HTTPS 被绕过(极少见),攻击者也只能看到客户端派生的密钥,无法还原原始密码。" + +**翻译:** 就算餐厅的摄像头被拆了(HTTPS 被攻破),坏蛋拍到的也只是你写在餐巾纸上的"暗语"和"废纸"。没有那套 PBKDF2 转换规则和原始盐值,他**无法反推出你最初输入的密码**,更不可能在 1 分钟后再次使用。 + +--- + +## 关联笔记 + +- [[金山办公作业/Week06/登录安全流程]] - 登录安全流程总览,本文为其进阶篇 diff --git a/金山办公作业/Week06/登录安全流程.md b/金山办公作业/Week06/登录安全流程.md new file mode 100644 index 0000000..ce90fba --- /dev/null +++ b/金山办公作业/Week06/登录安全流程.md @@ -0,0 +1,211 @@ +--- +tags: [go, frontend, security, authentication, login, encryption, assignment] +create time: 2026-04-25 14:30 +--- + +# 登录安全流程:前端与后端的加密协作 + +## 概述 + +登录是 Web 应用最敏感的操作之一。本文深入探讨前端和后端各自的安全职责,以及 HTTPS 协议下的标准登录认证流程。 + +## 核心问题:两端都需要加密吗? + +**不完全是。** 更准确的说法是——**传输层加密 + 密码侧哈希**。 + +> 💡 **想一想**:如果前端把密码用 SHA-256 哈希后再发送,这样后端收到的是哈希值而不是明文密码,是不是更安全? + +答案是否定的。因为**哈希后的值本身就是凭证**。攻击者截获这个哈希值后,可以直接重放给后端完成登录——哈希值被截获等于密码被截获。所以业界最主流的做法是:**前端直接发送明文密码,由 HTTPS 保护传输安全**。 + +## 登录全流程 + +```mermaid +sequenceDiagram + participant U as 用户 + participant F as 前端(React) + participant S as 后端(Go) + participant DB as 数据库 + + U->>F: 输入用户名 + 密码 + F->>S: POST /api/login (HTTPS) + Note over S: 传输层 TLS 加密 + S->>DB: SELECT * FROM users WHERE username=? + DB-->>S: 用户记录 (hashed_password) + S->>S: bcrypt.Compare(password, stored_hash) + + alt 密码正确 + S->>S: 生成 JWT Token + S-->>F: {access_token, expires_in} + F->>F: 保存 Token (Memory/LocalStorage) + F-->>U: 跳转首页 ✓ + else 密码错误 + S-->>F: 401 Unauthorized + F-->>U: 提示"用户名或密码错误" + end +``` + +## 三层安全模型 + +登录安全分为三个层次,每层由不同方负责: + +### 第一层:传输加密(HTTPS/TLS) + +**谁负责**:服务器端配置,前端透明使用 + +``` +HTTP (明文) → HTTPS (TLS 加密) +password = "123456" → TLS 密文传输 +``` + +- 所有请求自动加密,中间人无法窃取 +- 前端只需使用 `https://` 前缀的 API 地址 +- 这是**必须的**,没有替代方案 + +### 第二层:密码存储加密(后端) + +**谁负责**:后端 + +```go +package auth + +import "golang.org/x/crypto/bcrypt" + +// 注册时:密码哈希后存入数据库 +func Register(username, password string) error { + hash, err := bcrypt.GenerateFromPassword( + []byte(password), bcrypt.DefaultCost, + ) + // 默认成本因子 = 10(详见 [[金山办公作业/Week06/bcrypt成本因子]]) + if err != nil { + return err + } + // 存入数据库的是 hash,不是 password + return db.InsertUser(username, string(hash)) +} + +// 登录时:比对输入的密码与存储的哈希 +func Login(username, password string) (string, error) { + user, err := db.GetUser(username) + if err != nil { + return "", ErrUserNotFound + } + if err := bcrypt.CompareHashAndPassword( + []byte(user.HashedPassword), []byte(password), + ); err != nil { + return "", ErrWrongPassword + } + // 验证通过 → 签发 Token + return generateJWT(user), nil +} +``` + +默认成本因子 = 10(详见 [[金山办公作业/Week06/bcrypt成本因子]]) +**为什么用 bcrypt?** +- 自动加盐,相同密码在不同用户哈希结果不同 +- 计算成本可调,抵御暴力破解 +- 比 MD5/SHA-256 更适合密码哈希 + +### 第三层:认证凭据管理(前后端协作) + +**谁负责**:前后端共同 + +```go +// 后端签发 JWT +func generateJWT(userID string, username string) string { + claims := jwt.MapClaims{ + "user_id": userID, + "username": username, + "exp": time.Now().Add(2 * time.Hour).Unix(), + "iat": time.Now().Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(secretKey) +} +``` + +```typescript +// 前端登录后存储 Token +async function handleLogin(username: string, password: string) { + const resp = await fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + + if (resp.ok) { + const data = await resp.json(); + // 方式1:Memory(更安全,刷新后失效) + sessionStorage.setItem('token', data.access_token); + // 方式2:LocalStorage(持久登录) + localStorage.setItem('token', data.access_token); + + router.push('/dashboard'); + } +} +``` + +## 前端是否需要额外加密? + +| 方案 | 前端做哈希? | 安全性 | 复杂度 | 推荐度 | +|------|------------|--------|--------|--------| +| **标准方案** ✅ | ❌ 不加密,直接发 | 高(HTTPS 保护) | 低 | ⭐⭐⭐⭐⭐ | +| 前端 SHA-256 | ✅ 哈希后发送 | 中(哈希值可重放) | 中 | ⭐⭐ | +| 客户端密钥派生 | ✅ PBKDF2/Argon2 | 更高 | 高 | ⭐⭐⭐ | + +**为什么标准方案最优?** + +``` +方案A(前端不加密): + 前端明文 → HTTPS加密传输 → 后端验证 bcrypt 哈希 + ✅ 中间人看不到密码 + +方案B(前端加 SHA-256): + 前端 SHA-256(password) → 密文传输 → 后端比对 SHA-256 + ❌ 中间人截获哈希值 → 直接重放登录(哈希=密码) +``` + +**结论**:前端额外哈希不仅不能提升安全性,反而引入了"重放攻击"风险。 + +## 进阶:高安全场景下的前端处理 + +对于金融类应用等高安全需求场景,可以采用客户端密钥派生方案——前端在发送密码之前,先用后端下发的随机盐值做 PBKDF2 派生,让传输层上永远不出现原始密码。 + +详细流程请见:[[金山办公作业/Week06/客户端密钥派生]] + +## 最佳实践清单 + +### 前端 + +✅ **推荐** +- 使用 HTTPS 地址调用 API +- 登录后 Token 存储在内存(sessionStorage) +- 密码输入框使用 ``(防误看) +- 登录失败后清空密码输入框 +- 设置合理的 Token 过期时间(2h ~ 24h) + +❌ **避免** +- 在 URL 参数中传递密码 +- 用 localStorage 存明文密码 +- 登录失败后暴露具体原因("用户名不存在" vs "密码错误")——统一提示"用户名或密码错误" +- 前端对密码做 SHA-256 哈希 + +### 后端 + +✅ **推荐** +- 使用 bcrypt/argon2 哈希存储密码 +- 统一返回模糊错误信息(防止用户名枚举) +- 添加登录频率限制(防暴力破解) +- JWT 设置短期过期 + Refresh Token 机制 +- 密钥通过环境变量管理,不写死代码 + +❌ **避免** +- 明文存储密码 +- 使用 MD5/SHA-256 哈希密码 +- 错误信息暴露过多细节 + +## 关联笔记 + +- [[金山办公作业/Week05/用户认证.md]] - Go 用户认证完整实现 +- [[金山办公作业/Week06/客户端密钥派生]] - 客户端密钥派生(盐值挑战)详解 +- [[CS/NET/HTTPS]] - HTTPS/TLS 原理 +- [[CS/DB/SQL安全]] - 密码安全存储 diff --git a/金山办公作业/Week06/题目.md b/金山办公作业/Week06/题目.md index b8ee457..191226f 100644 --- a/金山办公作业/Week06/题目.md +++ b/金山办公作业/Week06/题目.md @@ -1,5 +1,7 @@ # 项目概述 +> 验收任务清单:[[验收任务清单]] + 👋 本作业要求实现一个在线学习管理平台的部分内容,涵盖前端与后端两部分。前端基于 React + TypeScript 技术栈构建,后端采用 Koa 开发。系统实现了用户登录认证、工作台数据可视化、课程管理、学生管理及学习总结展示等功能,支持数据的增删改查等操作。 @@ -11,6 +13,8 @@ ![](https://cloud-pic.wpsgo.com/dnhJS0pUekpNTlBOUFpsSXJncHJpa3JEWkpaM1ZTLzg4Tys5NWcrZE1kR0h3anJiZ3NYZW9XODBpUWovZ2VjT2w1TnhRZW5jbU4yZ3BCWmEweHgveHY1eXhDaDliV3M2ZU1nMGFZa2s1SzJ6VWNiT1RLVHVkc0hCeHJ5VHczRDZCdFltKzE0M0NMSS9RZEJ4YXVHcjV1UG1IWVBxbkhHUEhVUXNaV2oxRnhRZnlXWEN6NmFlN2lKRU4yaUdGRloxODltOTJXNU5KRm1qSmRrQlFkSm9iYVdKRHc0ZjU3ajhaZTl3Y3hsYjI0L3paZ280YWVNM3NBeUFUdWdCTXR6S1hWNlhrbXJmWDlKVFlDRk9zcWUrSnc9PQ==/attach/object/C3G74ARGAAQHM?) - 实现用户登录表单(用户名 + 密码),调用后端接口进行认证。 + +**相关**:[[登录安全流程]] - 前端与后端的加密协作、登录全流程、最佳实践 - 未登录状态下访问其他页面自动跳转到登录页。 - 固定测试账号/密码:`admin` / `admin123` diff --git a/金山办公作业/Week06/验收任务清单.md b/金山办公作业/Week06/验收任务清单.md new file mode 100644 index 0000000..f5edc13 --- /dev/null +++ b/金山办公作业/Week06/验收任务清单.md @@ -0,0 +1,68 @@ +# 验收任务清单 + +> 来源:[[题目]] + +## 一、前端功能验收 + +### 1. 登录认证 +- [ ] 实现用户登录表单(用户名 + 密码),调用后端接口进行认证 +- [ ] 未登录状态下访问其他页面自动跳转到登录页 +- [ ] 固定测试账号/密码:`admin` / `admin123` + +### 2. 工作台(Dashboard) +- [ ] 展示 4 个统计卡片:课程总数、学生总数、发布率、活跃率 +- [ ] 柱状图:课程选课人数排行 +- [ ] 折线图:近 7 天学习活跃度(学习人数 + 学习时长) +- [ ] 饼图:学生状态分布(活跃 / 非活跃) +- [ ] 饼图:课程分类分布 + +### 3. 课程管理 +- [ ] 课程列表展示,支持分页(默认 10 条每页,可调整) +- [ ] 按课程名/讲师搜索、按状态和分类筛选 +- [ ] 选课人数列的服务端排序 +- [ ] 新增、编辑、删除课程 +- [ ] 课程发布/下架状态切换 +- [ ] 删除操作气泡确认框 + +### 4. 学生管理 +- [ ] 学生列表展示,支持分页(默认 10 条每页,可调整) +- [ ] 按姓名/学号搜索、按班级和状态筛选 +- [ ] 新增、编辑、删除学生 +- [ ] 支持多选课程 + +### 5. 学习总结 +- [ ] 从服务端获取 Markdown 格式的学习总结内容,前端解析渲染展示 +- [ ] 修改 `server/data/summary.md` 内容为真实的前端学习总结 + +## 二、技术要求验收 + +### 前端技术栈 +- [ ] Vite + React + TypeScript +- [ ] Ant Design + Tailwind CSS +- [ ] 未使用第三方脚手架(UmiJS、飞冰、Ant Design Pro 等) + +### 后端技术栈 +- [ ] Node.js + Koa +- [ ] @koa/router + better-sqlite3 + jsonwebtoken +- [ ] 完善个别缺失的后端代码 + +### 开发与生产配置 +- [ ] 前端监听 5173 端口,Vite 代理到 3000 +- [ ] 前端 `npm run build` 打包到 `client/dist` +- [ ] 生产环境中 Koa 托管静态资源,`http://localhost:3000` 可直接访问 + +## 三、作业提交验收 + +- [ ] 作业放置于 `week07/homework/course` 目录 +- [ ] `client` 目录包含 package.json(`npm run dev` / `npm run build`) +- [ ] `server` 目录包含后端代码 +- [ ] 视频要求: + - [ ] 时长控制在 5 分钟内 + - [ ] 展示学生管理的增删改查、工作台和学习总结界面 + - [ ] 核心技术点讲解或优化思考 + - [ ] 视频开头说明身份信息 + - [ ] 命名格式:`姓名_学号_在线学习后台.mp4` +- [ ] README.md 包含: + - [ ] 项目基本信息(姓名、学校、学号) + - [ ] 开发任务索引(任务清单) + - [ ] 核心技术实现说明