Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-15-implement-b-end-auth-system/proposal.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

704 lines
22 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`
**类型**: 新功能
**优先级**: 高
**预计工作量**: 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 响应时间 < 200msP95
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 Token24 小时(可配置)
- Refresh Token7 天(可配置)
### 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_tokenrotation
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 认证 Handlerinternal/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%
## 风险和缓解
### 风险 1Redis 单点故障导致认证不可用
**影响**Redis 宕机导致所有用户无法登录和认证
**缓解措施**
- 使用 Redis 哨兵模式或集群模式(生产环境)
- 实现 Redis 健康检查和自动重连
- 添加 Circuit Breaker 模式,避免雪崩
- 日志记录 Redis 连接失败,便于快速排查
### 风险 2Token 泄露导致账号被盗用
**影响**:攻击者获取 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
## 依赖
### 外部依赖
- ✅ Redistoken 存储和验证
- ✅ PostgreSQL用户账号存储
- ✅ bcrypt密码哈希
- ✅ UUIDtoken 生成
### 内部依赖
-`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