主要变更: - 新增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模式)
13 KiB
13 KiB
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: 如何实现"记住我"功能?
当前系统不支持"记住我"。如需实现:
- 增加一个长期 Token 类型(30 天)
- 前端存储到 LocalStorage 或 Cookie
- 后端需要额外的安全机制(如设备指纹)
最佳实践
1. 安全实践
✅ 推荐做法:
- 所有敏感操作(修改密码、删除数据)要求二次验证
- Token 存储在 HttpOnly Cookie 或安全存储中
- 使用 HTTPS 传输
- 定期更新密码
- 修改密码后撤销所有旧 Token
❌ 避免做法:
- 不要在 URL 中传递 Token
- 不要在浏览器 LocalStorage 中存储 Token(XSS 风险)
- 不要在日志中记录完整 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
维护者: 君鸿卡管系统开发团队