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

22 KiB
Raw Blame History

提案:实现 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 存储 tokenpkg/validator/token.go

对比 JWT

  • JWT 无法撤销(除非维护黑名单,失去无状态优势)
  • JWT payload 可见Base64 解码即可查看)
  • 不适合需要频繁撤销的场景(后台管理系统)

2. Token 存储结构

Redis Key 设计

auth:token:{token}          → 用户基本信息JSON
auth:user:{userID}:tokens   → 用户的所有 token 列表Set

存储内容

{
  "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/loginH5 端登录(代理账号、企业账号)
    • 验证用户名/密码
    • 生成 access_token 和 refresh_token
    • 返回用户信息和权限列表
  2. 登出接口

    • POST /api/admin/logout:后台登出
    • POST /api/h5/logoutH5 端登出
    • 撤销 access_token
    • 撤销 refresh_token
    • 清理 Redis 缓存
  3. Token 刷新接口

    • POST /api/admin/refresh-token:后台刷新 token
    • POST /api/h5/refresh-tokenH5 端刷新 token
    • 验证 refresh_token
    • 生成新的 access_token
    • 可选:刷新 refresh_tokenrotation
  4. 认证中间件配置

    • Web 后台认证中间件
    • H5 端认证中间件
    • 统一使用 pkg/middleware/auth.goAuth() 函数
    • 配置不同的 token 验证器
  5. Token 管理服务

    • Token 生成access + refresh
    • Token 验证(从 Redis 查询)
    • Token 撤销(删除 Redis key
    • Token 续期(更新 TTL
    • 用户所有 token 查询和批量撤销

辅助功能

  1. 获取当前用户信息

    • GET /api/admin/me:后台当前用户
    • GET /api/h5/meH5 当前用户
    • 返回用户信息、角色、权限列表
  2. 修改当前用户密码

    • PUT /api/admin/password:后台修改密码
    • PUT /api/h5/passwordH5 修改密码
    • 验证旧密码
    • 更新密码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

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

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

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. 路由配置

// 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. 中间件配置

// 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 设计

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

// 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
    • 修改密码

集成测试

  1. 登录接口测试tests/integration/admin_auth_test.go

    • 后台登录成功
    • H5 登录成功
    • 用户名不存在
    • 密码错误
    • 账号禁用
    • 返回 token 和用户信息
  2. 认证中间件测试tests/integration/admin_auth_middleware_test.go

    • 有效 token 访问受保护路由
    • 无效 token 返回 401
    • 缺失 token 返回 401
    • 过期 token 返回 401
    • 用户类型不匹配返回 403
  3. Token 刷新测试tests/integration/token_refresh_test.go

    • 使用有效 refresh token 刷新
    • 使用无效 refresh token 失败
    • 撤销后的 refresh token 失败
  4. 登出测试tests/integration/logout_test.go

    • 登出后 token 失效
    • 登出后无法访问受保护路由

性能测试

  1. 认证性能测试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