--- 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安全]] - 密码安全存储