Files
junhong_cmp_fiber/docs/auth-usage-guide.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

13 KiB
Raw Permalink Blame History

B 端认证系统使用指南

本文档指导开发者如何在君鸿卡管系统中使用 B 端认证功能,包括在新路由中集成认证、获取用户信息、撤销 Token 等操作。


目录


快速开始

###认证系统已集成到项目的 bootstrap 流程中,无需额外配置即可使用。

核心组件

组件 位置 用途
TokenManager pkg/auth/token.go Token 生成、验证、撤销
AuthService internal/service/auth/service.go 认证业务逻辑
Auth Middleware pkg/middleware/auth.go 认证中间件
Auth Handler internal/handler/{admin,h5}/auth.go 认证接口处理器

配置项

configs/config.yaml 中配置 Token 有效期:

jwt:
  secret_key: "your-secret-key-here"
  token_duration: 3600                # JWT 有效期(个人客户,秒)
  access_token_ttl: 86400             # Access Token 有效期B端
  refresh_token_ttl: 604800           # Refresh Token 有效期B端

在路由中集成认证

1. 使用现有的认证中间件

后台和 H5 的认证中间件已在 internal/bootstrap/middlewares.go 中配置好。

后台路由示例

// internal/routes/admin.go
func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, middlewares *bootstrap.Middlewares, doc *openapi.Generator, basePath string) {
    // 公开路由(无需认证)
    router.Post(basePath+"/login", handlers.AdminAuth.Login)
    router.Post(basePath+"/refresh-token", handlers.AdminAuth.RefreshToken)

    // 受保护路由(需要认证)
    authGroup := router.Group("", middlewares.AdminAuth)
    authGroup.Post(basePath+"/logout", handlers.AdminAuth.Logout)
    authGroup.Get(basePath+"/me", handlers.AdminAuth.GetMe)
    authGroup.Post(basePath+"/password", handlers.AdminAuth.ChangePassword)
    
    // 添加其他需要认证的路由
    authGroup.Get(basePath+"/users", handlers.User.List)
    authGroup.Post(basePath+"/users", handlers.User.Create)
}

H5 路由示例

// internal/routes/h5.go
func RegisterH5Routes(router fiber.Router, handlers *bootstrap.Handlers, middlewares *bootstrap.Middlewares, doc *openapi.Generator, basePath string) {
    // 公开路由
    router.Post(basePath+"/login", handlers.H5Auth.Login)
    
    // 受保护路由
    authGroup := router.Group("", middlewares.H5Auth)
    authGroup.Get(basePath+"/orders", handlers.Order.List)
}

2. 创建自定义认证中间件

如果需要自定义认证逻辑(例如特殊权限检查),可以创建自己的中间件:

// internal/middleware/custom_auth.go
package middleware

import (
    "github.com/break/junhong_cmp_fiber/pkg/auth"
    "github.com/break/junhong_cmp_fiber/pkg/constants"
    "github.com/break/junhong_cmp_fiber/pkg/errors"
    pkgmiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
    "github.com/gofiber/fiber/v2"
)

// SuperAdminOnly 只允许超级管理员访问
func SuperAdminOnly(tokenManager *auth.TokenManager) fiber.Handler {
    return pkgmiddleware.Auth(pkgmiddleware.AuthConfig{
        TokenValidator: func(token string) (*pkgmiddleware.UserContextInfo, error) {
            tokenInfo, err := tokenManager.ValidateAccessToken(context.Background(), token)
            if err != nil {
                return nil, errors.New(errors.CodeInvalidToken, "令牌无效")
            }

            // 只允许超级管理员
            if tokenInfo.UserType != constants.UserTypeSuperAdmin {
                return nil, errors.New(errors.CodeForbidden, "权限不足")
            }

            return &pkgmiddleware.UserContextInfo{
                UserID:       tokenInfo.UserID,
                UserType:     tokenInfo.UserType,
                ShopID:       tokenInfo.ShopID,
                EnterpriseID: tokenInfo.EnterpriseID,
            }, nil
        },
        SkipPaths: []string{}, // 无公开路径
    })
}

获取当前用户信息

1. 在 Handler 中获取用户 ID

使用 pkg/middleware 提供的工具函数:

// internal/handler/admin/user.go
package admin

