优化结构
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": "安全服务暂不可用"})
|
||||
|
||||
Reference in New Issue
Block a user