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

506 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` | 认证接口处理器 |
### 配置项
`configs/config.yaml` 中配置 Token 有效期:
```yaml
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` 中配置好。
**后台路由示例**
```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 中存储 TokenXSS 风险)
- 不要在日志中记录完整 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
**维护者**: 君鸿卡管系统开发团队