Files
dujiao-next/internal/app/httpserver/middleware/channel_auth.go
T

102 lines
3.7 KiB
Go

package middleware
import (
"io"
"net/http"
"time"
"github.com/dujiao-next/internal/app/container"
"github.com/dujiao-next/internal/i18n"
"github.com/dujiao-next/internal/logger"
channelclientapp "github.com/dujiao-next/internal/modules/channelclient/application"
"github.com/dujiao-next/internal/platform/http/response"
"github.com/dujiao-next/internal/upstream"
"github.com/gin-gonic/gin"
)
const (
channelClientIDKey = "channel_client_id"
channelKeyCtxKey = "channel_key"
channelTypeCtxKey = "channel_type"
channelHeaderKey = "Dujiao-Next-Channel-Key"
channelHeaderTimestamp = "Dujiao-Next-Channel-Timestamp"
channelHeaderSignature = "Dujiao-Next-Channel-Signature"
)
// ChannelAPIAuthMiddleware 渠道 API 签名鉴权中间件
func ChannelAPIAuthMiddleware(container *container.Container) gin.HandlerFunc {
return func(c *gin.Context) {
channelKey := c.GetHeader(channelHeaderKey)
timestampStr := c.GetHeader(channelHeaderTimestamp)
signature := c.GetHeader(channelHeaderSignature)
if channelKey == "" || timestampStr == "" || signature == "" {
response.ChannelError(c, http.StatusUnauthorized, response.CodeUnauthorized, i18n.T(i18n.ResolveLocale(c), "error.unauthorized"), "channel_client_unauthorized")
c.Abort()
return
}
timestamp, err := upstream.ParseTimestamp(timestampStr)
if err != nil {
response.ChannelError(c, http.StatusUnauthorized, response.CodeUnauthorized, i18n.T(i18n.ResolveLocale(c), "error.unauthorized"), "channel_client_unauthorized")
c.Abort()
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 {
response.ChannelError(c, http.StatusBadRequest, response.CodeBadRequest, i18n.T(i18n.ResolveLocale(c), "error.bad_request"), "validation_error")
c.Abort()
return
}
// 重置 body 供后续 handler 读取
c.Request.Body = io.NopCloser(&bodyReader{data: body})
}
method := c.Request.Method
path := c.Request.URL.Path
client, err := container.ChannelClientService.VerifyChannelSignature(
channelKey, signature, timestamp, method, path, body,
)
if err != nil {
switch err {
case channelclientapp.ErrTimestampExpired:
response.ChannelError(c, http.StatusUnauthorized, response.CodeUnauthorized, i18n.T(i18n.ResolveLocale(c), "error.unauthorized"), "channel_client_unauthorized")
case channelclientapp.ErrNotFound:
response.ChannelError(c, http.StatusUnauthorized, response.CodeUnauthorized, i18n.T(i18n.ResolveLocale(c), "error.unauthorized"), "channel_client_unauthorized")
case channelclientapp.ErrDisabled:
response.ChannelError(c, http.StatusForbidden, response.CodeForbidden, i18n.T(i18n.ResolveLocale(c), "error.forbidden"), "channel_client_disabled")
case channelclientapp.ErrSignatureInvalid:
response.ChannelError(c, http.StatusUnauthorized, response.CodeUnauthorized, i18n.T(i18n.ResolveLocale(c), "error.unauthorized"), "channel_client_unauthorized")
default:
logger.Errorw("channel_auth_error", "error", err)
response.ChannelError(c, http.StatusInternalServerError, response.CodeInternal, i18n.T(i18n.ResolveLocale(c), "error.internal_error"), "internal_error")
}
c.Abort()
return
}
// 异步更新 last_used_at
now := time.Now()
go func() {
if updateErr := container.ChannelClientService.MarkUsed(client.ID, now); updateErr != nil {
logger.Warnw("channel_auth_update_last_used_failed", "error", updateErr)
}
}()
// 设置 context keys
c.Set(channelClientIDKey, client.ID)
c.Set(channelKeyCtxKey, client.ChannelKey)
c.Set(channelTypeCtxKey, client.ChannelType)
c.Next()
}
}