Files
junhong_cmp_fiber/internal/middleware/agent_open_api_auth.go
huang 98ff88d5c3
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
开放接口,修复上游同步不对的问题
2026-05-11 11:20:48 +08:00

250 lines
7.3 KiB
Go

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
}