Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-15-implement-b-end-auth-system/design.md
huang 18f35f3ef4 feat: 完成B端认证系统和商户管理模块测试补全
主要变更:
- 新增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模式)
2026-01-15 18:15:17 +08:00

987 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设计文档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 错误处理流程
```
业务层错误
├─ 返回 AppErrorerrors.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