# B 端认证系统使用指南

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

---

## 目录

- [快速开始](#快速开始)
- [在路由中集成认证](#在路由中集成认证)
- [获取当前用户信息](#获取当前用户信息)
- [Token 管理](#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` | 认证接口处理器 |

### 配置项

通过环境变量配置 Token 有效期：

```bash
# JWT 配置
export JUNHONG_JWT_SECRET_KEY="your-secret-key-here"
export JUNHONG_JWT_TOKEN_DURATION="24h"       # JWT 有效期（个人客户）
export JUNHONG_JWT_ACCESS_TOKEN_TTL="24h"     # Access Token 有效期（B端）
export JUNHONG_JWT_REFRESH_TOKEN_TTL="168h"   # Refresh Token 有效期（B端，7天）
```

详细配置说明见 [环境变量配置文档](environment-variables.md)

---

## 在路由中集成认证

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

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

**后台路由示例**：

```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 路由示例**：

```go
// 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. 创建自定义认证中间件

如果需要自定义认证逻辑（例如特殊权限检查），可以创建自己的中间件：

```go
// 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` 提供的工具函数：

```go
// 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. 获取完整的用户上下文

```go
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 获取：

```go
// 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

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

```go
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 验证已由中间件自动完成。如需手动验证：

```go
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**：

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

**撤销用户所有 Token**（例如修改密码后）：

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

---

## 常见问题

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

**方法 1：使用真实 Token**

```bash
# 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：在集成测试中模拟**

```go
// 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 刷新：

```javascript
// 前端示例（伪代码）
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` 字段区分：

```go
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 中存储 Token（XSS 风险）
- 不要在日志中记录完整 Token
- 不要与他人分享 Token

### 2. 错误处理

Handler 应返回 `*errors.AppError`，由全局 ErrorHandler 统一处理：

```go
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. 日志记录

记录关键认证事件：

```go
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. 测试覆盖

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

- [x] 登录成功
- [x] 登录失败（密码错误、账号禁用）
- [x] Token 验证成功
- [x] Token 过期处理
- [x] Token 刷新
- [x] 修改密码后 Token 失效
- [x] 并发访问

---

## 相关文档

- [API 文档](api/auth.md) - 完整的 API 接口说明
- [架构说明](auth-architecture.md) - 认证系统架构设计
- [错误处理指南](003-error-handling/使用指南.md) - 统一错误处理

---

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