优化结构

This commit is contained in:
Yvan
2026-08-22 14:12:00 +08:00
parent b27a66e9d3
commit 9dd62c9fb6
37 changed files with 352 additions and 130 deletions
+5
View File
@@ -22,6 +22,11 @@ func AccessControl(runtime *conf.Runtime, access accessController) gin.HandlerFu
NoAuth(c, "未登录或非法访问")
return
}
if access == nil {
Write(c, CodeError, gin.H{}, "权限服务不可用")
c.Abort()
return
}
path := c.Request.URL.Path
policyPath := path
if config := runtime.Admin(); config != nil {
+4 -1
View File
@@ -30,7 +30,10 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.H
var requestBody []byte
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
mediaUpload := multipart && isMediaUploadRoute(c.FullPath())
config := runtime.Admin()
var config *conf.AdminBackend
if runtime != nil {
config = runtime.Admin()
}
bodyLimit := defaultRequestBodyLimit
if mediaUpload {
bodyLimit = system.DefaultMaxMediaFileSize + (1 << 20)
+9 -23
View File
@@ -23,6 +23,10 @@ const ctxOperationAuditPersistFailedKey = "operation_audit_persist_failed"
func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.HandlerFunc {
return func(c *gin.Context) {
if runtime == nil || recorder == nil {
c.Next()
return
}
path := c.Request.URL.Path
if !recordsOperation(c.Request.Method, path) {
c.Next()
@@ -95,12 +99,11 @@ func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.
}
func operationQueryBody(raw string) []byte {
query, _ := url.QueryUnescape(raw)
values := make(map[string]string)
for _, item := range strings.Split(query, "&") {
parts := strings.Split(item, "=")
if len(parts) == 2 {
values[parts[0]] = parts[1]
parsed, _ := url.ParseQuery(raw)
values := make(map[string]string, len(parsed))
for key, items := range parsed {
if len(items) > 0 {
values[key] = items[len(items)-1]
}
}
body, _ := json.Marshal(&values)
@@ -189,23 +192,6 @@ func operationPathMatches(pattern, path string) bool {
return len(patternParts) == len(pathParts)
}
func routeSuffix(path string) string {
bestIndex := -1
bestPath := path
for _, marker := range []string{"/user/", "/api/", "/casbin/", "/authority/", "/menu/", "/department/", "/position/", "/sysDictionary/", "/sysDictionaryDetail/", "/sysParams/", "/securityConfig/", "/system/", "/sysApiToken/", "/sysVersion/", "/sysExportTemplate/", "/sysError/", "/sysLoginLog/", "/sysOperationRecord/", "/dataAccessLog/", "/timedTask/", "/info/", "/email/", "/integration/", "/payment/"} {
if index := strings.Index(path, marker); index >= 0 {
// Router prefixes may themselves contain a registered route marker
// (for example /api/integration/...). Keep the deepest match so the
// policy and audit route remain the actual application endpoint.
if index > bestIndex {
bestIndex = index
bestPath = path[index:]
}
}
}
return bestPath
}
var operationRoutes = func() map[string]struct{} {
values := []string{
"POST /user/admin_register", "POST /user/changePassword", "POST /user/setUserAuthority", "DELETE /user/deleteUser", "PUT /user/setUserInfo", "PUT /user/setSelfInfo", "POST /user/setUserAuthorities", "POST /user/setUserDepartments", "POST /user/setUserPositions", "POST /user/resetPassword", "PUT /user/setSelfSetting",
+7 -1
View File
@@ -68,7 +68,9 @@ func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool)
return true
}
func requestToken(c *gin.Context, allowQueryToken bool) string {
// RequestToken extracts the authentication token accepted by HTTP handlers.
// Query-string tokens are opt-in for WebSocket handshakes only.
func RequestToken(c *gin.Context, allowQueryToken bool) string {
token := strings.TrimSpace(c.GetHeader("x-token"))
if token == "" {
authorization := strings.TrimSpace(c.GetHeader("Authorization"))
@@ -91,6 +93,10 @@ 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 {
+15 -3
View File
@@ -19,6 +19,10 @@ const (
// configuration reload takes effect without rebuilding the Gin engine.
func CORS(runtime *conf.Runtime) gin.HandlerFunc {
return func(c *gin.Context) {
if runtime == nil {
c.Next()
return
}
config := runtime.Admin()
if config == nil || config.Cors == nil {
c.Next()
@@ -28,12 +32,14 @@ func CORS(runtime *conf.Runtime) gin.HandlerFunc {
origin := c.GetHeader("Origin")
corsHandled := false
if mode == "allow-all" {
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true)
corsHandled = true
if origin != "" {
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true)
corsHandled = true
}
} else if rule := matchingCORSRule(config.Cors.Whitelist, origin); rule != nil {
setCORSHeaders(c, rule.AllowOrigin, rule.AllowHeaders, rule.AllowMethods, rule.ExposeHeaders, rule.AllowCredentials)
corsHandled = true
} else if mode == "strict-whitelist" && !(c.Request.Method == http.MethodGet && c.Request.URL.Path == "/health") {
} else if mode == "strict-whitelist" && !(c.Request.Method == http.MethodGet && isHealthPath(c.Request.URL.Path)) {
c.AbortWithStatus(http.StatusForbidden)
return
}
@@ -45,6 +51,11 @@ func CORS(runtime *conf.Runtime) gin.HandlerFunc {
}
}
func isHealthPath(path string) bool {
path = strings.TrimSuffix(path, "/")
return path == "/health" || strings.HasSuffix(path, "/health")
}
func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.AdminBackend_CORSRule {
for _, rule := range rules {
if rule != nil && origin == rule.AllowOrigin {
@@ -55,6 +66,7 @@ func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.
}
func setCORSHeaders(c *gin.Context, origin, headers, methods, expose string, credentials bool) {
c.Header("Vary", "Origin")
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Headers", headers)
c.Header("Access-Control-Allow-Methods", methods)
+6
View File
@@ -37,3 +37,9 @@ func TestCORSConsumesMatchedWhitelistPreflight(t *testing.T) {
t.Fatalf("matched whitelist preflight status = %d, want %d", response.Code, http.StatusNoContent)
}
}
func TestCORSStrictWhitelistAllowsPrefixedHealth(t *testing.T) {
if !isHealthPath("/admin/health") {
t.Fatal("prefixed health endpoint was not recognized by strict whitelist")
}
}
@@ -2,6 +2,7 @@ package middleware
import (
"bytes"
"encoding/json"
"errors"
"log/slog"
"net/http"
@@ -12,6 +13,16 @@ import (
"github.com/gin-gonic/gin"
)
func TestOperationQueryBodyPreservesEncodedSeparators(t *testing.T) {
var values map[string]string
if err := json.Unmarshal(operationQueryBody("filter=one%3Dtwo%26three&repeat=first&repeat=last"), &values); err != nil {
t.Fatal(err)
}
if values["filter"] != "one=two&three" || values["repeat"] != "last" {
t.Fatalf("unexpected query body: %#v", values)
}
}
func runErrorAudit(t *testing.T, path, response string, logger *slog.Logger) {
t.Helper()
gin.SetMode(gin.TestMode)
+9 -1
View File
@@ -16,6 +16,10 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
c.Next()
return
}
if settings == nil {
c.Next()
return
}
config, err := settings.CurrentSecurity(c.Request.Context())
if err != nil || config == nil {
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
@@ -29,7 +33,11 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
if window < 1 {
window = 60
}
key := "KRA_SecLimit" + c.ClientIP() + c.FullPath()
route := c.FullPath()
if route == "" {
route = path
}
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": "安全服务暂不可用"})