6.7 KiB
6.7 KiB
tags, create time
| tags | create time | |||||||
|---|---|---|---|---|---|---|---|---|
|
2026-04-25 14:30 |
登录安全流程:前端与后端的加密协作
概述
登录是 Web 应用最敏感的操作之一。本文深入探讨前端和后端各自的安全职责,以及 HTTPS 协议下的标准登录认证流程。
核心问题:两端都需要加密吗?
不完全是。 更准确的说法是——传输层加密 + 密码侧哈希。
💡 想一想:如果前端把密码用 SHA-256 哈希后再发送,这样后端收到的是哈希值而不是明文密码,是不是更安全?
答案是否定的。因为哈希后的值本身就是凭证。攻击者截获这个哈希值后,可以直接重放给后端完成登录——哈希值被截获等于密码被截获。所以业界最主流的做法是:前端直接发送明文密码,由 HTTPS 保护传输安全。
登录全流程
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 地址 - 这是必须的,没有替代方案
第二层:密码存储加密(后端)
谁负责:后端
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 更适合密码哈希
第三层:认证凭据管理(前后端协作)
谁负责:前后端共同
// 后端签发 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)
}
// 前端登录后存储 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安全 - 密码安全存储