# 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 中存储 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 **维护者**: 君鸿卡管系统开发团队