# 提案:实现 B 端认证系统 **Change ID**: `implement-b-end-auth-system` **类型**: 新功能 **优先级**: 高 **预计工作量**: 3-5 天 --- ## 概述 完成 B 端(Web 后台 + H5 端)的完整认证系统,包括后台管理员登录、代理商登录、企业用户登录,以及配套的 token 管理、登出、刷新等功能。 ## 背景 ### 当前状态 项目已完成: - ✅ C 端(个人客户)JWT 认证 - ✅ 通用认证中间件框架 (`pkg/middleware/auth.go`) - ✅ RBAC 权限体系(角色、权限、数据权限过滤) - ✅ 用户上下文传递机制 - ✅ 密码加密(bcrypt) 缺失功能: - ❌ B 端登录接口(后台/代理/企业) - ❌ B 端 token 生成和 Redis 存储 - ❌ 登出功能(token 撤销) - ❌ Token 刷新机制 - ❌ 多端认证中间件配置 ### 用户需求 用户明确要求: > "目前不需要做个人用户登录,只需要做后台代理商/平台登录,h5端代理/企业用户登录" 需要支持: 1. **Web 后台登录**:平台管理员、代理商账号 2. **H5 端登录**:代理商账号、企业账号 ## 目标 ### 业务目标 1. 实现后台管理员、代理商、企业用户的账号密码登录 2. 支持多端(Web 后台、H5)分别认证 3. 提供完整的 token 生命周期管理(生成、验证、刷新、撤销) 4. 与现有 RBAC 权限体系无缝集成 5. 保持与 C 端认证的架构一致性 ### 技术目标 1. 复用现有认证中间件框架 2. 遵循项目分层架构(Handler → Service → Store → Model) 3. 统一错误处理和响应格式 4. 所有 API 响应时间 < 200ms(P95) 5. Token 验证缓存在 Redis,支持高并发 ## 设计决策 ### 1. 认证方式选择 **决策**:B 端使用 **Redis Token** 认证,而非 JWT **理由**: - ✅ **可撤销性**:支持立即登出和强制下线 - ✅ **灵活性**:可存储额外会话信息(登录时间、设备信息等) - ✅ **安全性**:Token 可以是随机 UUID,不携带敏感信息 - ✅ **分布式友好**:Redis 集群天然支持多服务器部署 - ✅ **与现有架构一致**:项目已使用 Redis 存储 token(`pkg/validator/token.go`) **对比 JWT**: - ❌ JWT 无法撤销(除非维护黑名单,失去无状态优势) - ❌ JWT payload 可见(Base64 解码即可查看) - ❌ 不适合需要频繁撤销的场景(后台管理系统) ### 2. Token 存储结构 **Redis Key 设计**: ``` auth:token:{token} → 用户基本信息(JSON) auth:user:{userID}:tokens → 用户的所有 token 列表(Set) ``` **存储内容**: ```json { "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" } ``` **TTL 配置**: - Access Token:24 小时(可配置) - Refresh Token:7 天(可配置) ### 3. 多端认证设计 **Web 后台**: - 路由前缀:`/api/admin/*` - 认证方式:Bearer Token - 权限过滤:`platform = 'web' OR platform = 'all'` - 支持用户类型:超级管理员、平台用户、代理账号 **H5 端**: - 路由前缀:`/api/h5/*` - 认证方式:Bearer Token - 权限过滤:`platform = 'h5' OR platform = 'all'` - 支持用户类型:代理账号、企业账号 ### 4. 登录流程设计 ``` ┌─────────────────────────────────────────────────────────────┐ │ POST /api/admin/login │ │ POST /api/h5/login │ └────────────────────────────┬────────────────────────────────┘ │ ┌──────────▼──────────┐ │ 1. 验证用户名/密码 │ │ (bcrypt.Compare)│ └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ 2. 检查账号状态 │ │ (status=1) │ └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ 3. 生成 UUID Token │ │ (uuid.New()) │ └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ 4. 存储到 Redis │ │ (TTL: 24h) │ └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ 5. 返回 Token │ │ (+ 用户信息) │ └──────────┬──────────┘ │ ┌────────────────────────────▼────────────────────────────────┐ │ Response: {token, refresh_token, user_info, permissions} │ └─────────────────────────────────────────────────────────────┘ ``` ### 5. 权限检查流程 ``` 请求 → Auth 中间件 → Permission 中间件 → 业务处理器 ↓ ↓ 验证 Token 检查权限码 ↓ ↓ 设置用户上下文 验证角色权限 ``` ## 范围 ### 包含功能 #### 核心功能 1. **登录接口** - `POST /api/admin/login`:后台登录(平台用户、代理账号) - `POST /api/h5/login`:H5 端登录(代理账号、企业账号) - 验证用户名/密码 - 生成 access_token 和 refresh_token - 返回用户信息和权限列表 2. **登出接口** - `POST /api/admin/logout`:后台登出 - `POST /api/h5/logout`:H5 端登出 - 撤销 access_token - 撤销 refresh_token - 清理 Redis 缓存 3. **Token 刷新接口** - `POST /api/admin/refresh-token`:后台刷新 token - `POST /api/h5/refresh-token`:H5 端刷新 token - 验证 refresh_token - 生成新的 access_token - 可选:刷新 refresh_token(rotation) 4. **认证中间件配置** - Web 后台认证中间件 - H5 端认证中间件 - 统一使用 `pkg/middleware/auth.go` 的 `Auth()` 函数 - 配置不同的 token 验证器 5. **Token 管理服务** - Token 生成(access + refresh) - Token 验证(从 Redis 查询) - Token 撤销(删除 Redis key) - Token 续期(更新 TTL) - 用户所有 token 查询和批量撤销 #### 辅助功能 6. **获取当前用户信息** - `GET /api/admin/me`:后台当前用户 - `GET /api/h5/me`:H5 当前用户 - 返回用户信息、角色、权限列表 7. **修改当前用户密码** - `PUT /api/admin/password`:后台修改密码 - `PUT /api/h5/password`:H5 修改密码 - 验证旧密码 - 更新密码(bcrypt 哈希) - 撤销所有旧 token ### 不包含功能 - ❌ 找回密码(通过邮件/短信)→ 后续迭代 - ❌ 两步验证(2FA)→ 后续迭代 - ❌ 单点登录(SSO)→ 后续迭代 - ❌ OAuth 第三方登录(微信、钉钉等)→ 后续迭代 - ❌ 设备管理和多设备限制 → 后续迭代 - ❌ 登录历史和审计日志 → 后续迭代 ## 技术方案 ### 1. 目录结构 ``` internal/ ├── handler/ │ ├── admin/ │ │ └── auth.go # 后台认证 Handler(新增) │ └── h5/ │ └── auth.go # H5 认证 Handler(新增) ├── service/ │ └── auth/ │ └── service.go # 认证服务(新增) ├── store/ │ └── postgres/ │ └── account_store.go # 账号查询(已存在,扩展方法) ├── model/ │ └── auth_dto.go # 认证 DTO(新增) pkg/ ├── auth/ │ └── token.go # Token 管理工具(新增) ├── constants/ │ └── auth.go # 认证常量(新增) └── middleware/ └── auth.go # 通用认证中间件(已存在,无需修改) ``` ### 2. 核心模块设计 #### 2.1 Token 管理器(pkg/auth/token.go) ```go package auth import ( "context" "time" "github.com/google/uuid" "github.com/redis/go-redis/v9" ) // TokenManager Token 管理器 type TokenManager struct { rdb *redis.Client accessTokenTTL time.Duration refreshTokenTTL time.Duration } // TokenInfo Token 信息(存储在 Redis) 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"` } // GenerateTokenPair 生成 access token 和 refresh token func (m *TokenManager) GenerateTokenPair(ctx context.Context, info *TokenInfo) (accessToken, refreshToken string, err error) // ValidateAccessToken 验证 access token 并返回用户信息 func (m *TokenManager) ValidateAccessToken(ctx context.Context, token string) (*TokenInfo, error) // ValidateRefreshToken 验证 refresh token func (m *TokenManager) ValidateRefreshToken(ctx context.Context, token string) (*TokenInfo, error) // RefreshAccessToken 使用 refresh token 刷新 access token func (m *TokenManager) RefreshAccessToken(ctx context.Context, refreshToken string) (newAccessToken string, err error) // RevokeToken 撤销单个 token func (m *TokenManager) RevokeToken(ctx context.Context, token string) error // RevokeAllUserTokens 撤销用户的所有 token func (m *TokenManager) RevokeAllUserTokens(ctx context.Context, userID uint) error // RenewTokenTTL 续期 token(用于"记住我"功能) func (m *TokenManager) RenewTokenTTL(ctx context.Context, token string, ttl time.Duration) error ``` #### 2.2 认证服务(internal/service/auth/service.go) ```go package auth import ( "context" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/auth" "golang.org/x/crypto/bcrypt" ) // Service 认证服务 type Service struct { accountStore AccountStore tokenManager *auth.TokenManager logger *zap.Logger } // LoginRequest 登录请求 type LoginRequest struct { Username string `json:"username" validate:"required"` Password string `json:"password" validate:"required"` Device string `json:"device"` // web / h5 / mobile } // LoginResponse 登录响应 type LoginResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` User *model.Account `json:"user"` Permissions []string `json:"permissions"` } // Login 用户登录 func (s *Service) Login(ctx context.Context, req *LoginRequest, clientIP string) (*LoginResponse, error) // Logout 用户登出 func (s *Service) Logout(ctx context.Context, token string) error // RefreshToken 刷新 token func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (newAccessToken string, error) // GetCurrentUser 获取当前用户信息 func (s *Service) GetCurrentUser(ctx context.Context, userID uint) (*model.Account, []string, error) // ChangePassword 修改密码 func (s *Service) ChangePassword(ctx context.Context, userID uint, oldPassword, newPassword string) error ``` #### 2.3 认证 Handler(internal/handler/admin/auth.go) ```go package admin import ( "github.com/gofiber/fiber/v2" "github.com/break/junhong_cmp_fiber/internal/service/auth" "github.com/break/junhong_cmp_fiber/pkg/response" ) // AuthHandler 认证处理器 type AuthHandler struct { authService *auth.Service } // Login 登录 // POST /api/admin/login func (h *AuthHandler) Login(c *fiber.Ctx) error // Logout 登出 // POST /api/admin/logout func (h *AuthHandler) Logout(c *fiber.Ctx) error // RefreshToken 刷新 token // POST /api/admin/refresh-token func (h *AuthHandler) RefreshToken(c *fiber.Ctx) error // GetMe 获取当前用户信息 // GET /api/admin/me func (h *AuthHandler) GetMe(c *fiber.Ctx) error // ChangePassword 修改密码 // PUT /api/admin/password func (h *AuthHandler) ChangePassword(c *fiber.Ctx) error ``` ### 3. 路由配置 ```go // internal/routes/admin.go // 公开路由(无需认证) public := api.Group("/admin") public.Post("/login", authHandler.Login) public.Post("/refresh-token", authHandler.RefreshToken) // 受保护路由(需要认证) protected := api.Group("/admin") protected.Use(adminAuthMiddleware) // 使用后台认证中间件 protected.Post("/logout", authHandler.Logout) protected.Get("/me", authHandler.GetMe) protected.Put("/password", authHandler.ChangePassword) // ... 其他受保护路由 ``` ### 4. 中间件配置 ```go // internal/bootstrap/middlewares.go // 后台认证中间件 adminAuthMiddleware := middleware.Auth(middleware.AuthConfig{ TokenValidator: func(token string) (*middleware.UserContextInfo, error) { tokenInfo, err := tokenManager.ValidateAccessToken(ctx, token) if err != nil { return nil, err } // 检查用户类型(后台只允许平台用户和代理账号) if tokenInfo.UserType != constants.UserTypeSuperAdmin && tokenInfo.UserType != constants.UserTypePlatform && tokenInfo.UserType != constants.UserTypeAgent { return nil, errors.New(errors.CodeForbidden, "无权访问后台") } return &middleware.UserContextInfo{ UserID: tokenInfo.UserID, UserType: tokenInfo.UserType, ShopID: tokenInfo.ShopID, EnterpriseID: tokenInfo.EnterpriseID, }, nil }, SkipPaths: []string{"/api/admin/login", "/api/admin/refresh-token"}, }) // H5 认证中间件 h5AuthMiddleware := middleware.Auth(middleware.AuthConfig{ TokenValidator: func(token string) (*middleware.UserContextInfo, error) { tokenInfo, err := tokenManager.ValidateAccessToken(ctx, token) if err != nil { return nil, err } // 检查用户类型(H5 只允许代理账号和企业账号) if tokenInfo.UserType != constants.UserTypeAgent && tokenInfo.UserType != constants.UserTypeEnterprise { return nil, errors.New(errors.CodeForbidden, "无权访问 H5 端") } return &middleware.UserContextInfo{ UserID: tokenInfo.UserID, UserType: tokenInfo.UserType, ShopID: tokenInfo.ShopID, EnterpriseID: tokenInfo.EnterpriseID, }, nil }, SkipPaths: []string{"/api/h5/login", "/api/h5/refresh-token"}, }) ``` ### 5. Redis Key 设计 ```go // pkg/constants/auth.go // RedisAuthTokenKey 生成认证令牌的 Redis 键 func RedisAuthTokenKey(token string) string { return fmt.Sprintf("auth:token:%s", token) } // RedisRefreshTokenKey 生成刷新令牌的 Redis 键 func RedisRefreshTokenKey(token string) string { return fmt.Sprintf("auth:refresh:%s", token) } // RedisUserTokensKey 生成用户令牌列表的 Redis 键 func RedisUserTokensKey(userID uint) string { return fmt.Sprintf("auth:user:%d:tokens", userID) } ``` ### 6. 错误码扩展 ```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 // 密码强度不足(已存在) ``` ## 实现计划 详见 `tasks.md` ## 测试策略 ### 单元测试 1. **Token 管理器测试**(`pkg/auth/token_test.go`) - 生成 token 对 - 验证 access token - 验证 refresh token - 刷新 token - 撤销 token - Redis 连接失败处理 2. **认证服务测试**(`internal/service/auth/service_test.go`) - 登录成功 - 登录失败(密码错误、账号禁用) - 登出 - 刷新 token - 修改密码 ### 集成测试 3. **登录接口测试**(`tests/integration/admin_auth_test.go`) - 后台登录成功 - H5 登录成功 - 用户名不存在 - 密码错误 - 账号禁用 - 返回 token 和用户信息 4. **认证中间件测试**(`tests/integration/admin_auth_middleware_test.go`) - 有效 token 访问受保护路由 - 无效 token 返回 401 - 缺失 token 返回 401 - 过期 token 返回 401 - 用户类型不匹配返回 403 5. **Token 刷新测试**(`tests/integration/token_refresh_test.go`) - 使用有效 refresh token 刷新 - 使用无效 refresh token 失败 - 撤销后的 refresh token 失败 6. **登出测试**(`tests/integration/logout_test.go`) - 登出后 token 失效 - 登出后无法访问受保护路由 ### 性能测试 7. **认证性能测试**(`tests/benchmark/auth_bench_test.go`) - Token 验证性能(目标:< 5ms) - 登录性能(目标:< 200ms) - 并发登录测试(1000 并发) ### 测试覆盖率目标 - 核心业务逻辑:≥ 90% - Handler 层:≥ 80% - 整体覆盖率:≥ 70% ## 风险和缓解 ### 风险 1:Redis 单点故障导致认证不可用 **影响**:Redis 宕机导致所有用户无法登录和认证 **缓解措施**: - 使用 Redis 哨兵模式或集群模式(生产环境) - 实现 Redis 健康检查和自动重连 - 添加 Circuit Breaker 模式,避免雪崩 - 日志记录 Redis 连接失败,便于快速排查 ### 风险 2:Token 泄露导致账号被盗用 **影响**:攻击者获取 token 后可以冒充用户 **缓解措施**: - Token 使用 UUID v4(不可预测) - HTTPS 强制加密传输 - Token 设置合理的过期时间(24 小时) - 实现 IP 绑定和设备指纹(后续迭代) - 异常登录检测和通知(后续迭代) ### 风险 3:暴力破解登录 **影响**:攻击者通过暴力破解获取账号密码 **缓解措施**: - 集成现有的限流中间件(`pkg/middleware/ratelimit.go`) - 登录失败次数限制(5 次锁定 15 分钟) - 添加图形验证码(后续迭代) - 记录登录失败日志,便于审计 ### 风险 4:密码存储安全 **影响**:数据库泄露导致密码被破解 **缓解措施**: - 已使用 bcrypt 哈希(cost=10) - 禁止明文密码传输(HTTPS) - 密码复杂度要求(8-32 位,含字母数字) - 定期密码过期提醒(后续迭代) ### 风险 5:与现有代码集成冲突 **影响**:新代码与现有认证逻辑冲突 **缓解措施**: - 复用现有的 `pkg/middleware/auth.go` 框架 - 不修改 C 端认证逻辑(`internal/middleware/personal_auth.go`) - 充分的集成测试覆盖 - 代码审查(Code Review) ## 依赖 ### 外部依赖 - ✅ Redis:token 存储和验证 - ✅ PostgreSQL:用户账号存储 - ✅ bcrypt:密码哈希 - ✅ UUID:token 生成 ### 内部依赖 - ✅ `pkg/middleware/auth.go`:通用认证中间件 - ✅ `pkg/errors`:统一错误处理 - ✅ `pkg/response`:统一响应格式 - ✅ `pkg/constants`:常量定义 - ✅ `internal/model/account.go`:账号模型 - ✅ `internal/store/postgres/account_store.go`:账号数据访问 ## 文档 需要创建的文档: 1. **API 文档**(`docs/api/auth.md`) - 登录接口说明 - 登出接口说明 - Token 刷新接口说明 - 错误码说明 - 示例请求和响应 2. **使用指南**(`docs/auth-usage-guide.md`) - 如何在新路由中集成认证中间件 - 如何获取当前用户信息 - 如何撤销用户 token - 常见问题(FAQ) 3. **架构说明**(`docs/auth-architecture.md`) - 认证流程图 - Token 存储结构 - 中间件执行顺序 - 安全机制说明 ## 验收标准 1. ✅ 后台管理员可以使用用户名/密码登录 2. ✅ H5 代理商/企业用户可以使用用户名/密码登录 3. ✅ 登录成功返回 access_token、refresh_token 和用户信息 4. ✅ 受保护的 API 需要携带有效 token 才能访问 5. ✅ Token 过期或无效时返回 401 错误 6. ✅ 用户可以登出,登出后 token 立即失效 7. ✅ 用户可以使用 refresh_token 刷新 access_token 8. ✅ 用户可以修改密码,修改后所有旧 token 失效 9. ✅ 不同用户类型只能访问对应端口的 API(后台/H5) 10. ✅ 所有测试通过,覆盖率达标 11. ✅ API 响应时间 P95 < 200ms 12. ✅ 文档完整,便于其他开发者使用 ## 后续迭代 以下功能留待后续迭代: 1. **找回密码**:通过邮件/短信发送重置链接 2. **两步验证(2FA)**:短信验证码、TOTP 3. **单点登录(SSO)**:统一登录入口 4. **OAuth 第三方登录**:微信企业登录、钉钉登录 5. **设备管理**:查看登录设备、强制下线 6. **登录历史**:记录登录时间、IP、设备 7. **审计日志**:记录认证授权相关操作 8. **IP 白名单**:限制特定 IP 访问 9. **账号锁定策略**:登录失败次数限制 10. **密码策略**:强制定期修改、密码历史记录 --- **提案状态**:待审批 **创建时间**:2026-01-15 **最后更新**:2026-01-15