import (
    "github.com/break/junhong_cmp_fiber/pkg/errors"
    "github.com/break/junhong_cmp_fiber/pkg/middleware"
    "github.com/break/junhong_cmp_fiber/pkg/response"
    "github.com/gofiber/fiber/v2"
)

type UserHandler struct {
    userService *user.Service
}

func (h *UserHandler) GetProfile(c *fiber.Ctx) error {
    // 从 context 获取当前用户 ID
    userID := middleware.GetUserIDFromContext(c.UserContext())
    if userID == 0 {
        return errors.New(errors.CodeUnauthorized, "未授权访问")
    }

    // 使用 userID 查询用户信息
    profile, err := h.userService.GetProfile(c.UserContext(), userID)
    if err != nil {
        return err
    }

    return response.Success(c, profile)
}

2. 获取完整的用户上下文

func (h *UserHandler) DoSomething(c *fiber.Ctx) error {
    ctx := c.UserContext()
    
    // 获取各种用户信息
    userID := middleware.GetUserIDFromContext(ctx)
    userType := middleware.GetUserTypeFromContext(ctx)
    shopID := middleware.GetShopIDFromContext(ctx)
    enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
    
    // 根据用户类型执行不同逻辑
    switch userType {
    case constants.UserTypeSuperAdmin:
        // 超级管理员逻辑
    case constants.UserTypeAgent:
        // 代理商逻辑,使用 shopID
    case constants.UserTypeEnterprise:
        // 企业客户逻辑,使用 enterpriseID
    }
    
    return response.Success(c, nil)
}

3. 在 Service 层使用用户信息

Service 层应通过参数接收用户信息,而不是直接从 context 获取:

// internal/service/order/service.go
package order

type Service struct {
    orderStore *postgres.OrderStore
}

// 推荐:显式传递 userID
func (s *Service) ListOrders(ctx context.Context, userID uint, filters *OrderFilters) ([]*model.Order, error) {
    // 根据用户权限过滤订单
    return s.orderStore.ListByUser(ctx, userID, filters)
}

// 不推荐:从 context 中获取
// func (s *Service) ListOrders(ctx context.Context, filters *OrderFilters) ([]*model.Order, error) {
//     userID := middleware.GetUserIDFromContext(ctx) // 不推荐
//     ...
// }

Token 管理

1. 生成 Token

在认证服务中已实现,无需手动调用。如需在其他场景使用:

package myservice

import (
    "github.com/break/junhong_cmp_fiber/pkg/auth"
)

func (s *Service) IssueTokenForUser(ctx context.Context, userID uint) (string, string, error) {
    tokenInfo := &auth.TokenInfo{
        UserID:       userID,
        UserType:     1,
        ShopID:       0,
        EnterpriseID: 0,
        Username:     "user",
        Device:       "web",
        IP:           "127.0.0.1",
    }

    accessToken, refreshToken, err := s.tokenManager.GenerateTokenPair(ctx, tokenInfo)
    if err != nil {
        return "", "", err
    }

    return accessToken, refreshToken, nil
}

2. 验证 Token

Token 验证已由中间件自动完成。如需手动验证:

func (s *Service) ManuallyValidateToken(ctx context.Context, token string) (*auth.TokenInfo, error) {
    tokenInfo, err := s.tokenManager.ValidateAccessToken(ctx, token)
    if err != nil {
        return nil, err
    }
    
    return tokenInfo, nil
}

3. 撤销 Token

撤销单个 Token

func (s *Service) RevokeToken(ctx context.Context, token string) error {
    return s.tokenManager.RevokeToken(ctx, token)
}

撤销用户所有 Token(例如修改密码后):

func (s *Service) RevokeAllUserTokens(ctx context.Context, userID uint) error {
    return s.tokenManager.RevokeAllUserTokens(ctx, userID)
}

常见问题

Q1: 如何测试需要认证的接口?

方法 1使用真实 Token

# 1. 先登录获取 token
TOKEN=$(curl -s -X POST http://localhost:8080/api/admin/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"Admin@123456"}' \
  | jq -r '.data.access_token')

# 2. 使用 token 访问接口
curl -X GET http://localhost:8080/api/admin/users \
  -H "Authorization: Bearer $TOKEN"

方法 2在集成测试中模拟

