开放接口,修复上游同步不对的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
This commit is contained in:
249
internal/middleware/agent_open_api_auth.go
Normal file
249
internal/middleware/agent_open_api_auth.go
Normal file
@@ -0,0 +1,249 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user