优化结构
This commit is contained in:
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/system"
|
||||
httpx "kra/pkg/httpx"
|
||||
|
||||
"kra/internal/config"
|
||||
"kra/internal/utils/routepath"
|
||||
@@ -19,11 +20,11 @@ func AccessControl(runtime *config.Store, access accessController) gin.HandlerFu
|
||||
return func(c *gin.Context) {
|
||||
claims := Claims(c)
|
||||
if claims == nil {
|
||||
NoAuth(c, "未登录或非法访问")
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
if access == nil {
|
||||
Write(c, CodeError, gin.H{}, "权限服务不可用")
|
||||
httpx.Write(c, httpx.CodeError, gin.H{}, "权限服务不可用")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
@@ -36,13 +37,13 @@ func AccessControl(runtime *config.Store, access accessController) gin.HandlerFu
|
||||
}
|
||||
allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, policyPath, c.Request.Method)
|
||||
if err != nil || !allowed {
|
||||
Write(c, CodeError, gin.H{}, "权限不足")
|
||||
httpx.Write(c, httpx.CodeError, gin.H{}, "权限不足")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
|
||||
if err != nil {
|
||||
Write(c, CodeError, gin.H{}, "数据权限解析失败")
|
||||
httpx.Write(c, httpx.CodeError, gin.H{}, "数据权限解析失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"kra/internal/biz/system"
|
||||
httpx "kra/pkg/httpx"
|
||||
"log/slog"
|
||||
"mime"
|
||||
"net/http"
|
||||
@@ -51,7 +52,7 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
|
||||
c.Header("X-Kra-Version", version)
|
||||
requestReadFailed := c.Request.ContentLength > bodyLimit
|
||||
if requestReadFailed {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": CodeError, "msg": "请求体超过大小上限"})
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
|
||||
} else if c.Request.Body != nil && !mediaUpload {
|
||||
limited := http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
|
||||
var err error
|
||||
@@ -60,9 +61,9 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
|
||||
requestReadFailed = true
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": CodeError, "msg": "请求体超过大小上限"})
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": CodeError, "msg": "请求体读取失败"})
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": httpx.CodeError, "msg": "请求体读取失败"})
|
||||
}
|
||||
} else {
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
@@ -204,9 +205,8 @@ func paymentConfigSummary(body []byte) string {
|
||||
func redactHeaders(headers map[string][]string) map[string]string {
|
||||
out := make(map[string]string, len(headers))
|
||||
for key, values := range headers {
|
||||
lower := strings.ToLower(key)
|
||||
if lower == "authorization" || lower == "cookie" || lower == "set-cookie" || lower == "x-token" {
|
||||
out[key] = "***"
|
||||
if isSensitiveHeader(key) {
|
||||
out[key] = redactedValue
|
||||
} else {
|
||||
out[key] = strings.Join(values, ",")
|
||||
}
|
||||
@@ -220,23 +220,13 @@ func redactQuery(raw string) string {
|
||||
}
|
||||
values, _ := url.ParseQuery(raw)
|
||||
for key := range values {
|
||||
if sensitiveQueryKey(key) {
|
||||
values[key] = []string{"***"}
|
||||
if isSensitivePayloadKey(key) {
|
||||
values[key] = []string{redactedValue}
|
||||
}
|
||||
}
|
||||
return values.Encode()
|
||||
}
|
||||
|
||||
func sensitiveQueryKey(key string) bool {
|
||||
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
|
||||
switch normalized {
|
||||
case "token", "accesstoken", "refreshtoken", "authorization", "apikey", "secret":
|
||||
return true
|
||||
default:
|
||||
return strings.HasSuffix(normalized, "token")
|
||||
}
|
||||
}
|
||||
|
||||
func stringValueFromContext(c *gin.Context, key string) string {
|
||||
value, _ := c.Get(key)
|
||||
return stringValue(value)
|
||||
|
||||
@@ -135,25 +135,12 @@ 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 _, secret := operationSecretKeys[normalized]; secret {
|
||||
current[key] = "***"
|
||||
if isSensitivePayloadKey(key) {
|
||||
current[key] = redactedValue
|
||||
continue
|
||||
}
|
||||
maskOperationBody(item)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"kra/internal/biz/system"
|
||||
httpx "kra/pkg/httpx"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -43,11 +44,11 @@ func AuthenticateWebSocket(c *gin.Context, auth TokenAuthenticator) bool {
|
||||
func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool) bool {
|
||||
token := RequestToken(c, allowQueryToken)
|
||||
if token == "" {
|
||||
NoAuth(c, "未登录或非法访问,请登录")
|
||||
httpx.NoAuth(c, "未登录或非法访问,请登录")
|
||||
return false
|
||||
}
|
||||
if auth == nil {
|
||||
NoAuth(c, "认证服务不可用")
|
||||
httpx.NoAuth(c, "认证服务不可用")
|
||||
return false
|
||||
}
|
||||
// Followers of a singleflight flight must not inherit the leader's request
|
||||
@@ -61,20 +62,20 @@ func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool)
|
||||
return auth.AuthenticateToken(ctx, token)
|
||||
})
|
||||
if err != nil {
|
||||
SetTokenCookie(c, "", -1)
|
||||
NoAuth(c, tokenErrorMessage(err))
|
||||
httpx.SetTokenCookie(c, "", -1)
|
||||
httpx.NoAuth(c, tokenErrorMessage(err))
|
||||
return false
|
||||
}
|
||||
authentication, ok := value.(*system.TokenAuthentication)
|
||||
if !ok || authentication == nil || authentication.Claims == nil {
|
||||
SetTokenCookie(c, "", -1)
|
||||
NoAuth(c, "无法处理此token")
|
||||
httpx.SetTokenCookie(c, "", -1)
|
||||
httpx.NoAuth(c, "无法处理此token")
|
||||
return false
|
||||
}
|
||||
if authentication.Refreshed != nil {
|
||||
c.Header("new-token", authentication.Refreshed.Value)
|
||||
c.Header("new-expires-at", strconv.FormatInt(authentication.Refreshed.ExpiresAt.Unix(), 10))
|
||||
SetTokenCookie(c, authentication.Refreshed.Value, int(authentication.Refreshed.TTL.Seconds()))
|
||||
httpx.SetTokenCookie(c, authentication.Refreshed.Value, int(authentication.Refreshed.TTL.Seconds()))
|
||||
}
|
||||
c.Set(claimsKey, authentication.Claims)
|
||||
return true
|
||||
@@ -140,6 +141,6 @@ func MustChangePassword() gin.HandlerFunc {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusConflict, Response{Code: CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
|
||||
c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
httpx "kra/pkg/httpx"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
@@ -24,7 +25,7 @@ func ErrorAudit(logger *slog.Logger) gin.HandlerFunc {
|
||||
if auditPersistFailed != true && (strings.Contains(c.Request.URL.Path, "/sysError/") || strings.Contains(c.Request.URL.Path, "/logViewer/") || c.Writer.Status() >= 300 && c.Writer.Status() < 500) {
|
||||
return
|
||||
}
|
||||
var response Response
|
||||
var response httpx.Response
|
||||
var body []byte
|
||||
if value, ok := c.Get(ctxRespBufferKey); ok {
|
||||
if buffer, valid := value.(*bytes.Buffer); valid {
|
||||
@@ -34,7 +35,7 @@ func ErrorAudit(logger *slog.Logger) gin.HandlerFunc {
|
||||
if json.Unmarshal(body, &response) != nil && privateErrors == "" {
|
||||
return
|
||||
}
|
||||
if response.Code == CodeSuccess && privateErrors == "" {
|
||||
if response.Code == httpx.CodeSuccess && privateErrors == "" {
|
||||
return
|
||||
}
|
||||
if privateErrors == "" && expectedClientFailure(response.Msg) {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
httpx "kra/pkg/httpx"
|
||||
)
|
||||
|
||||
const (
|
||||
CodeSuccess = httpx.CodeSuccess
|
||||
CodeError = httpx.CodeError
|
||||
CodePasswordChangeRequired = httpx.CodePasswordChangeRequired
|
||||
)
|
||||
|
||||
type Response = httpx.Response
|
||||
|
||||
func Write(c *gin.Context, code int, data any, message string) { httpx.Write(c, code, data, message) }
|
||||
func NoAuth(c *gin.Context, message string) { httpx.NoAuth(c, message) }
|
||||
func SetTokenCookie(c *gin.Context, value string, maxAge int) { httpx.SetTokenCookie(c, value, maxAge) }
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"kra/internal/service"
|
||||
httpx "kra/pkg/httpx"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -22,7 +23,7 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
|
||||
}
|
||||
config, err := settings.CurrentSecurity(c.Request.Context())
|
||||
if err != nil || config == nil {
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
|
||||
return
|
||||
}
|
||||
if !config.LimitEnable {
|
||||
@@ -40,11 +41,11 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
|
||||
key := "KRA_SecLimit:" + c.ClientIP() + ":" + route
|
||||
count, cacheErr := settings.IncrementRateLimit(c.Request.Context(), key, time.Duration(window)*time.Second)
|
||||
if cacheErr != nil {
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
|
||||
return
|
||||
}
|
||||
if int(count) > config.LimitCount {
|
||||
c.JSON(200, gin.H{"code": CodeError, "msg": "请求太过频繁,请稍后再试"})
|
||||
c.JSON(200, gin.H{"code": httpx.CodeError, "msg": "请求太过频繁,请稍后再试"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"kra/internal/service"
|
||||
systemservice "kra/internal/service/system"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -38,7 +38,7 @@ func (c rateLimitCache) Increment(context.Context, string, time.Duration) (int64
|
||||
|
||||
func TestSecurityRateLimitMatchesResponseContract(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
settings := service.NewSecurityService(system.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{}, nil, nil))
|
||||
settings := systemservice.NewSecurityService(system.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{}, nil, nil))
|
||||
engine := gin.New()
|
||||
engine.Use(SecurityRateLimit(settings))
|
||||
engine.POST("/base/login", func(c *gin.Context) { c.Status(http.StatusNoContent) })
|
||||
@@ -56,7 +56,7 @@ func TestSecurityRateLimitMatchesResponseContract(t *testing.T) {
|
||||
|
||||
func TestSecurityRateLimitFailsClosedWhenCacheIsUnavailable(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
settings := service.NewSecurityService(system.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{err: errors.New("cache unavailable")}, nil, nil))
|
||||
settings := systemservice.NewSecurityService(system.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{err: errors.New("cache unavailable")}, nil, nil))
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(SecurityRateLimit(settings))
|
||||
|
||||
@@ -64,9 +64,8 @@ func recoveryRequestDump(request *http.Request) string {
|
||||
clone := request.Clone(request.Context())
|
||||
clone.Header = request.Header.Clone()
|
||||
for key := range clone.Header {
|
||||
switch strings.ToLower(key) {
|
||||
case "authorization", "cookie", "set-cookie", "x-token", "proxy-authorization":
|
||||
clone.Header[key] = []string{"***"}
|
||||
if isSensitiveHeader(key) {
|
||||
clone.Header[key] = []string{redactedValue}
|
||||
}
|
||||
}
|
||||
if request.URL != nil {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package middleware
|
||||
|
||||
import "strings"
|
||||
|
||||
// redactedValue replaces a sensitive value in every log line and audit record.
|
||||
const redactedValue = "***"
|
||||
|
||||
// sensitiveHeaders lists the headers whose value never reaches a log, compared
|
||||
// after lowercasing.
|
||||
var sensitiveHeaders = map[string]struct{}{
|
||||
"authorization": {}, "proxy-authorization": {}, "cookie": {}, "set-cookie": {}, "x-token": {},
|
||||
}
|
||||
|
||||
// sensitivePayloadKeys holds the normalized query and JSON body keys whose
|
||||
// values never reach a log or an operation record. Keys are compared after
|
||||
// lowercasing and stripping separators, so "new_password" and "newPassword"
|
||||
// both match "newpassword".
|
||||
var sensitivePayloadKeys = 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": {},
|
||||
"authorization": {},
|
||||
}
|
||||
|
||||
func isSensitiveHeader(key string) bool {
|
||||
_, sensitive := sensitiveHeaders[strings.ToLower(key)]
|
||||
return sensitive
|
||||
}
|
||||
|
||||
// isSensitivePayloadKey also matches every key ending in "token" so provider
|
||||
// specific token names stay masked without being enumerated.
|
||||
func isSensitivePayloadKey(key string) bool {
|
||||
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
|
||||
if _, sensitive := sensitivePayloadKeys[normalized]; sensitive {
|
||||
return true
|
||||
}
|
||||
return strings.HasSuffix(normalized, "token")
|
||||
}
|
||||
Reference in New Issue
Block a user