主要变更: - 新增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模式)
22 KiB
提案:实现 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端代理/企业用户登录"
需要支持:
- Web 后台登录:平台管理员、代理商账号
- H5 端登录:代理商账号、企业账号
目标
业务目标
- 实现后台管理员、代理商、企业用户的账号密码登录
- 支持多端(Web 后台、H5)分别认证
- 提供完整的 token 生命周期管理(生成、验证、刷新、撤销)
- 与现有 RBAC 权限体系无缝集成
- 保持与 C 端认证的架构一致性
技术目标
- 复用现有认证中间件框架
- 遵循项目分层架构(Handler → Service → Store → Model)
- 统一错误处理和响应格式
- 所有 API 响应时间 < 200ms(P95)
- 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)
存储内容:
{
"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 检查权限码
↓ ↓
设置用户上下文 验证角色权限
范围
包含功能
核心功能
-
登录接口
POST /api/admin/login:后台登录(平台用户、代理账号)POST /api/h5/login:H5 端登录(代理账号、企业账号)- 验证用户名/密码
- 生成 access_token 和 refresh_token
- 返回用户信息和权限列表
-
登出接口
POST /api/admin/logout:后台登出POST /api/h5/logout:H5 端登出- 撤销 access_token
- 撤销 refresh_token
- 清理 Redis 缓存
-
Token 刷新接口
POST /api/admin/refresh-token:后台刷新 tokenPOST /api/h5/refresh-token:H5 端刷新 token- 验证 refresh_token
- 生成新的 access_token
- 可选:刷新 refresh_token(rotation)
-
认证中间件配置
- Web 后台认证中间件
- H5 端认证中间件
- 统一使用
pkg/middleware/auth.go的Auth()函数 - 配置不同的 token 验证器
-
Token 管理服务
- Token 生成(access + refresh)
- Token 验证(从 Redis 查询)
- Token 撤销(删除 Redis key)
- Token 续期(更新 TTL)
- 用户所有 token 查询和批量撤销
辅助功能
-
获取当前用户信息
GET /api/admin/me:后台当前用户GET /api/h5/me:H5 当前用户- 返回用户信息、角色、权限列表
-
修改当前用户密码
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)
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 认证 Handler(internal/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
测试策略
单元测试
-
Token 管理器测试(
pkg/auth/token_test.go)- 生成 token 对
- 验证 access token
- 验证 refresh token
- 刷新 token
- 撤销 token
- Redis 连接失败处理
-
认证服务测试(
internal/service/auth/service_test.go)- 登录成功
- 登录失败(密码错误、账号禁用)
- 登出
- 刷新 token
- 修改密码
集成测试
-
登录接口测试(
tests/integration/admin_auth_test.go)- 后台登录成功
- H5 登录成功
- 用户名不存在
- 密码错误
- 账号禁用
- 返回 token 和用户信息
-
认证中间件测试(
tests/integration/admin_auth_middleware_test.go)- 有效 token 访问受保护路由
- 无效 token 返回 401
- 缺失 token 返回 401
- 过期 token 返回 401
- 用户类型不匹配返回 403
-
Token 刷新测试(
tests/integration/token_refresh_test.go)- 使用有效 refresh token 刷新
- 使用无效 refresh token 失败
- 撤销后的 refresh token 失败
-
登出测试(
tests/integration/logout_test.go)- 登出后 token 失效
- 登出后无法访问受保护路由
性能测试
- 认证性能测试(
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:账号数据访问
文档
需要创建的文档:
-
API 文档(
docs/api/auth.md)- 登录接口说明
- 登出接口说明
- Token 刷新接口说明
- 错误码说明
- 示例请求和响应
-
使用指南(
docs/auth-usage-guide.md)- 如何在新路由中集成认证中间件
- 如何获取当前用户信息
- 如何撤销用户 token
- 常见问题(FAQ)
-
架构说明(
docs/auth-architecture.md)- 认证流程图
- Token 存储结构
- 中间件执行顺序
- 安全机制说明
验收标准
- ✅ 后台管理员可以使用用户名/密码登录
- ✅ H5 代理商/企业用户可以使用用户名/密码登录
- ✅ 登录成功返回 access_token、refresh_token 和用户信息
- ✅ 受保护的 API 需要携带有效 token 才能访问
- ✅ Token 过期或无效时返回 401 错误
- ✅ 用户可以登出,登出后 token 立即失效
- ✅ 用户可以使用 refresh_token 刷新 access_token
- ✅ 用户可以修改密码,修改后所有旧 token 失效
- ✅ 不同用户类型只能访问对应端口的 API(后台/H5)
- ✅ 所有测试通过,覆盖率达标
- ✅ API 响应时间 P95 < 200ms
- ✅ 文档完整,便于其他开发者使用
后续迭代
以下功能留待后续迭代:
- 找回密码:通过邮件/短信发送重置链接
- 两步验证(2FA):短信验证码、TOTP
- 单点登录(SSO):统一登录入口
- OAuth 第三方登录:微信企业登录、钉钉登录
- 设备管理:查看登录设备、强制下线
- 登录历史:记录登录时间、IP、设备
- 审计日志:记录认证授权相关操作
- IP 白名单:限制特定 IP 访问
- 账号锁定策略:登录失败次数限制
- 密码策略:强制定期修改、密码历史记录
提案状态:待审批
创建时间:2026-01-15
最后更新:2026-01-15