refactor: separate backend domains and storage adapters
This commit is contained in:
@@ -3,9 +3,9 @@ package middleware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -22,16 +22,16 @@ func AccessControl(access *service.AccessService, audit *service.AuditService) g
|
||||
if err != nil || !allowed {
|
||||
requestID, _ := c.Get("request_id")
|
||||
requestIDText, _ := requestID.(string)
|
||||
_ = audit.RecordDataAccess(c.Request.Context(), &biz.DataAccessLog{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
|
||||
_ = audit.RecordDataAccessRequest(c.Request.Context(), &dto.DataAccessRecordRequest{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "权限不足"})
|
||||
return
|
||||
}
|
||||
scope, err := access.ResolveDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
|
||||
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "数据权限加载失败")
|
||||
return
|
||||
}
|
||||
c.Request = c.Request.WithContext(biz.NewDataScopeContext(c.Request.Context(), scope))
|
||||
c.Request = c.Request.WithContext(requestContext)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func OperationAudit(service *service.AuditService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
path := c.Request.URL.Path
|
||||
if strings.Contains(path, "sysOperationRecord") || strings.Contains(path, "sysLoginLog") || strings.Contains(path, "dataAccessLog") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
var requestBody []byte
|
||||
if c.Request.Body != nil {
|
||||
requestBody, _ = io.ReadAll(io.LimitReader(c.Request.Body, 32769))
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
}
|
||||
writer := &captureWriter{ResponseWriter: c.Writer}
|
||||
c.Writer = writer
|
||||
started := time.Now()
|
||||
c.Next()
|
||||
userID := uint(0)
|
||||
if claims := Claims(c); claims != nil {
|
||||
userID = claims.ID
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
status, errorMessage := c.Writer.Status(), ""
|
||||
if status >= 400 {
|
||||
errorMessage = writer.body.String()
|
||||
}
|
||||
_ = service.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: redactJSON(requestBody), Response: redactJSON(writer.body.Bytes()), UserID: userID, RequestID: stringValue(requestID), TraceID: c.GetHeader("traceparent"), DeviceID: c.GetHeader("X-Device-Id")})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type captureWriter struct {
|
||||
gin.ResponseWriter
|
||||
body bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *captureWriter) Write(data []byte) (int, error) {
|
||||
if w.body.Len() < 32768 {
|
||||
remaining := 32768 - w.body.Len()
|
||||
if len(data) > remaining {
|
||||
_, _ = w.body.Write(data[:remaining])
|
||||
} else {
|
||||
_, _ = w.body.Write(data)
|
||||
}
|
||||
}
|
||||
return w.ResponseWriter.Write(data)
|
||||
}
|
||||
|
||||
func redactJSON(raw []byte) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(raw) > 32768 {
|
||||
raw = raw[:32768]
|
||||
}
|
||||
var value any
|
||||
if json.Unmarshal(raw, &value) != nil {
|
||||
return string(raw)
|
||||
}
|
||||
var clean func(any)
|
||||
clean = func(current any) {
|
||||
switch v := current.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range v {
|
||||
lower := strings.ToLower(key)
|
||||
if strings.Contains(lower, "password") || strings.Contains(lower, "token") || strings.Contains(lower, "secret") {
|
||||
v[key] = "******"
|
||||
} else {
|
||||
clean(item)
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
clean(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
clean(value)
|
||||
encoded, _ := json.Marshal(value)
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func stringValue(value any) string { text, _ := value.(string); return text }
|
||||
|
||||
func isBootstrapPath(path string) bool {
|
||||
return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/")
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func EmailErrorAlert(service *service.EmailService, logger *slog.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if strings.Contains(c.Request.URL.Path, "/email/") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
writer := &captureWriter{ResponseWriter: c.Writer}
|
||||
c.Writer = writer
|
||||
started := time.Now()
|
||||
c.Next()
|
||||
failed := c.Writer.Status() >= 400
|
||||
if !failed {
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
failed = json.Unmarshal(writer.body.Bytes(), &result) == nil && result.Code != httpx.CodeSuccess
|
||||
}
|
||||
if !failed {
|
||||
return
|
||||
}
|
||||
username := "Unknown"
|
||||
if claims := Claims(c); claims != nil && claims.Username != "" {
|
||||
username = claims.Username
|
||||
}
|
||||
subject := fmt.Sprintf("%s %s 调用 %s 报错", username, c.ClientIP(), c.Request.URL.Path)
|
||||
body := bytes.NewBuffer(nil)
|
||||
fmt.Fprintf(body, "请求方式:%s<br>请求路径:%s<br>状态码:%d<br>耗时:%s<br>错误响应:<pre>%s</pre>", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), time.Since(started), redactJSON(writer.body.Bytes()))
|
||||
if err := service.Alert(c.Request.Context(), subject, body.String()); err != nil && logger != nil {
|
||||
logger.Error("send HTTP error email", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SecurityRateLimit(system *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if isBootstrapPath(c.Request.URL.Path) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
config, err := settings.CurrentSecurity(c.Request.Context())
|
||||
if err != nil || config == nil || !config.LimitEnable {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
window := config.LimitWindow
|
||||
if window < 1 {
|
||||
window = 60
|
||||
}
|
||||
bucket := time.Now().Unix() / int64(window)
|
||||
key := "rate:" + c.ClientIP() + ":" + strconv.FormatInt(bucket, 10)
|
||||
count, cacheErr := system.CacheIncrement(c.Request.Context(), key, time.Duration(window+1)*time.Second)
|
||||
if cacheErr == nil && int(count) > config.LimitCount {
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "请求过于频繁"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user