实现面向个人客户的 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 表
116 lines
3.7 KiB
Go
116 lines
3.7 KiB
Go
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
|
||
}
|