vault backup: 2026-04-25 16:06:25
This commit is contained in:
@@ -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)
|
||||
- 密码输入框使用 `<input type="password">`(防误看)
|
||||
- 登录失败后清空密码输入框
|
||||
- 设置合理的 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安全]] - 密码安全存储
|
||||
Reference in New Issue
Block a user