119 lines
3.9 KiB
Go
119 lines
3.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
apicredentialdomain "github.com/dujiao-next/internal/modules/apicredential/domain"
|
|
|
|
"github.com/dujiao-next/internal/constants"
|
|
"github.com/dujiao-next/internal/logger"
|
|
"github.com/dujiao-next/internal/upstream"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const upstreamUserIDKey = "upstream_user_id"
|
|
const upstreamCredentialIDKey = "upstream_credential_id"
|
|
|
|
// UpstreamCredentialStore 只暴露签名鉴权链路所需的凭证能力。
|
|
type UpstreamCredentialStore interface {
|
|
GetByApiKey(apiKey string) (*apicredentialdomain.ApiCredential, error)
|
|
Update(credential *apicredentialdomain.ApiCredential) error
|
|
}
|
|
|
|
// UpstreamAPIAuthMiddleware 上游 API 签名鉴权中间件
|
|
func UpstreamAPIAuthMiddleware(credRepo UpstreamCredentialStore) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
apiKey := c.GetHeader(upstream.HeaderApiKey)
|
|
timestampStr := c.GetHeader(upstream.HeaderTimestamp)
|
|
signature := c.GetHeader(upstream.HeaderSignature)
|
|
|
|
if apiKey == "" || timestampStr == "" || signature == "" {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"ok": false, "error_code": "missing_auth_headers", "error_message": "missing authentication headers"})
|
|
return
|
|
}
|
|
|
|
timestamp, err := upstream.ParseTimestamp(timestampStr)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"ok": false, "error_code": "invalid_timestamp", "error_message": "invalid timestamp"})
|
|
return
|
|
}
|
|
|
|
if !upstream.IsTimestampValid(timestamp) {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"ok": false, "error_code": "timestamp_expired", "error_message": "timestamp expired"})
|
|
return
|
|
}
|
|
|
|
cred, err := credRepo.GetByApiKey(apiKey)
|
|
if err != nil {
|
|
logger.Errorw("upstream_auth_db_error", "error", err)
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"ok": false, "error_code": "internal_error", "error_message": "internal error"})
|
|
return
|
|
}
|
|
if cred == nil || cred.Status != constants.ApiCredentialStatusApproved || !cred.IsActive {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"ok": false, "error_code": "invalid_api_key", "error_message": "api key is invalid or disabled"})
|
|
return
|
|
}
|
|
if cred.User == nil || cred.User.Status != constants.UserStatusActive {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"ok": false, "error_code": "user_disabled", "error_message": "user account is disabled"})
|
|
return
|
|
}
|
|
|
|
// 读取 body 用于签名验证(限制最大 10MB 防止内存耗尽)
|
|
var body []byte
|
|
if c.Request.Body != nil {
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10<<20)
|
|
body, err = io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"ok": false, "error_code": "bad_request", "error_message": "failed to read request body"})
|
|
return
|
|
}
|
|
// 重置 body 供后续 handler 读取
|
|
c.Request.Body = io.NopCloser(
|
|
&bodyReader{data: body},
|
|
)
|
|
}
|
|
|
|
method := c.Request.Method
|
|
path := c.Request.URL.Path
|
|
|
|
if !upstream.Verify(cred.ApiSecret, method, path, signature, timestamp, body) {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"ok": false, "error_code": "invalid_signature", "error_message": "signature verification failed"})
|
|
return
|
|
}
|
|
|
|
// 更新最后使用时间(异步,不阻塞请求)
|
|
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)
|
|
}
|
|
}()
|
|
|
|
// 将凭证信息存入 context
|
|
c.Set(upstreamUserIDKey, cred.UserID)
|
|
c.Set(upstreamCredentialIDKey, cred.ID)
|
|
c.Set("upstream_api_key", cred.ApiKey)
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// bodyReader 实现 io.Reader,用于重置 body
|
|
type bodyReader struct {
|
|
data []byte
|
|
offset int
|
|
}
|
|
|
|
func (r *bodyReader) Read(p []byte) (n int, err error) {
|
|
if r.offset >= len(r.data) {
|
|
return 0, io.EOF
|
|
}
|
|
n = copy(p, r.data[r.offset:])
|
|
r.offset += n
|
|
return n, nil
|
|
}
|