# 提案：实现 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