// tests/integration/user_test.go
func TestListUsers(t *testing.T) {
    // 创建测试账号
    account := createTestAccount(t)
    
    // 生成 token
    tokenManager := auth.NewTokenManager(redisClient, 24*time.Hour, 7*24*time.Hour)
    accessToken, _, err := tokenManager.GenerateTokenPair(ctx, &auth.TokenInfo{
        UserID:   account.ID,
        UserType: account.UserType,
        Username: account.Username,
    })
    require.NoError(t, err)
    
    // 发送请求
    req := httptest.NewRequest("GET", "/api/admin/users", nil)
    req.Header.Set("Authorization", "Bearer "+accessToken)
    
    resp, err := app.Test(req)
    require.NoError(t, err)
    assert.Equal(t, 200, resp.StatusCode)
}

Q2: 如何处理 Token 过期?

前端应捕获 1003 错误码,自动使用 Refresh Token 刷新:

// 前端示例(伪代码)
async function apiRequest(url, options) {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${getAccessToken()}`
    }
  });

  // Token 过期
  if (response.status === 401 && response.data.code === 1003) {
    // 刷新 token
    const newToken = await refreshAccessToken();
    setAccessToken(newToken);
    
    // 重试原请求
    response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${newToken}`
      }
    });
  }

  return response;
}

async function refreshAccessToken() {
  const response = await fetch('/api/admin/refresh-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refresh_token: getRefreshToken() })
  });
  
  const data = await response.json();
  return data.data.access_token;
}

Q3: 如何区分后台和 H5 用户?

通过 userType 字段区分:

userType := middleware.GetUserTypeFromContext(ctx)

switch userType {
case constants.UserTypeSuperAdmin:  // 1
    // 超级管理员
case constants.UserTypePlatform:    // 2
    // 平台用户
case constants.UserTypeAgent:       // 3
    // 代理商(后台和 H5 都可以)
case constants.UserTypeEnterprise:  // 4
    // 企业客户(仅 H5
}

Q4: 如何实现"记住我"功能?

当前系统不支持"记住我"。如需实现:

  1. 增加一个长期 Token 类型30 天)
  2. 前端存储到 LocalStorage 或 Cookie
  3. 后端需要额外的安全机制(如设备指纹)

最佳实践

1. 安全实践

推荐做法

  • 所有敏感操作(修改密码、删除数据)要求二次验证
  • Token 存储在 HttpOnly Cookie 或安全存储中
  • 使用 HTTPS 传输
  • 定期更新密码
  • 修改密码后撤销所有旧 Token

避免做法

  • 不要在 URL 中传递 Token
  • 不要在浏览器 LocalStorage 中存储 TokenXSS 风险)
  • 不要在日志中记录完整 Token
  • 不要与他人分享 Token

2. 错误处理

Handler 应返回 *errors.AppError,由全局 ErrorHandler 统一处理:

func (h *UserHandler) Create(c *fiber.Ctx) error {
    userID := middleware.GetUserIDFromContext(c.UserContext())
    if userID == 0 {
        // 返回 AppError不要自己构造 JSON
        return errors.New(errors.CodeUnauthorized, "未授权访问")
    }
    
    // ... 业务逻辑
    
    return response.Success(c, result)
}

3. 性能优化

  • Token 验证操作已由 Redis 优化,平均耗时 < 5ms
  • 避免在循环中重复验证 Token
  • 使用批量操作减少 Redis 调用

4. 日志记录

记录关键认证事件:

import "go.uber.org/zap"

// 登录成功
logger.Info("用户登录成功",
    zap.Uint("user_id", userID),
    zap.String("username", username),
    zap.String("ip", clientIP),
    zap.String("device", device),
)

// 登录失败
logger.Warn("登录失败",
    zap.String("username", username),
    zap.String("ip", clientIP),
    zap.String("reason", "密码错误"),
)

// Token 撤销
logger.Info("Token 已撤销",
    zap.Uint("user_id", userID),
    zap.String("reason", "修改密码"),
)

5. 测试覆盖

确保以下场景有测试覆盖:

  • 登录成功
  • 登录失败(密码错误、账号禁用)
  • Token 验证成功
  • Token 过期处理
  • Token 刷新
  • 修改密码后 Token 失效
  • 并发访问

相关文档


文档版本: v1.0
最后更新: 2026-01-15
维护者: 君鸿卡管系统开发团队