package middleware import ( "context" "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/hex" "net/url" "sort" "strconv" "strings" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "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" "github.com/redis/go-redis/v9" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) // AgentOpenAPIAuthConfig 代理开放接口认证中间件配置 type AgentOpenAPIAuthConfig struct { AccountStore *postgres.AccountStore ShopStore *postgres.ShopStore Redis *redis.Client } // NewAgentOpenAPIAuthMiddleware 创建代理开放接口认证中间件 func NewAgentOpenAPIAuthMiddleware(config AgentOpenAPIAuthConfig) fiber.Handler { return func(c *fiber.Ctx) error { if c.Method() == fiber.MethodOptions { return c.SendStatus(fiber.StatusNoContent) } credentials := extractAgentOpenAPICredentials(c) if err := credentials.validateRequired(); err != nil { return err } account, err := authenticateAgentAccountForOpenAPI(c.UserContext(), config, credentials) if err != nil { return err } if err := validateAgentOpenAPITimestamp(credentials.Timestamp, time.Now()); err != nil { return err } if err := reserveAgentOpenAPINonce(c.UserContext(), config.Redis, credentials.Account, credentials.Nonce); err != nil { return err } payload := buildAgentOpenAPISignPayload(c, credentials) if err := verifyAgentOpenAPISign(payload, credentials.Password, credentials.Sign); err != nil { return err } if err := injectAgentOpenAPIContext(c, config.ShopStore, account); err != nil { return err } return c.Next() } } type agentOpenAPICredentials struct { Account string Password string Timestamp string Nonce string Sign string } func extractAgentOpenAPICredentials(c *fiber.Ctx) agentOpenAPICredentials { return agentOpenAPICredentials{ Account: strings.TrimSpace(c.Get(constants.AgentOpenAPIHeaderAccount)), Password: c.Get(constants.AgentOpenAPIHeaderPassword), Timestamp: strings.TrimSpace(c.Get(constants.AgentOpenAPIHeaderTimestamp)), Nonce: strings.TrimSpace(c.Get(constants.AgentOpenAPIHeaderNonce)), Sign: strings.TrimSpace(c.Get(constants.AgentOpenAPIHeaderSign)), } } func (c agentOpenAPICredentials) validateRequired() error { if c.Account == "" || c.Password == "" || c.Timestamp == "" || c.Nonce == "" || c.Sign == "" { return errors.New(errors.CodeOpenAPIAuthFailed) } return nil } func authenticateAgentAccountForOpenAPI(ctx context.Context, config AgentOpenAPIAuthConfig, credentials agentOpenAPICredentials) (*model.Account, error) { if config.AccountStore == nil { return nil, errors.New(errors.CodeInternalError, "开放接口认证依赖未配置") } account, err := config.AccountStore.GetByUsernameOrPhone(ctx, credentials.Account) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeOpenAPIAuthFailed) } return nil, errors.Wrap(errors.CodeInternalError, err, "查询开放接口账号失败") } if account.UserType != constants.UserTypeAgent || account.ShopID == nil || *account.ShopID == 0 { return nil, errors.New(errors.CodeForbidden, "开放接口仅允许代理账号调用") } if account.Status != constants.StatusEnabled { return nil, errors.New(errors.CodeOpenAPIAuthFailed) } if config.ShopStore == nil { return nil, errors.New(errors.CodeInternalError, "开放接口店铺认证依赖未配置") } shop, err := config.ShopStore.GetByID(ctx, *account.ShopID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeOpenAPIAuthFailed) } return nil, errors.Wrap(errors.CodeInternalError, err, "查询开放接口代理店铺失败") } if shop.Status != constants.StatusEnabled { return nil, errors.New(errors.CodeOpenAPIAuthFailed) } if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)); err != nil { return nil, errors.New(errors.CodeOpenAPIAuthFailed) } return account, nil } func validateAgentOpenAPITimestamp(raw string, now time.Time) error { requestTime, err := parseAgentOpenAPITimestamp(raw) if err != nil { return errors.New(errors.CodeOpenAPITimestampInvalid) } diff := now.Sub(requestTime) if diff < 0 { diff = -diff } if diff > constants.AgentOpenAPISignatureWindow { return errors.New(errors.CodeOpenAPITimestampInvalid) } return nil } func parseAgentOpenAPITimestamp(raw string) (time.Time, error) { if ts, err := strconv.ParseInt(raw, 10, 64); err == nil { if len(raw) == 13 { return time.UnixMilli(ts), nil } return time.Unix(ts, 0), nil } return time.Parse(time.RFC3339, raw) } func reserveAgentOpenAPINonce(ctx context.Context, redisClient *redis.Client, account, nonce string) error { if redisClient == nil { return errors.New(errors.CodeInternalError, "Redis 未配置") } key := constants.RedisAgentOpenAPINonceKey(account, nonce) ok, err := redisClient.SetNX(ctx, key, "1", constants.AgentOpenAPISignatureWindow).Result() if err != nil { return errors.Wrap(errors.CodeRedisError, err, "记录开放接口 nonce 失败") } if !ok { return errors.New(errors.CodeOpenAPINonceReplay) } return nil } func buildAgentOpenAPISignPayload(c *fiber.Ctx, credentials agentOpenAPICredentials) string { bodyHash := sha256.Sum256(c.Body()) parts := []string{ strings.ToUpper(c.Method()), c.Path(), canonicalAgentOpenAPIQuery(string(c.Request().URI().QueryString())), hex.EncodeToString(bodyHash[:]), credentials.Timestamp, credentials.Nonce, credentials.Account, } return strings.Join(parts, "\n") } func canonicalAgentOpenAPIQuery(rawQuery string) string { values, err := url.ParseQuery(rawQuery) if err != nil || len(values) == 0 { return "" } for key := range values { if strings.EqualFold(key, "sign") { delete(values, key) } } keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) sort.Strings(values[key]) } sort.Strings(keys) pairs := make([]string, 0, len(keys)) for _, key := range keys { for _, value := range values[key] { pairs = append(pairs, url.QueryEscape(key)+"="+url.QueryEscape(value)) } } return strings.Join(pairs, "&") } func verifyAgentOpenAPISign(payload, password, providedSign string) error { mac := hmac.New(sha256.New, []byte(password)) _, _ = mac.Write([]byte(payload)) expectedSign := hex.EncodeToString(mac.Sum(nil)) if subtle.ConstantTimeCompare([]byte(expectedSign), []byte(strings.ToLower(providedSign))) != 1 { return errors.New(errors.CodeOpenAPISignInvalid) } return nil } func injectAgentOpenAPIContext(c *fiber.Ctx, shopStore *postgres.ShopStore, account *model.Account) error { shopID := uint(0) if account.ShopID != nil { shopID = *account.ShopID } shopIDs := []uint{shopID} if shopStore != nil && shopID > 0 { var err error shopIDs, err = shopStore.GetSubordinateShopIDs(c.UserContext(), shopID) if err != nil || len(shopIDs) == 0 { shopIDs = []uint{shopID} } } info := &pkgmiddleware.UserContextInfo{ UserID: account.ID, UserType: constants.UserTypeAgent, Username: account.Username, ShopID: shopID, SubordinateShopIDs: shopIDs, } pkgmiddleware.SetUserToFiberContext(c, info) return nil }