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

31 KiB
Raw Blame History

设计文档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 后台 超级管理员
平台用户
代理账号
Bearer Token Redis Token Redis
H5 端 代理账号
企业账号
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

接口设计

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 刷新业务逻辑
  • 处理密码修改业务逻辑
  • 查询用户权限列表

接口设计

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.goAuth() 函数

// 通用认证中间件(已存在)
func Auth(config AuthConfig) fiber.Handler

配置方式

// 后台认证中间件
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 验证器

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 验证器

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

// 公开路由(无需认证)
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

// 公开路由(无需认证)
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

// 公开路由(无需认证)
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

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

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

type RefreshTokenRequest struct {
	RefreshToken string `json:"refresh_token" validate:"required" description:"刷新令牌"`
}

刷新 Token 响应RefreshTokenResponse

type RefreshTokenResponse struct {
	AccessToken string `json:"access_token" description:"新的访问令牌"`
	ExpiresIn   int64  `json:"expires_in" description:"过期时间(秒)"`
}

修改密码请求ChangePasswordRequest

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 的统一格式:

{
  "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"
}

错误响应

{
  "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
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 连接池

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

代码示例

// 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 错误码扩展

// 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 日志记录

登录成功

logger.Info("用户登录成功",
	zap.Uint("user_id", userID),
	zap.String("username", username),
	zap.String("device", device),
	zap.String("ip", clientIP),
)

登录失败

logger.Warn("用户登录失败",
	zap.String("username", username),
	zap.String("reason", "密码错误"),
	zap.String("ip", clientIP),
)

Token 验证失败

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 AccountStoreRedis
  • 覆盖率目标:≥ 90%

8.2 集成测试

覆盖接口

  • 后台登录、登出、刷新 token
  • H5 登录、登出、刷新 token
  • 认证中间件行为

测试环境

  • 使用 testcontainers 启动真实 PostgreSQL 和 Redis
  • 测试完整的请求-响应流程
  • 验证 Redis 数据存储正确

8.3 性能测试

测试场景

  • Token 验证性能(目标:< 5ms
  • 登录性能(目标:< 200ms
  • 并发登录1000 并发)

工具

  • Go Benchmarkgo test -bench
  • Apache Benchab
  • Vegeta负载测试

9. 部署和运维

9.1 环境配置

开发环境configs/config.dev.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

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

配置示例

redis:
  mode: sentinel  # sentinel / cluster / standalone
  master_name: "mymaster"
  sentinel_addrs:
    - "sentinel1:26379"
    - "sentinel2:26379"
    - "sentinel3:26379"

9.3 健康检查

API 健康检查

GET /health

响应

{
  "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