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