fix(upstream): 鉴权中间件只更新 last_used_at 单列,限流 key 绑定来源 IP
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user