优化结构

This commit is contained in:
Yvan
2026-08-25 00:20:29 +08:00
parent 969afee284
commit ee179d6b34
49 changed files with 289 additions and 591 deletions
+5 -4
View File
@@ -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
}
+8 -18
View File
@@ -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)
+2 -15
View File
@@ -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)
+9 -8
View File
@@ -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 -2
View File
@@ -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) {
-18
View File
@@ -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) }
+4 -3
View File
@@ -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))
+2 -3
View File
@@ -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 {
+41
View File
@@ -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")
}