fix(upstream): 鉴权中间件只更新 last_used_at 单列,限流 key 绑定来源 IP

This commit is contained in:
dujiao-next committed 2026-09-15 22:17:40 +08:00
1 parent f517f12b53
commit 9f8579ca93
4 files changed
+40 -15

No files matched your search

@@ -247,13 +247,24 @@ func KeyByUserIDAndIP(c *gin.Context) string {
return fmt.Sprintf("%v|%s", userID, c.ClientIP())
}
// KeyByUpstreamApiKey 使用上游 API Key 作为限流 key
func KeyByUpstreamApiKey(c *gin.Context) string {
apiKey := c.GetHeader("Dujiao-Next-Api-Key")
if apiKey != "" {
return apiKey
// KeyByIPAndHeader 以 "IP|header 值" 作为限流 key。
// 限流位于鉴权之前,header 未经校验,因此计数必须同时绑定来源 IP。
func KeyByIPAndHeader(header string) RateLimitKeyFunc {
return func(c *gin.Context) string {
value := c.GetHeader(header)
if len(value) > 128 {
value = value[:128]
}
if value == "" {
return c.ClientIP()
}
return c.ClientIP() + "|" + value
}
return c.ClientIP()
}
// KeyByUpstreamApiKey 使用 "IP|上游 API Key" 作为限流 key
func KeyByUpstreamApiKey(c *gin.Context) string {
return KeyByIPAndHeader("Dujiao-Next-Api-Key")(c)
}
// KeyByIPAndJSONField 使用 IP + JSON 字段作为限流 key
@@ -20,7 +20,7 @@ const upstreamCredentialIDKey = "upstream_credential_id"
// UpstreamCredentialStore 只暴露签名鉴权链路所需的凭证能力。
type UpstreamCredentialStore interface {
GetByApiKey(apiKey string) (*apicredentialdomain.ApiCredential, error)
Update(credential *apicredentialdomain.ApiCredential) error
TouchLastUsedAt(id uint, at time.Time) error
}
// UpstreamAPIAuthMiddleware 上游 API 签名鉴权中间件
@@ -84,14 +84,16 @@ func UpstreamAPIAuthMiddleware(credRepo UpstreamCredentialStore) gin.HandlerFunc
return
}
// 更新最后使用时间(异步,不阻塞请求)
// 更新最后使用时间(异步、只写单列、60s 节流)
now := time.Now()
cred.LastUsedAt = &now
go func() {
if updateErr := credRepo.Update(cred); updateErr != nil {
logger.Warnw("upstream_auth_update_last_used_failed", "error", updateErr)
}
}()
if cred.LastUsedAt == nil || now.Sub(*cred.LastUsedAt) > time.Minute {
credID := cred.ID
go func() {
if updateErr := credRepo.TouchLastUsedAt(credID, now); updateErr != nil {
logger.Warnw("upstream_auth_update_last_used_failed", "error", updateErr)
}
}()
}
// 将凭证信息存入 context
c.Set(upstreamUserIDKey, cred.UserID)
@@ -1,6 +1,10 @@
package contract
import apicredentialdomain "github.com/dujiao-next/internal/modules/apicredential/domain"
import (
"time"
apicredentialdomain "github.com/dujiao-next/internal/modules/apicredential/domain"
)
type ListFilter struct {
Status string
@@ -17,6 +21,7 @@ type Repository interface {
GetByApiKey(apiKey string) (*apicredentialdomain.ApiCredential, error)
Create(credential *apicredentialdomain.ApiCredential) error
Update(credential *apicredentialdomain.ApiCredential) error
TouchLastUsedAt(id uint, at time.Time) error
UpdateAny(credential *apicredentialdomain.ApiCredential) error
Delete(id uint) error
List(filter ListFilter) ([]apicredentialdomain.ApiCredential, int64, error)
@@ -76,6 +76,13 @@ func (r *Store) Update(cred *apicredentialdomain.ApiCredential) error {
return r.db.Save(cred).Error
}
// TouchLastUsedAt 只更新 last_used_at 单列,不覆盖其它字段。
func (r *Store) TouchLastUsedAt(id uint, at time.Time) error {
return r.db.Model(&apicredentialdomain.ApiCredential{}).
Where("id = ? AND deleted_at IS NULL", id).
UpdateColumn("last_used_at", at).Error
}
// UpdateAny 更新凭证,包含软删除记录。
func (r *Store) UpdateAny(cred *apicredentialdomain.ApiCredential) error {
return r.db.Save(cred).Error