This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/金山办公作业/Week06/客户端密钥派生.md
T

138 lines
5.8 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, 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/登录安全流程]] - 登录安全流程总览,本文为其进阶篇