Files
junhong_cmp_fiber/internal/middleware/personal_auth.go
huang df76e33105 feat: 实现 C 端完整认证系统(client-auth-system)
实现面向个人客户的 7 个认证接口(A1-A7),覆盖资产验证、
微信公众号/小程序登录、手机号绑定/换绑、退出登录完整流程。

主要变更:
- 新增 PersonalCustomerOpenID 模型,支持多 AppID 多 OpenID 管理
- 实现有状态 JWT(JWT + Redis 双重校验),支持服务端主动失效
- 扩展微信 SDK:小程序 Code2Session + 3 个 DB 动态工厂函数
- 实现 A1 资产验证 IP 限流(30/min)和 A4 三层验证码限流
- 新增 7 个错误码(1180-1186)和 6 个 Redis Key 函数
- 注册 /api/c/v1/auth/* 下 7 个端点并更新 OpenAPI 文档
- 数据库迁移 000083:新建 tb_personal_customer_openid 表
2026-03-19 11:33:41 +08:00

116 lines
3.7 KiB
Go
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.
package middleware
import (
"context"
"strings"
"github.com/break/junhong_cmp_fiber/pkg/auth"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/gofiber/fiber/v2"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
// PersonalAuthMiddleware 个人客户认证中间件
type PersonalAuthMiddleware struct {
jwtManager *auth.JWTManager
redis *redis.Client
logger *zap.Logger
}
// NewPersonalAuthMiddleware 创建个人客户认证中间件
func NewPersonalAuthMiddleware(jwtManager *auth.JWTManager, rdb *redis.Client, logger *zap.Logger) *PersonalAuthMiddleware {
return &PersonalAuthMiddleware{
jwtManager: jwtManager,
redis: rdb,
logger: logger,
}
}
// Authenticate 认证中间件
// JWT + Redis 双重校验:先验证 JWT 签名和有效期,再检查 Redis 中 token 是否存在
func (m *PersonalAuthMiddleware) Authenticate() fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
m.logger.Warn("个人客户认证失败:缺少 Authorization header",
zap.String("path", c.Path()),
zap.String("method", c.Method()),
)
return errors.New(errors.CodeUnauthorized, "未提供认证令牌")
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
m.logger.Warn("个人客户认证失败Authorization header 格式错误",
zap.String("path", c.Path()),
zap.String("auth_header", authHeader),
)
return errors.New(errors.CodeUnauthorized, "认证令牌格式错误")
}
token := parts[1]
claims, err := m.jwtManager.VerifyPersonalCustomerToken(token)
if err != nil {
m.logger.Warn("个人客户认证失败token 验证失败",
zap.String("path", c.Path()),
zap.Error(err),
)
return errors.New(errors.CodeUnauthorized, "认证令牌无效或已过期")
}
// Redis 有效性检查token 必须在 Redis 中存在才视为有效
// 支持服务端主动失效(封禁/强制下线/退出登录)
redisKey := constants.RedisPersonalCustomerTokenKey(claims.CustomerID)
storedToken, redisErr := m.redis.Get(context.Background(), redisKey).Result()
if redisErr == redis.Nil {
m.logger.Warn("个人客户认证失败token 已被服务端失效",
zap.Uint("customer_id", claims.CustomerID),
zap.String("path", c.Path()),
)
return errors.New(errors.CodeUnauthorized, "认证令牌已失效,请重新登录")
}
if redisErr != nil {
m.logger.Error("个人客户认证Redis 查询异常",
zap.Uint("customer_id", claims.CustomerID),
zap.Error(redisErr),
)
return errors.New(errors.CodeUnauthorized, "认证服务异常,请稍后重试")
}
// 比对 Redis 中存储的 token 与当前请求 token 是否一致
if storedToken != token {
m.logger.Warn("个人客户认证失败token 不匹配(可能已在其他设备登录)",
zap.Uint("customer_id", claims.CustomerID),
zap.String("path", c.Path()),
)
return errors.New(errors.CodeUnauthorized, "认证令牌已失效,请重新登录")
}
c.Locals("customer_id", claims.CustomerID)
c.Locals("customer_phone", claims.Phone)
c.Locals("skip_owner_filter", true)
m.logger.Debug("个人客户认证成功",
zap.Uint("customer_id", claims.CustomerID),
zap.String("phone", claims.Phone),
zap.String("path", c.Path()),
)
return c.Next()
}
}
// GetCustomerID 从 context 中获取当前个人客户 ID
func GetCustomerID(c *fiber.Ctx) (uint, bool) {
customerID, ok := c.Locals("customer_id").(uint)
return customerID, ok
}
// GetCustomerPhone 从 context 中获取当前个人客户手机号
func GetCustomerPhone(c *fiber.Ctx) (string, bool) {
phone, ok := c.Locals("customer_phone").(string)
return phone, ok
}