主要变更: - 新增B端认证系统(后台+H5):登录、登出、Token刷新、密码修改 - 完善商户管理和商户账号管理功能 - 补全单元测试(ShopService: 72.5%, ShopAccountService: 79.8%) - 新增集成测试(商户管理+商户账号管理) - 归档OpenSpec提案(add-shop-account-management, implement-b-end-auth-system) - 完善文档(使用指南、API文档、认证架构说明) 测试统计: - 13个测试套件,37个测试用例,100%通过率 - 平均覆盖率76.2%,达标 OpenSpec验证:通过(strict模式)
987 lines
31 KiB
Markdown
987 lines
31 KiB
Markdown
# 设计文档:B 端认证系统
|
||
|
||
**Change ID**: `implement-b-end-auth-system`
|
||
|
||
---
|
||
|
||
## 1. 架构概览
|
||
|
||
### 1.1 系统分层
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ HTTP 层(Fiber) │
|
||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||
│ │ Admin Auth │ │ H5 Auth │ │ Personal │ │
|
||
│ │ Handler │ │ Handler │ │ Customer │ │
|
||
│ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │
|
||
└─────────┼────────────────┼────────────────┼─────────────────┘
|
||
│ │ │
|
||
┌─────────▼────────────────▼────────────────▼─────────────────┐
|
||
│ 中间件层(Middleware) │
|
||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||
│ │ Admin Auth │ │ H5 Auth │ │ Personal │ │
|
||
│ │ Middleware │ │ Middleware │ │ Auth │ │
|
||
│ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │
|
||
└─────────┼────────────────┼────────────────┼─────────────────┘
|
||
│ │ │
|
||
│ │ │
|
||
┌─────────▼────────────────▼────────────────▼─────────────────┐
|
||
│ 业务逻辑层(Service) │
|
||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||
│ │ Auth │ │ Permission │ │ Personal │ │
|
||
│ │ Service │ │ Service │ │ Customer │ │
|
||
│ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │
|
||
└─────────┼────────────────┼────────────────┼─────────────────┘
|
||
│ │ │
|
||
┌─────────▼────────────────▼────────────────▼─────────────────┐
|
||
│ 数据访问层(Store) │
|
||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||
│ │ Account │ │ Role │ │ Personal │ │
|
||
│ │ Store │ │ Store │ │ Customer │ │
|
||
│ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │
|
||
└─────────┼────────────────┼────────────────┼─────────────────┘
|
||
│ │ │
|
||
┌─────────▼────────────────▼────────────────▼─────────────────┐
|
||
│ 数据存储层 │
|
||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||
│ │ PostgreSQL │ │ Redis │ │
|
||
│ │ (账号、角色、权限) │ │ (Token、缓存) │ │
|
||
│ └──────────────────────┘ └──────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.2 认证方式对比
|
||
|
||
| 端口 | 用户类型 | 认证方式 | Token 类型 | 存储方式 |
|
||
|------|---------|---------|-----------|----------|
|
||
| **Web 后台** | 超级管理员<br>平台用户<br>代理账号 | Bearer Token | Redis Token | Redis |
|
||
| **H5 端** | 代理账号<br>企业账号 | Bearer Token | Redis Token | Redis |
|
||
| **个人客户端** | 个人客户 | Bearer Token | JWT | 无状态(自签名) |
|
||
|
||
**设计理由**:
|
||
- **B 端(Web + H5)**:使用 Redis Token,支持立即登出和撤销
|
||
- **C 端(个人客户)**:使用 JWT,减轻服务器压力,适合高并发场景
|
||
|
||
---
|
||
|
||
## 2. 核心模块设计
|
||
|
||
### 2.1 Token 管理器(TokenManager)
|
||
|
||
#### 职责
|
||
- 生成 access token 和 refresh token
|
||
- 验证 token 有效性
|
||
- 刷新 access token
|
||
- 撤销 token
|
||
- 管理用户的所有 token
|
||
|
||
#### 接口设计
|
||
|
||
```go
|
||
package auth
|
||
|
||
type TokenManager struct {
|
||
rdb *redis.Client
|
||
accessTokenTTL time.Duration // 24 小时(可配置)
|
||
refreshTokenTTL time.Duration // 7 天(可配置)
|
||
}
|
||
|
||
type TokenInfo struct {
|
||
UserID uint `json:"user_id"`
|
||
UserType int `json:"user_type"`
|
||
ShopID uint `json:"shop_id,omitempty"`
|
||
EnterpriseID uint `json:"enterprise_id,omitempty"`
|
||
Username string `json:"username"`
|
||
LoginTime time.Time `json:"login_time"`
|
||
Device string `json:"device"` // web / h5 / mobile
|
||
IP string `json:"ip"`
|
||
}
|
||
|
||
// 生成 token 对
|
||
func (m *TokenManager) GenerateTokenPair(ctx context.Context, info *TokenInfo) (accessToken, refreshToken string, err error)
|
||
|
||
// 验证 access token
|
||
func (m *TokenManager) ValidateAccessToken(ctx context.Context, token string) (*TokenInfo, error)
|
||
|
||
// 验证 refresh token
|
||
func (m *TokenManager) ValidateRefreshToken(ctx context.Context, token string) (*TokenInfo, error)
|
||
|
||
// 刷新 access token
|
||
func (m *TokenManager) RefreshAccessToken(ctx context.Context, refreshToken string) (newAccessToken string, err error)
|
||
|
||
// 撤销单个 token
|
||
func (m *TokenManager) RevokeToken(ctx context.Context, token string) error
|
||
|
||
// 撤销用户的所有 token
|
||
func (m *TokenManager) RevokeAllUserTokens(ctx context.Context, userID uint) error
|
||
```
|
||
|
||
#### Redis 存储结构
|
||
|
||
```
|
||
# Access Token
|
||
Key: auth:token:{uuid}
|
||
Value: JSON(TokenInfo)
|
||
TTL: 24h
|
||
|
||
# Refresh Token
|
||
Key: auth:refresh:{uuid}
|
||
Value: JSON(TokenInfo)
|
||
TTL: 7d
|
||
|
||
# 用户 Token 列表(Set)
|
||
Key: auth:user:{userID}:tokens
|
||
Value: Set[access_token_uuid, refresh_token_uuid]
|
||
TTL: 7d
|
||
```
|
||
|
||
**示例**:
|
||
```
|
||
# Access Token
|
||
redis> GET auth:token:550e8400-e29b-41d4-a716-446655440000
|
||
{
|
||
"user_id": 123,
|
||
"user_type": 2,
|
||
"shop_id": 10,
|
||
"enterprise_id": 0,
|
||
"username": "admin",
|
||
"login_time": "2026-01-15T12:00:00Z",
|
||
"device": "web",
|
||
"ip": "192.168.1.1"
|
||
}
|
||
|
||
# 用户 Token 列表
|
||
redis> SMEMBERS auth:user:123:tokens
|
||
1) "550e8400-e29b-41d4-a716-446655440000" # access token
|
||
2) "660e8400-e29b-41d4-a716-446655440001" # refresh token
|
||
```
|
||
|
||
#### Token 生成流程
|
||
|
||
```
|
||
GenerateTokenPair()
|
||
├─ 1. 生成 access token UUID (uuid.New())
|
||
├─ 2. 生成 refresh token UUID (uuid.New())
|
||
├─ 3. 序列化 TokenInfo 为 JSON
|
||
├─ 4. 存储 access token 到 Redis (TTL: 24h)
|
||
├─ 5. 存储 refresh token 到 Redis (TTL: 7d)
|
||
├─ 6. 将两个 token 添加到用户 token 列表(Set)
|
||
└─ 7. 返回 access token 和 refresh token
|
||
```
|
||
|
||
#### Token 验证流程
|
||
|
||
```
|
||
ValidateAccessToken(token)
|
||
├─ 1. 从 Redis 查询 auth:token:{token}
|
||
├─ 2. 如果不存在或过期 → 返回 CodeInvalidToken
|
||
├─ 3. 反序列化 JSON 为 TokenInfo
|
||
├─ 4. 验证账号状态(可选,需查询数据库)
|
||
└─ 5. 返回 TokenInfo
|
||
```
|
||
|
||
#### Token 刷新流程
|
||
|
||
```
|
||
RefreshAccessToken(refreshToken)
|
||
├─ 1. 验证 refresh token (ValidateRefreshToken)
|
||
├─ 2. 撤销旧的 access token (RevokeToken)
|
||
├─ 3. 生成新的 access token UUID
|
||
├─ 4. 存储新 token 到 Redis (TTL: 24h)
|
||
├─ 5. 更新用户 token 列表(删除旧 access token,添加新)
|
||
└─ 6. 返回新 access token
|
||
```
|
||
|
||
#### Token 撤销流程
|
||
|
||
```
|
||
RevokeToken(token)
|
||
├─ 1. 删除 Redis key: auth:token:{token}
|
||
├─ 2. 从用户 token 列表中删除该 token
|
||
└─ 3. 返回成功
|
||
|
||
RevokeAllUserTokens(userID)
|
||
├─ 1. 获取用户 token 列表 (SMEMBERS auth:user:{userID}:tokens)
|
||
├─ 2. 批量删除所有 token (DEL auth:token:{uuid} ...)
|
||
├─ 3. 删除用户 token 列表 (DEL auth:user:{userID}:tokens)
|
||
└─ 4. 返回成功
|
||
```
|
||
|
||
---
|
||
|
||
### 2.2 认证服务(AuthService)
|
||
|
||
#### 职责
|
||
- 处理登录业务逻辑(验证密码、生成 token)
|
||
- 处理登出业务逻辑(撤销 token)
|
||
- 处理 token 刷新业务逻辑
|
||
- 处理密码修改业务逻辑
|
||
- 查询用户权限列表
|
||
|
||
#### 接口设计
|
||
|
||
```go
|
||
package auth
|
||
|
||
type Service struct {
|
||
accountStore AccountStore
|
||
roleStore RoleStore
|
||
permissionStore PermissionStore
|
||
tokenManager *auth.TokenManager
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// 登录
|
||
func (s *Service) Login(ctx context.Context, req *LoginRequest, clientIP string) (*LoginResponse, error)
|
||
|
||
// 登出
|
||
func (s *Service) Logout(ctx context.Context, token string) error
|
||
|
||
// 刷新 token
|
||
func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (newAccessToken string, error)
|
||
|
||
// 获取当前用户信息和权限
|
||
func (s *Service) GetCurrentUser(ctx context.Context, userID uint) (*model.Account, []string, error)
|
||
|
||
// 修改密码
|
||
func (s *Service) ChangePassword(ctx context.Context, userID uint, oldPassword, newPassword string) error
|
||
```
|
||
|
||
#### 登录流程设计
|
||
|
||
```
|
||
Login(username, password, device, clientIP)
|
||
├─ 1. 根据用户名查询账号 (accountStore.GetByUsername)
|
||
│ ├─ 如果不存在 → 返回 CodeInvalidCredentials ("用户名或密码错误")
|
||
│ └─ 获取账号信息(包含密码哈希、状态)
|
||
│
|
||
├─ 2. 验证密码 (bcrypt.CompareHashAndPassword)
|
||
│ ├─ 如果错误 → 返回 CodeInvalidCredentials
|
||
│ └─ 密码正确
|
||
│
|
||
├─ 3. 检查账号状态
|
||
│ ├─ status = 0 → 返回 CodeAccountDisabled ("账号已禁用")
|
||
│ └─ status = 1 → 继续
|
||
│
|
||
├─ 4. 构造 TokenInfo
|
||
│ ├─ UserID = account.ID
|
||
│ ├─ UserType = account.UserType
|
||
│ ├─ ShopID = account.ShopID
|
||
│ ├─ EnterpriseID = account.EnterpriseID
|
||
│ ├─ Username = account.Username
|
||
│ ├─ LoginTime = time.Now()
|
||
│ ├─ Device = device
|
||
│ └─ IP = clientIP
|
||
│
|
||
├─ 5. 生成 token 对 (tokenManager.GenerateTokenPair)
|
||
│ ├─ 生成 access token (UUID)
|
||
│ ├─ 生成 refresh token (UUID)
|
||
│ └─ 存储到 Redis
|
||
│
|
||
├─ 6. 查询用户权限列表 (permissionService.GetUserPermissions)
|
||
│ ├─ 查询用户的所有角色 (accountRoleStore.GetByAccountID)
|
||
│ ├─ 查询角色的所有权限 (rolePermissionStore.GetByRoleIDs)
|
||
│ └─ 返回权限编码列表 (["user:create", "user:update", ...])
|
||
│
|
||
├─ 7. 构造响应
|
||
│ ├─ AccessToken = access token
|
||
│ ├─ RefreshToken = refresh token
|
||
│ ├─ User = account(隐藏密码字段)
|
||
│ └─ Permissions = 权限列表
|
||
│
|
||
└─ 8. 返回 LoginResponse
|
||
```
|
||
|
||
**安全考虑**:
|
||
- ✅ 密码错误和用户名不存在返回相同错误消息,防止用户枚举攻击
|
||
- ✅ 密码使用 bcrypt 哈希,成本因子 = 10
|
||
- ✅ Token 使用 UUID v4(不可预测)
|
||
- ✅ 登录时记录 IP 和设备信息
|
||
|
||
#### 登出流程设计
|
||
|
||
```
|
||
Logout(token)
|
||
├─ 1. 验证 token (tokenManager.ValidateAccessToken)
|
||
│ ├─ 如果无效 → 返回 CodeInvalidToken
|
||
│ └─ 获取 TokenInfo
|
||
│
|
||
├─ 2. 撤销 access token (tokenManager.RevokeToken)
|
||
│ └─ 删除 Redis key: auth:token:{token}
|
||
│
|
||
├─ 3. 撤销 refresh token(可选)
|
||
│ ├─ 从用户 token 列表获取对应的 refresh token
|
||
│ └─ 删除 Redis key: auth:refresh:{refreshToken}
|
||
│
|
||
└─ 4. 返回成功
|
||
```
|
||
|
||
**设计选择**:
|
||
- ❓ **是否同时撤销 refresh token**?
|
||
- **方案 A**:只撤销 access token,保留 refresh token(允许继续刷新)
|
||
- **方案 B**:同时撤销 access token 和 refresh token(完全登出)
|
||
- **推荐**:方案 B(安全性优先,符合用户预期)
|
||
|
||
#### 密码修改流程设计
|
||
|
||
```
|
||
ChangePassword(userID, oldPassword, newPassword)
|
||
├─ 1. 查询账号 (accountStore.GetByID)
|
||
│ ├─ 如果不存在 → 返回 CodeNotFound
|
||
│ └─ 获取账号信息(包含密码哈希)
|
||
│
|
||
├─ 2. 验证旧密码 (bcrypt.CompareHashAndPassword)
|
||
│ ├─ 如果错误 → 返回 CodeInvalidOldPassword
|
||
│ └─ 密码正确
|
||
│
|
||
├─ 3. 验证新密码格式
|
||
│ ├─ 长度 8-32 位
|
||
│ ├─ 包含字母和数字
|
||
│ └─ 如果不符合 → 返回 CodeInvalidPassword
|
||
│
|
||
├─ 4. 哈希新密码 (bcrypt.GenerateFromPassword)
|
||
│ └─ cost = 10
|
||
│
|
||
├─ 5. 更新数据库 (accountStore.UpdatePassword)
|
||
│ └─ 更新 password 字段
|
||
│
|
||
├─ 6. 撤销所有旧 token (tokenManager.RevokeAllUserTokens)
|
||
│ ├─ 删除用户的所有 access token
|
||
│ ├─ 删除用户的所有 refresh token
|
||
│ └─ 强制用户重新登录
|
||
│
|
||
└─ 7. 返回成功
|
||
```
|
||
|
||
**安全考虑**:
|
||
- ✅ 修改密码后立即撤销所有旧 token,防止密码泄露后被利用
|
||
- ✅ 需要验证旧密码,防止未授权修改
|
||
- ✅ 新密码复杂度要求(后续可加强:特殊字符、大小写等)
|
||
|
||
---
|
||
|
||
### 2.3 认证中间件(Auth Middleware)
|
||
|
||
#### 职责
|
||
- 从请求中提取 token
|
||
- 验证 token 有效性
|
||
- 检查用户类型权限
|
||
- 将用户信息注入 context
|
||
|
||
#### 设计架构
|
||
|
||
**复用现有中间件**:`pkg/middleware/auth.go` 的 `Auth()` 函数
|
||
|
||
```go
|
||
// 通用认证中间件(已存在)
|
||
func Auth(config AuthConfig) fiber.Handler
|
||
```
|
||
|
||
**配置方式**:
|
||
```go
|
||
// 后台认证中间件
|
||
adminAuthMiddleware := middleware.Auth(middleware.AuthConfig{
|
||
TokenValidator: adminTokenValidator, // 自定义验证函数
|
||
SkipPaths: []string{"/api/admin/login", "/api/admin/refresh-token"},
|
||
})
|
||
|
||
// H5 认证中间件
|
||
h5AuthMiddleware := middleware.Auth(middleware.AuthConfig{
|
||
TokenValidator: h5TokenValidator, // 自定义验证函数
|
||
SkipPaths: []string{"/api/h5/login", "/api/h5/refresh-token"},
|
||
})
|
||
```
|
||
|
||
#### Token 验证器设计
|
||
|
||
**后台 Token 验证器**:
|
||
```go
|
||
adminTokenValidator := func(token string) (*middleware.UserContextInfo, error) {
|
||
// 1. 验证 token
|
||
tokenInfo, err := tokenManager.ValidateAccessToken(ctx, token)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 2. 检查用户类型(后台只允许平台用户和代理账号)
|
||
allowedTypes := []int{
|
||
constants.UserTypeSuperAdmin, // 超级管理员
|
||
constants.UserTypePlatform, // 平台用户
|
||
constants.UserTypeAgent, // 代理账号
|
||
}
|
||
if !contains(allowedTypes, tokenInfo.UserType) {
|
||
return nil, errors.New(errors.CodeForbidden, "无权访问后台")
|
||
}
|
||
|
||
// 3. 返回用户上下文信息
|
||
return &middleware.UserContextInfo{
|
||
UserID: tokenInfo.UserID,
|
||
UserType: tokenInfo.UserType,
|
||
ShopID: tokenInfo.ShopID,
|
||
EnterpriseID: tokenInfo.EnterpriseID,
|
||
}, nil
|
||
}
|
||
```
|
||
|
||
**H5 Token 验证器**:
|
||
```go
|
||
h5TokenValidator := func(token string) (*middleware.UserContextInfo, error) {
|
||
// 1. 验证 token
|
||
tokenInfo, err := tokenManager.ValidateAccessToken(ctx, token)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 2. 检查用户类型(H5 只允许代理账号和企业账号)
|
||
allowedTypes := []int{
|
||
constants.UserTypeAgent, // 代理账号
|
||
constants.UserTypeEnterprise, // 企业账号
|
||
}
|
||
if !contains(allowedTypes, tokenInfo.UserType) {
|
||
return nil, errors.New(errors.CodeForbidden, "无权访问 H5 端")
|
||
}
|
||
|
||
// 3. 返回用户上下文信息
|
||
return &middleware.UserContextInfo{
|
||
UserID: tokenInfo.UserID,
|
||
UserType: tokenInfo.UserType,
|
||
ShopID: tokenInfo.ShopID,
|
||
EnterpriseID: tokenInfo.EnterpriseID,
|
||
}, nil
|
||
}
|
||
```
|
||
|
||
#### 中间件执行流程
|
||
|
||
```
|
||
HTTP 请求
|
||
│
|
||
├─ 1. Auth 中间件(pkg/middleware/auth.go)
|
||
│ ├─ 检查路径是否在 SkipPaths 中
|
||
│ │ ├─ 是 → 跳过认证,执行下一个中间件
|
||
│ │ └─ 否 → 继续认证流程
|
||
│ │
|
||
│ ├─ 提取 token(从 Authorization header)
|
||
│ │ ├─ 如果缺失 → 返回 CodeMissingToken (401)
|
||
│ │ └─ 提取 "Bearer {token}"
|
||
│ │
|
||
│ ├─ 调用 TokenValidator 函数
|
||
│ │ ├─ 验证 token(查询 Redis)
|
||
│ │ ├─ 检查用户类型权限
|
||
│ │ └─ 返回 UserContextInfo
|
||
│ │
|
||
│ ├─ 将用户信息注入 context
|
||
│ │ ├─ c.Locals(ContextKeyUserID, userInfo.UserID)
|
||
│ │ ├─ c.Locals(ContextKeyUserType, userInfo.UserType)
|
||
│ │ ├─ c.Locals(ContextKeyShopID, userInfo.ShopID)
|
||
│ │ ├─ c.Locals(ContextKeyEnterpriseID, userInfo.EnterpriseID)
|
||
│ │ └─ c.SetUserContext(ctx) // 用于 GORM 数据权限过滤
|
||
│ │
|
||
│ └─ 执行下一个中间件 (c.Next())
|
||
│
|
||
├─ 2. Permission 中间件(可选,pkg/middleware/permission.go)
|
||
│ ├─ 从 context 获取 userID
|
||
│ ├─ 检查权限码(如 "user:create")
|
||
│ └─ 如果无权限 → 返回 CodeForbidden (403)
|
||
│
|
||
└─ 3. 业务处理器(Handler)
|
||
├─ 从 context 获取用户信息
|
||
└─ 执行业务逻辑
|
||
```
|
||
|
||
---
|
||
|
||
### 2.4 路由设计
|
||
|
||
#### 后台路由(/api/admin)
|
||
|
||
```go
|
||
// 公开路由(无需认证)
|
||
public := api.Group("/admin")
|
||
public.Post("/login", authHandler.Login) // 登录
|
||
public.Post("/refresh-token", authHandler.RefreshToken) // 刷新 token
|
||
|
||
// 受保护路由(需要认证)
|
||
protected := api.Group("/admin")
|
||
protected.Use(adminAuthMiddleware) // 应用认证中间件
|
||
protected.Post("/logout", authHandler.Logout) // 登出
|
||
protected.Get("/me", authHandler.GetMe) // 获取当前用户
|
||
protected.Put("/password", authHandler.ChangePassword) // 修改密码
|
||
|
||
// 其他受保护路由(业务模块)
|
||
protected.Get("/accounts", accountHandler.List) // 账号管理
|
||
protected.Get("/roles", roleHandler.List) // 角色管理
|
||
protected.Get("/permissions", permissionHandler.List) // 权限管理
|
||
// ...
|
||
```
|
||
|
||
#### H5 路由(/api/h5)
|
||
|
||
```go
|
||
// 公开路由(无需认证)
|
||
public := api.Group("/h5")
|
||
public.Post("/login", authHandler.Login) // 登录
|
||
public.Post("/refresh-token", authHandler.RefreshToken) // 刷新 token
|
||
|
||
// 受保护路由(需要认证)
|
||
protected := api.Group("/h5")
|
||
protected.Use(h5AuthMiddleware) // 应用认证中间件
|
||
protected.Post("/logout", authHandler.Logout) // 登出
|
||
protected.Get("/me", authHandler.GetMe) // 获取当前用户
|
||
protected.Put("/password", authHandler.ChangePassword) // 修改密码
|
||
|
||
// H5 业务路由
|
||
protected.Get("/shops", shopHandler.List) // 店铺列表
|
||
protected.Get("/enterprises", enterpriseHandler.List) // 企业列表
|
||
// ...
|
||
```
|
||
|
||
#### 个人客户路由(/api/c)
|
||
|
||
```go
|
||
// 公开路由(无需认证)
|
||
public := api.Group("/c/v1")
|
||
public.Post("/login/send-code", personalCustomerHandler.SendCode) // 发送验证码
|
||
public.Post("/login", personalCustomerHandler.Login) // 登录
|
||
|
||
// 受保护路由(需要认证)
|
||
protected := api.Group("/c/v1")
|
||
protected.Use(personalAuthMiddleware) // 应用个人客户认证中间件
|
||
protected.Get("/profile", personalCustomerHandler.GetProfile) // 获取个人资料
|
||
protected.Put("/profile", personalCustomerHandler.UpdateProfile) // 更新个人资料
|
||
// ...
|
||
```
|
||
|
||
**路由层级关系**:
|
||
```
|
||
/api
|
||
├── /admin (后台,adminAuthMiddleware)
|
||
│ ├── /login (公开)
|
||
│ ├── /logout (受保护)
|
||
│ └── ...
|
||
├── /h5 (H5 端,h5AuthMiddleware)
|
||
│ ├── /login (公开)
|
||
│ ├── /logout (受保护)
|
||
│ └── ...
|
||
└── /c/v1 (个人客户,personalAuthMiddleware)
|
||
├── /login (公开)
|
||
├── /profile (受保护)
|
||
└── ...
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 数据模型设计
|
||
|
||
### 3.1 DTO 设计
|
||
|
||
#### 登录请求(LoginRequest)
|
||
|
||
```go
|
||
type LoginRequest struct {
|
||
Username string `json:"username" validate:"required" description:"用户名或手机号"`
|
||
Password string `json:"password" validate:"required" description:"密码"`
|
||
Device string `json:"device" validate:"omitempty,oneof=web h5 mobile" description:"设备类型"`
|
||
}
|
||
```
|
||
|
||
#### 登录响应(LoginResponse)
|
||
|
||
```go
|
||
type LoginResponse struct {
|
||
AccessToken string `json:"access_token" description:"访问令牌"`
|
||
RefreshToken string `json:"refresh_token" description:"刷新令牌"`
|
||
ExpiresIn int64 `json:"expires_in" description:"访问令牌过期时间(秒)"`
|
||
User UserInfo `json:"user" description:"用户信息"`
|
||
Permissions []string `json:"permissions" description:"权限列表"`
|
||
}
|
||
|
||
type UserInfo struct {
|
||
ID uint `json:"id"`
|
||
Username string `json:"username"`
|
||
Phone string `json:"phone"`
|
||
UserType int `json:"user_type"`
|
||
UserTypeName string `json:"user_type_name"` // "超级管理员" / "平台用户" / ...
|
||
ShopID uint `json:"shop_id,omitempty"`
|
||
ShopName string `json:"shop_name,omitempty"`
|
||
EnterpriseID uint `json:"enterprise_id,omitempty"`
|
||
EnterpriseName string `json:"enterprise_name,omitempty"`
|
||
}
|
||
```
|
||
|
||
#### 刷新 Token 请求(RefreshTokenRequest)
|
||
|
||
```go
|
||
type RefreshTokenRequest struct {
|
||
RefreshToken string `json:"refresh_token" validate:"required" description:"刷新令牌"`
|
||
}
|
||
```
|
||
|
||
#### 刷新 Token 响应(RefreshTokenResponse)
|
||
|
||
```go
|
||
type RefreshTokenResponse struct {
|
||
AccessToken string `json:"access_token" description:"新的访问令牌"`
|
||
ExpiresIn int64 `json:"expires_in" description:"过期时间(秒)"`
|
||
}
|
||
```
|
||
|
||
#### 修改密码请求(ChangePasswordRequest)
|
||
|
||
```go
|
||
type ChangePasswordRequest struct {
|
||
OldPassword string `json:"old_password" validate:"required" description:"旧密码"`
|
||
NewPassword string `json:"new_password" validate:"required,min=8,max=32" description:"新密码(8-32位)"`
|
||
}
|
||
```
|
||
|
||
### 3.2 统一响应格式
|
||
|
||
所有 API 响应使用 `pkg/response` 的统一格式:
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"msg": "成功",
|
||
"data": {
|
||
"access_token": "550e8400-e29b-41d4-a716-446655440000",
|
||
"refresh_token": "660e8400-e29b-41d4-a716-446655440001",
|
||
"expires_in": 86400,
|
||
"user": {
|
||
"id": 123,
|
||
"username": "admin",
|
||
"phone": "13800000000",
|
||
"user_type": 2,
|
||
"user_type_name": "平台用户"
|
||
},
|
||
"permissions": ["user:create", "user:update", "user:delete"]
|
||
},
|
||
"timestamp": "2026-01-15T12:00:00Z"
|
||
}
|
||
```
|
||
|
||
**错误响应**:
|
||
```json
|
||
{
|
||
"code": 1010,
|
||
"msg": "用户名或密码错误",
|
||
"data": null,
|
||
"timestamp": "2026-01-15T12:00:00Z"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 安全设计
|
||
|
||
### 4.1 密码安全
|
||
|
||
| 机制 | 实现方式 | 说明 |
|
||
|------|---------|------|
|
||
| **密码哈希** | bcrypt (cost=10) | 慢哈希算法,防暴力破解 |
|
||
| **密码复杂度** | 8-32 位,字母+数字 | Validator 验证 |
|
||
| **密码存储** | 不返回给客户端 | `json:"-"` 标签 |
|
||
| **密码传输** | HTTPS 加密 | 生产环境强制 HTTPS |
|
||
|
||
### 4.2 Token 安全
|
||
|
||
| 机制 | 实现方式 | 说明 |
|
||
|------|---------|------|
|
||
| **Token 生成** | UUID v4 | 不可预测,128 位随机 |
|
||
| **Token 过期** | 24 小时(access)<br>7 天(refresh) | 配置化 |
|
||
| **Token 撤销** | Redis 删除 key | 支持立即登出 |
|
||
| **Token 绑定** | 记录 IP、设备 | 便于审计(后续可加强验证) |
|
||
|
||
### 4.3 防暴力破解
|
||
|
||
| 机制 | 实现方式 | 说明 |
|
||
|------|---------|------|
|
||
| **限流** | 集成 `pkg/middleware/ratelimit.go` | 同一 IP 每分钟最多 10 次登录尝试 |
|
||
| **错误消息** | 统一返回"用户名或密码错误" | 防止用户枚举攻击 |
|
||
| **账号锁定** | 后续迭代 | 5 次失败锁定 15 分钟 |
|
||
|
||
### 4.4 HTTPS 强制
|
||
|
||
**生产环境**:
|
||
- 配置 Fiber HTTPS
|
||
- 使用 Let's Encrypt 自动签发证书
|
||
- 重定向 HTTP → HTTPS
|
||
|
||
**开发环境**:
|
||
- 允许 HTTP
|
||
- 使用自签名证书测试
|
||
|
||
---
|
||
|
||
## 5. 性能优化
|
||
|
||
### 5.1 Redis 连接池
|
||
|
||
```go
|
||
redis.Options{
|
||
Addr: "localhost:6379",
|
||
PoolSize: 100, // 连接池大小
|
||
MinIdleConns: 10, // 最小空闲连接
|
||
MaxRetries: 3, // 重试次数
|
||
DialTimeout: 5 * time.Second,
|
||
ReadTimeout: 3 * time.Second,
|
||
WriteTimeout: 3 * time.Second,
|
||
}
|
||
```
|
||
|
||
### 5.2 Token 验证缓存
|
||
|
||
**优化策略**:
|
||
- ✅ Redis 查询已经很快(< 5ms)
|
||
- ❌ 不再添加本地缓存(避免分布式一致性问题)
|
||
- ✅ 使用 Redis Pipeline 批量操作(撤销多个 token)
|
||
|
||
### 5.3 权限查询优化
|
||
|
||
**问题**:每次登录都查询用户权限,涉及多表 JOIN
|
||
|
||
**优化方案**:
|
||
1. 查询用户的所有角色(`account_role` 表)
|
||
2. 批量查询角色的权限(`role_permission` 表,使用 `IN` 查询)
|
||
3. 去重权限编码
|
||
4. 缓存到 Redis(可选,5 分钟 TTL)
|
||
|
||
**代码示例**:
|
||
```go
|
||
// 1. 查询用户角色
|
||
roleIDs, err := accountRoleStore.GetRoleIDsByAccountID(ctx, userID)
|
||
|
||
// 2. 批量查询权限
|
||
permissions, err := permissionStore.GetByRoleIDs(ctx, roleIDs)
|
||
|
||
// 3. 提取权限编码
|
||
permCodes := make([]string, 0, len(permissions))
|
||
for _, perm := range permissions {
|
||
permCodes = append(permCodes, perm.PermCode)
|
||
}
|
||
|
||
return permCodes, nil
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 错误处理
|
||
|
||
### 6.1 错误码扩展
|
||
|
||
```go
|
||
// pkg/errors/codes.go
|
||
|
||
// 认证相关错误码
|
||
CodeMissingToken = 1002 // 缺失认证令牌
|
||
CodeInvalidToken = 1003 // 无效或过期的令牌
|
||
CodeUnauthorized = 1004 // 未授权
|
||
CodeForbidden = 1005 // 禁止访问
|
||
|
||
// 登录相关错误码(新增)
|
||
CodeInvalidCredentials = 1010 // 用户名或密码错误
|
||
CodeAccountDisabled = 1011 // 账号已禁用
|
||
CodeAccountLocked = 1012 // 账号已锁定
|
||
CodePasswordExpired = 1013 // 密码已过期
|
||
CodeInvalidOldPassword = 1014 // 旧密码错误
|
||
CodeInvalidPassword = 1015 // 密码格式不正确(已存在)
|
||
CodePasswordTooWeak = 1016 // 密码强度不足(已存在)
|
||
```
|
||
|
||
### 6.2 错误处理流程
|
||
|
||
```
|
||
业务层错误
|
||
│
|
||
├─ 返回 AppError(errors.New(code, message))
|
||
│
|
||
↓
|
||
Handler 层接收错误
|
||
│
|
||
├─ 直接返回 error(由全局 ErrorHandler 处理)
|
||
│
|
||
↓
|
||
全局 ErrorHandler
|
||
│
|
||
├─ 提取错误码和消息
|
||
├─ 生成统一 JSON 响应
|
||
├─ 设置 HTTP 状态码
|
||
├─ 记录日志
|
||
└─ 返回给客户端
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 监控和日志
|
||
|
||
### 7.1 日志记录
|
||
|
||
**登录成功**:
|
||
```go
|
||
logger.Info("用户登录成功",
|
||
zap.Uint("user_id", userID),
|
||
zap.String("username", username),
|
||
zap.String("device", device),
|
||
zap.String("ip", clientIP),
|
||
)
|
||
```
|
||
|
||
**登录失败**:
|
||
```go
|
||
logger.Warn("用户登录失败",
|
||
zap.String("username", username),
|
||
zap.String("reason", "密码错误"),
|
||
zap.String("ip", clientIP),
|
||
)
|
||
```
|
||
|
||
**Token 验证失败**:
|
||
```go
|
||
logger.Warn("Token 验证失败",
|
||
zap.String("token", token[:10]+"..."), // 只记录前 10 位
|
||
zap.String("reason", "已过期"),
|
||
zap.String("ip", clientIP),
|
||
)
|
||
```
|
||
|
||
### 7.2 监控指标
|
||
|
||
**关键指标**:
|
||
- 登录成功率
|
||
- 登录失败率(按原因分类)
|
||
- Token 验证耗时(P50、P95、P99)
|
||
- Redis 连接错误次数
|
||
- 并发登录数
|
||
|
||
**告警规则**:
|
||
- 登录失败率 > 30%(可能是暴力破解)
|
||
- Token 验证耗时 P95 > 10ms
|
||
- Redis 连接错误次数 > 10 次/分钟
|
||
|
||
---
|
||
|
||
## 8. 测试策略
|
||
|
||
### 8.1 单元测试
|
||
|
||
**覆盖模块**:
|
||
- Token 管理器(`pkg/auth/token_test.go`)
|
||
- 认证服务(`internal/service/auth/service_test.go`)
|
||
|
||
**测试方法**:
|
||
- 使用 Mock 对象(`github.com/stretchr/testify/mock`)
|
||
- Mock `AccountStore`、`Redis`
|
||
- 覆盖率目标:≥ 90%
|
||
|
||
### 8.2 集成测试
|
||
|
||
**覆盖接口**:
|
||
- 后台登录、登出、刷新 token
|
||
- H5 登录、登出、刷新 token
|
||
- 认证中间件行为
|
||
|
||
**测试环境**:
|
||
- 使用 `testcontainers` 启动真实 PostgreSQL 和 Redis
|
||
- 测试完整的请求-响应流程
|
||
- 验证 Redis 数据存储正确
|
||
|
||
### 8.3 性能测试
|
||
|
||
**测试场景**:
|
||
- Token 验证性能(目标:< 5ms)
|
||
- 登录性能(目标:< 200ms)
|
||
- 并发登录(1000 并发)
|
||
|
||
**工具**:
|
||
- Go Benchmark(`go test -bench`)
|
||
- Apache Bench(`ab`)
|
||
- Vegeta(负载测试)
|
||
|
||
---
|
||
|
||
## 9. 部署和运维
|
||
|
||
### 9.1 环境配置
|
||
|
||
**开发环境**(`configs/config.dev.yaml`):
|
||
```yaml
|
||
jwt:
|
||
secret_key: "dev-secret-key-32-characters-long"
|
||
access_token_ttl: 24h
|
||
refresh_token_ttl: 168h # 7 days
|
||
|
||
redis:
|
||
address: "localhost:6379"
|
||
password: ""
|
||
db: 0
|
||
```
|
||
|
||
**生产环境**(`configs/config.prod.yaml`):
|
||
```yaml
|
||
jwt:
|
||
secret_key: "${JWT_SECRET_KEY}" # 从环境变量读取
|
||
access_token_ttl: 24h
|
||
refresh_token_ttl: 168h
|
||
|
||
redis:
|
||
address: "${REDIS_ADDR}"
|
||
password: "${REDIS_PASSWORD}"
|
||
db: 0
|
||
```
|
||
|
||
### 9.2 Redis 高可用
|
||
|
||
**生产环境推荐**:
|
||
- 使用 Redis 哨兵模式(Sentinel)或集群模式(Cluster)
|
||
- 配置主从复制
|
||
- 定期备份(RDB + AOF)
|
||
|
||
**配置示例**:
|
||
```yaml
|
||
redis:
|
||
mode: sentinel # sentinel / cluster / standalone
|
||
master_name: "mymaster"
|
||
sentinel_addrs:
|
||
- "sentinel1:26379"
|
||
- "sentinel2:26379"
|
||
- "sentinel3:26379"
|
||
```
|
||
|
||
### 9.3 健康检查
|
||
|
||
**API 健康检查**:
|
||
```
|
||
GET /health
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"redis": "connected",
|
||
"postgres": "connected"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 后续优化方向
|
||
|
||
1. **Token Rotation**:刷新 token 时同时更新 refresh token
|
||
2. **设备指纹**:绑定 token 到设备,防止 token 被盗用
|
||
3. **IP 白名单**:限制特定 IP 访问
|
||
4. **账号锁定策略**:登录失败 5 次锁定 15 分钟
|
||
5. **两步验证(2FA)**:短信验证码、TOTP
|
||
6. **单点登录(SSO)**:统一登录入口
|
||
7. **审计日志**:记录登录、权限变更等操作
|
||
8. **密码策略**:强制定期修改、密码历史记录
|
||
9. **OAuth 第三方登录**:微信企业登录、钉钉登录
|
||
10. **实时踢人**:管理员强制下线用户
|
||
|
||
---
|
||
|
||
**文档状态**: 待审批
|
||
**创建时间**: 2026-01-15
|
||
**最后更新**: 2026-01-15
|