优化结构
This commit is contained in:
@@ -2,11 +2,13 @@ package handler
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
// ProviderSet wires the system HTTP handlers.
|
||||
// ProviderSet wires the system HTTP handlers. Set itself is filled field by
|
||||
// field so adding a handler only means adding its constructor here.
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewAuthority, NewMenu, NewAPI, NewPermission, NewOrganization,
|
||||
NewAnnouncement, NewEmail, NewPayment, NewTask, NewMedia, NewAudit,
|
||||
NewExport, NewVersion, NewDictionary, NewParameter, NewAPIToken,
|
||||
NewSystemConfig, NewPublic, NewUser, NewNavigation, NewSession, NewSet,
|
||||
NewSystemConfig, NewPublic, NewUser, NewNavigation, NewSession,
|
||||
NewIntegrationConfig,
|
||||
wire.Struct(new(Set), "*"),
|
||||
)
|
||||
|
||||
@@ -24,7 +24,3 @@ type Set struct {
|
||||
Session *Session
|
||||
IntegrationConfig *IntegrationConfig
|
||||
}
|
||||
|
||||
func NewSet(authority *Authority, menu *Menu, api *API, permission *Permission, organization *Organization, announcement *Announcement, email *Email, payment *Payment, task *Task, media *Media, audit *Audit, export *Export, version *Version, dictionary *Dictionary, parameter *Parameter, apiToken *APIToken, systemConfig *SystemConfig, public *Public, user *User, navigation *Navigation, session *Session, integrationConfig *IntegrationConfig) *Set {
|
||||
return &Set{Authority: authority, Menu: menu, API: api, Permission: permission, Organization: organization, Announcement: announcement, Email: email, Payment: payment, Task: task, Media: media, Audit: audit, Export: export, Version: version, Dictionary: dictionary, Parameter: parameter, APIToken: apiToken, SystemConfig: systemConfig, Public: public, User: user, Navigation: navigation, Session: session, IntegrationConfig: integrationConfig}
|
||||
}
|
||||
|
||||
@@ -142,17 +142,28 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
|
||||
"req_query", redactQuery(c.Request.URL.RawQuery),
|
||||
}
|
||||
}
|
||||
if !paymentCallback && admin != nil && admin.Zap != nil && admin.Zap.AccessReqHeaders {
|
||||
attributes = append(attributes, "req_headers", redactHeaders(c.Request.Header))
|
||||
}
|
||||
if !paymentCallback && admin != nil && admin.Zap != nil && admin.Zap.AccessReqBody {
|
||||
attributes = append(attributes, "req_body", requestText)
|
||||
}
|
||||
if !paymentCallback && admin != nil && admin.Zap != nil && admin.Zap.AccessRespData {
|
||||
attributes = append(attributes, "resp_data", responseText)
|
||||
}
|
||||
if !paymentCallback && privateErrors != "" {
|
||||
attributes = append(attributes, "error_msg", privateErrors)
|
||||
// A payment callback is logged through its own attribute set: neither the
|
||||
// configurable request/response capture nor the private error text may leak
|
||||
// provider payloads into the generic access log.
|
||||
if !paymentCallback {
|
||||
var zap *config.Zap
|
||||
if admin != nil {
|
||||
zap = admin.Zap
|
||||
}
|
||||
if zap != nil {
|
||||
if zap.AccessReqHeaders {
|
||||
attributes = append(attributes, "req_headers", redactHeaders(c.Request.Header))
|
||||
}
|
||||
if zap.AccessReqBody {
|
||||
attributes = append(attributes, "req_body", requestText)
|
||||
}
|
||||
if zap.AccessRespData {
|
||||
attributes = append(attributes, "resp_data", responseText)
|
||||
}
|
||||
}
|
||||
if privateErrors != "" {
|
||||
attributes = append(attributes, "error_msg", privateErrors)
|
||||
}
|
||||
}
|
||||
logger.InfoContext(c.Request.Context(), "请求完成", attributes...)
|
||||
}
|
||||
@@ -173,12 +184,6 @@ func paymentCallbackProvider(path string) string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// isPaymentIntegrationConfigWrite is kept as a compatibility seam for
|
||||
// middleware tests; policy ownership lives in routecatalog.
|
||||
func isPaymentIntegrationConfigWrite(method, path string) bool {
|
||||
return routecatalog.BodyPolicyFor(method, path) == routecatalog.BodyPolicyPaymentConfig
|
||||
}
|
||||
|
||||
func paymentCallbackSummary(body []byte, contentType string) string {
|
||||
mediaType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil || mediaType == "" {
|
||||
|
||||
@@ -135,12 +135,24 @@ func operationRequestBody(raw []byte, contentType string, limit int) string {
|
||||
return text
|
||||
}
|
||||
|
||||
// operationSecretKeys holds the normalized JSON keys whose values never reach
|
||||
// the operation record. Keys are compared after lowercasing and stripping
|
||||
// separators, so "new_password" and "newPassword" both match "newpassword".
|
||||
var operationSecretKeys = map[string]struct{}{
|
||||
"password": {}, "newpassword": {}, "oldpassword": {}, "confirmpassword": {},
|
||||
"passwd": {}, "pwd": {}, "token": {}, "accesstoken": {}, "refreshtoken": {},
|
||||
"secret": {}, "clientsecret": {}, "apikey": {}, "privatekey": {}, "idcard": {},
|
||||
"appkey": {}, "mchkey": {}, "apiv3key": {}, "clientcert": {}, "clientkey": {},
|
||||
"platformcert": {}, "platformserialno": {}, "credentialcode": {}, "certfile": {},
|
||||
"keyfile": {}, "publickey": {}, "rootcert": {}, "appcert": {}, "webhookid": {},
|
||||
}
|
||||
|
||||
func maskOperationBody(value any) {
|
||||
switch current := value.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range current {
|
||||
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
|
||||
if normalized == "password" || normalized == "newpassword" || normalized == "oldpassword" || normalized == "confirmpassword" || normalized == "passwd" || normalized == "pwd" || normalized == "token" || normalized == "accesstoken" || normalized == "refreshtoken" || normalized == "secret" || normalized == "clientsecret" || normalized == "apikey" || normalized == "privatekey" || normalized == "idcard" || normalized == "appkey" || normalized == "mchkey" || normalized == "apiv3key" || normalized == "clientcert" || normalized == "clientkey" || normalized == "platformcert" || normalized == "platformserialno" || normalized == "credentialcode" || normalized == "certfile" || normalized == "keyfile" || normalized == "publickey" || normalized == "rootcert" || normalized == "appcert" || normalized == "webhookid" {
|
||||
if _, secret := operationSecretKeys[normalized]; secret {
|
||||
current[key] = "***"
|
||||
continue
|
||||
}
|
||||
@@ -153,21 +165,26 @@ func maskOperationBody(value any) {
|
||||
}
|
||||
}
|
||||
|
||||
func isDownloadResponse(c *gin.Context) bool {
|
||||
header := c.Writer.Header()
|
||||
return strings.Contains(header.Get("Pragma"), "public") ||
|
||||
strings.Contains(header.Get("Expires"), "0") ||
|
||||
strings.Contains(header.Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/force-download") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/octet-stream") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/vnd.ms-excel") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/download") ||
|
||||
strings.Contains(header.Get("Content-Disposition"), "attachment") ||
|
||||
strings.Contains(header.Get("Content-Transfer-Encoding"), "binary")
|
||||
// operationDownloadHeaders lists the response headers whose presence marks a
|
||||
// download: the value is the substring that identifies it.
|
||||
var operationDownloadHeaders = [...]struct{ header, marker string }{
|
||||
{"Pragma", "public"},
|
||||
{"Expires", "0"},
|
||||
{"Cache-Control", "must-revalidate, post-check=0, pre-check=0"},
|
||||
{"Content-Type", "application/force-download"},
|
||||
{"Content-Type", "application/octet-stream"},
|
||||
{"Content-Type", "application/vnd.ms-excel"},
|
||||
{"Content-Type", "application/download"},
|
||||
{"Content-Disposition", "attachment"},
|
||||
{"Content-Transfer-Encoding", "binary"},
|
||||
}
|
||||
|
||||
// recordsOperation remains as a small compatibility helper for tests and
|
||||
// custom middleware chains that do not have a Gin context.
|
||||
func recordsOperation(method, path string) bool {
|
||||
return routecatalog.ShouldAudit(method, path)
|
||||
func isDownloadResponse(c *gin.Context) bool {
|
||||
header := c.Writer.Header()
|
||||
for _, candidate := range operationDownloadHeaders {
|
||||
if strings.Contains(header.Get(candidate.header), candidate.marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/sync/singleflight"
|
||||
@@ -14,6 +15,10 @@ import (
|
||||
|
||||
const claimsKey = "admin_claims"
|
||||
|
||||
// tokenAuthTimeout bounds a shared token authentication flight. It replaces the
|
||||
// leader request's own deadline, which followers must not inherit.
|
||||
const tokenAuthTimeout = 10 * time.Second
|
||||
|
||||
var refreshTokens singleflight.Group
|
||||
|
||||
type TokenAuthenticator interface {
|
||||
@@ -36,7 +41,7 @@ func AuthenticateWebSocket(c *gin.Context, auth TokenAuthenticator) bool {
|
||||
}
|
||||
|
||||
func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool) bool {
|
||||
token := requestToken(c, allowQueryToken)
|
||||
token := RequestToken(c, allowQueryToken)
|
||||
if token == "" {
|
||||
NoAuth(c, "未登录或非法访问,请登录")
|
||||
return false
|
||||
@@ -45,8 +50,15 @@ func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool)
|
||||
NoAuth(c, "认证服务不可用")
|
||||
return false
|
||||
}
|
||||
// Followers of a singleflight flight must not inherit the leader's request
|
||||
// context: if the leader's client disconnects mid-flight, its cancellation
|
||||
// would surface as an auth failure for every follower and force-log-out
|
||||
// unrelated sessions. Detach cancellation and bound the shared call with an
|
||||
// explicit timeout instead.
|
||||
value, err, _ := refreshTokens.Do(token, func() (any, error) {
|
||||
return auth.AuthenticateToken(c.Request.Context(), token)
|
||||
ctx, cancel := context.WithTimeout(context.WithoutCancel(c.Request.Context()), tokenAuthTimeout)
|
||||
defer cancel()
|
||||
return auth.AuthenticateToken(ctx, token)
|
||||
})
|
||||
if err != nil {
|
||||
SetTokenCookie(c, "", -1)
|
||||
@@ -93,10 +105,6 @@ func RequestToken(c *gin.Context, allowQueryToken bool) string {
|
||||
return token
|
||||
}
|
||||
|
||||
func requestToken(c *gin.Context, allowQueryToken bool) string {
|
||||
return RequestToken(c, allowQueryToken)
|
||||
}
|
||||
|
||||
func tokenErrorMessage(err error) string {
|
||||
message := "无法处理此token"
|
||||
switch {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/routecatalog"
|
||||
)
|
||||
|
||||
func TestPaymentIntegrationSecretsAreRedacted(t *testing.T) {
|
||||
@@ -35,7 +37,7 @@ func TestPaymentOperationsAreAuditedWithRouterPrefix(t *testing.T) {
|
||||
{method: "POST", path: "/api/payment/fulfill"},
|
||||
{method: "POST", path: "/api/payment/providers/alipay/test"},
|
||||
} {
|
||||
if !recordsOperation(route.method, route.path) {
|
||||
if !routecatalog.ShouldAudit(route.method, route.path) {
|
||||
t.Fatalf("payment operation was not audited: %s %s", route.method, route.path)
|
||||
}
|
||||
}
|
||||
@@ -47,10 +49,10 @@ func TestPaymentIntegrationConfigUsesRouteLevelSummary(t *testing.T) {
|
||||
if strings.Contains(summary, "secret") || strings.Contains(summary, "certificate") {
|
||||
t.Fatalf("payment configuration summary leaked payload: %s", summary)
|
||||
}
|
||||
if !isPaymentIntegrationConfigWrite("PUT", "/api/integration/configs/payment/saobei") {
|
||||
if routecatalog.BodyPolicyFor("PUT", "/api/integration/configs/payment/saobei") != routecatalog.BodyPolicyPaymentConfig {
|
||||
t.Fatal("payment configuration write route was not recognized")
|
||||
}
|
||||
if isPaymentIntegrationConfigWrite("PUT", "/api/integration/configs/mq/emqx") {
|
||||
if routecatalog.BodyPolicyFor("PUT", "/api/integration/configs/mq/emqx") == routecatalog.BodyPolicyPaymentConfig {
|
||||
t.Fatal("non-payment integration was treated as payment configuration")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user