后端重构

This commit is contained in:
Yvan
2026-01-08 17:41:41 +08:00
parent fad3767b79
commit 8800c069d9
47 changed files with 3135 additions and 978 deletions
-64
View File
@@ -1,64 +0,0 @@
package middleware
import (
"context"
"errors"
"strconv"
"strings"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
pkgcasbin "kra/pkg/casbin"
)
var (
ErrPermissionDenied = errors.New("权限不足")
)
// CasbinRBAC Casbin权限中间件
func CasbinRBAC(routerPrefix string) middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
claims, ok := GetClaims(ctx)
if !ok {
return nil, ErrMissingToken
}
if tr, ok := transport.FromServerContext(ctx); ok {
// 获取请求路径
path := tr.Operation()
if routerPrefix != "" {
path = strings.TrimPrefix(path, routerPrefix)
}
// 获取请求方法
act := "GET"
if header := tr.RequestHeader(); header != nil {
if method := header.Get(":method"); method != "" {
act = method
}
}
// 获取用户角色
sub := strconv.Itoa(int(claims.AuthorityID))
// 检查权限
enforcer := pkgcasbin.GetEnforcer()
if enforcer == nil {
return handler(ctx, req)
}
success, err := enforcer.Enforce(sub, path, act)
if err != nil {
return nil, err
}
if !success {
return nil, ErrPermissionDenied
}
}
return handler(ctx, req)
}
}
}
-78
View File
@@ -1,78 +0,0 @@
package middleware
import (
"bytes"
"context"
"fmt"
"io"
"time"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
kratoshttp "github.com/go-kratos/kratos/v2/transport/http"
)
// EmailSender 邮件发送接口
type EmailSender interface {
SendError(subject, body string) error
}
// ErrorToEmail 错误邮件通知中间件
func ErrorToEmail(sender EmailSender, logger log.Logger) middleware.Middleware {
helper := log.NewHelper(logger)
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
var (
username string
body string
method string
path string
ip string
)
// 获取用户信息
if claims, ok := GetClaims(ctx); ok {
username = claims.Username
}
if username == "" {
username = "Unknown"
}
if tr, ok := transport.FromServerContext(ctx); ok {
path = tr.Operation()
if header := tr.RequestHeader(); header != nil {
method = header.Get(":method")
}
if ht, ok := tr.(kratoshttp.Transporter); ok {
r := ht.Request()
ip = getClientIP(r)
// 读取body
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
body = string(bodyBytes)
}
}
start := time.Now()
reply, err := handler(ctx, req)
latency := time.Since(start)
// 如果有错误,发送邮件
if err != nil && sender != nil {
subject := fmt.Sprintf("%s %s调用了%s报错了", username, ip, path)
content := fmt.Sprintf(
"接收到的请求为%s\n请求方式为%s\n报错信息如下%s\n耗时%s\n",
body, method, err.Error(), latency.String(),
)
if sendErr := sender.SendError(subject, content); sendErr != nil {
helper.Error("ErrorToEmail Failed, err:", sendErr)
}
}
return reply, err
}
}
}
+65
View File
@@ -0,0 +1,65 @@
package middleware
import (
"strconv"
"strings"
"kra/pkg/casbin"
"kra/pkg/response"
"kra/pkg/utils"
"github.com/gin-gonic/gin"
)
// CasbinConfig Casbin中间件配置
type CasbinConfig struct {
RouterPrefix string // 路由前缀,需要从路径中去除
}
// 全局配置
var casbinConfig *CasbinConfig
// SetCasbinConfig 设置Casbin配置
func SetCasbinConfig(cfg *CasbinConfig) {
casbinConfig = cfg
}
// CasbinHandler Casbin权限拦截器(与 gva 保持一致)
func CasbinHandler() gin.HandlerFunc {
return func(c *gin.Context) {
waitUse, _ := utils.GetClaims(c)
if waitUse == nil {
response.FailWithDetailed(gin.H{}, "权限不足", c)
c.Abort()
return
}
// 获取请求的PATH
path := c.Request.URL.Path
// 去除路由前缀
if casbinConfig != nil && casbinConfig.RouterPrefix != "" {
path = strings.TrimPrefix(path, casbinConfig.RouterPrefix)
}
// 获取请求方法
act := c.Request.Method
// 获取用户的角色
sub := strconv.Itoa(int(waitUse.AuthorityID))
// 判断策略中是否存在
e := casbin.GetEnforcer()
if e == nil {
response.FailWithDetailed(gin.H{}, "Casbin未初始化", c)
c.Abort()
return
}
success, _ := e.Enforce(sub, path, act)
if !success {
response.FailWithDetailed(gin.H{}, "权限不足", c)
c.Abort()
return
}
c.Next()
}
}
+113
View File
@@ -0,0 +1,113 @@
package middleware
import (
"errors"
"strconv"
"time"
"kra/pkg/jwt"
"kra/pkg/response"
"kra/pkg/utils"
jwtv5 "github.com/golang-jwt/jwt/v5"
"github.com/gin-gonic/gin"
)
// BlacklistChecker JWT黑名单检查接口
type BlacklistChecker interface {
IsBlacklist(jwt string) bool
}
// JWTConfig JWT中间件配置
type JWTConfig struct {
JWT *jwt.JWT
BlacklistChecker BlacklistChecker
UseMultipoint bool
SetRedisJWT func(token, username string) error
}
// 全局配置(需要在初始化时设置)
var jwtConfig *JWTConfig
// SetJWTConfig 设置JWT配置
func SetJWTConfig(cfg *JWTConfig) {
jwtConfig = cfg
// 同时设置 utils 包的 JWT 实例
if cfg != nil && cfg.JWT != nil {
utils.SetJWT(cfg.JWT)
}
}
// JWTAuth JWT认证中间件(与 gva 保持一致)
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
token := utils.GetToken(c)
if token == "" {
response.NoAuth("未登录或非法访问,请登录", c)
c.Abort()
return
}
// 检查黑名单
if jwtConfig != nil && jwtConfig.BlacklistChecker != nil && jwtConfig.BlacklistChecker.IsBlacklist(token) {
response.NoAuth("您的帐户异地登陆或令牌失效", c)
utils.ClearToken(c)
c.Abort()
return
}
if jwtConfig == nil || jwtConfig.JWT == nil {
response.NoAuth("JWT配置错误", c)
c.Abort()
return
}
// parseToken 解析token包含的信息
claims, err := jwtConfig.JWT.ParseToken(token)
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
response.NoAuth("登录已过期,请重新登录", c)
utils.ClearToken(c)
c.Abort()
return
}
response.NoAuth(err.Error(), c)
utils.ClearToken(c)
c.Abort()
return
}
// 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开
// 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开
c.Set("claims", claims)
// 检查是否需要刷新token
if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime {
claims.ExpiresAt = jwtv5.NewNumericDate(time.Now().Add(jwtConfig.JWT.ExpiresAt))
newToken, newClaims, err := jwtConfig.JWT.CreateTokenByOldToken(token, *claims)
if err == nil {
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
utils.SetToken(c, newToken, int(jwtConfig.JWT.ExpiresAt.Seconds()/60))
// 多点登录记录新JWT
if jwtConfig.UseMultipoint && jwtConfig.SetRedisJWT != nil {
_ = jwtConfig.SetRedisJWT(newToken, newClaims.Username)
}
}
}
c.Next()
// 处理后续中间件设置的新token
if newToken, exists := c.Get("new-token"); exists {
c.Header("new-token", newToken.(string))
}
if newExpiresAt, exists := c.Get("new-expires-at"); exists {
c.Header("new-expires-at", newExpiresAt.(string))
}
}
}
+172
View File
@@ -0,0 +1,172 @@
package middleware
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"kra/pkg/utils"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
var respPool sync.Pool
var bufferSize = 1024
func init() {
respPool.New = func() interface{} {
return make([]byte, bufferSize)
}
}
// OperationRecord 操作记录结构
type OperationRecord struct {
IP string `json:"ip"`
Method string `json:"method"`
Path string `json:"path"`
Status int `json:"status"`
Latency time.Duration `json:"latency"`
Agent string `json:"agent"`
ErrorMessage string `json:"error_message"`
Body string `json:"body"`
Resp string `json:"resp"`
UserID int `json:"user_id"`
}
// OperationRecordCreator 操作记录创建接口
type OperationRecordCreator interface {
CreateOperationRecord(record *OperationRecord) error
}
// 全局操作记录创建器
var operationRecordCreator OperationRecordCreator
var operationLogger *zap.Logger
// SetOperationRecordCreator 设置操作记录创建器
func SetOperationRecordCreator(creator OperationRecordCreator) {
operationRecordCreator = creator
}
// SetOperationLogger 设置操作记录日志
func SetOperationLogger(logger *zap.Logger) {
operationLogger = logger
}
// OperationRecordMiddleware 操作记录中间件(与 gva 保持一致)
func OperationRecordMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
var body []byte
var userId int
if c.Request.Method != http.MethodGet {
var err error
body, err = io.ReadAll(c.Request.Body)
if err != nil {
if operationLogger != nil {
operationLogger.Error("read body from request error:", zap.Error(err))
}
} else {
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
}
} else {
query := c.Request.URL.RawQuery
query, _ = url.QueryUnescape(query)
split := strings.Split(query, "&")
m := make(map[string]string)
for _, v := range split {
kv := strings.Split(v, "=")
if len(kv) == 2 {
m[kv[0]] = kv[1]
}
}
body, _ = json.Marshal(&m)
}
claims, _ := utils.GetClaims(c)
if claims != nil && claims.BaseClaims.ID != 0 {
userId = int(claims.BaseClaims.ID)
} else {
id, err := strconv.Atoi(c.Request.Header.Get("x-user-id"))
if err != nil {
userId = 0
}
userId = id
}
record := OperationRecord{
IP: c.ClientIP(),
Method: c.Request.Method,
Path: c.Request.URL.Path,
Agent: c.Request.UserAgent(),
Body: "",
UserID: userId,
}
// 上传文件时候 中间件日志进行裁断操作
if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
record.Body = "[文件]"
} else {
if len(body) > bufferSize {
record.Body = "[超出记录长度]"
} else {
record.Body = string(body)
}
}
writer := responseBodyWriter{
ResponseWriter: c.Writer,
body: &bytes.Buffer{},
}
c.Writer = writer
now := time.Now()
c.Next()
latency := time.Since(now)
record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String()
record.Status = c.Writer.Status()
record.Latency = latency
record.Resp = writer.body.String()
// 检查是否为下载响应,如果是则截断
if strings.Contains(c.Writer.Header().Get("Pragma"), "public") ||
strings.Contains(c.Writer.Header().Get("Expires"), "0") ||
strings.Contains(c.Writer.Header().Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") ||
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/force-download") ||
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/octet-stream") ||
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/vnd.ms-excel") ||
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/download") ||
strings.Contains(c.Writer.Header().Get("Content-Disposition"), "attachment") ||
strings.Contains(c.Writer.Header().Get("Content-Transfer-Encoding"), "binary") {
if len(record.Resp) > bufferSize {
record.Resp = "[超出记录长度]"
}
}
// 保存操作记录
if operationRecordCreator != nil {
if err := operationRecordCreator.CreateOperationRecord(&record); err != nil {
if operationLogger != nil {
operationLogger.Error("create operation record error:", zap.Error(err))
}
}
}
}
}
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r responseBodyWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
@@ -0,0 +1,98 @@
package middleware
import (
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"kra/pkg/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// 全局日志实例
var recoveryLogger *zap.Logger
// SetRecoveryLogger 设置Recovery日志
func SetRecoveryLogger(logger *zap.Logger) {
recoveryLogger = logger
}
// GinRecovery 自定义Recovery中间件(与 gva 保持一致)
func GinRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 检查是否为断开的连接
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
if recoveryLogger != nil {
recoveryLogger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
// 如果连接已断开,无法写入状态
c.Error(err.(error))
c.Abort()
return
}
if stack {
if recoveryLogger != nil {
recoveryLogger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
}
} else {
if recoveryLogger != nil {
recoveryLogger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
}
response.FailWithMessage("服务器内部错误", c)
c.Abort()
}
}()
c.Next()
}
}
// Cors 跨域中间件
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,X-Token,X-User-Id")
c.Header("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT")
c.Header("Access-Control-Expose-Headers", "Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Content-Type,New-Token,New-Expires-At")
c.Header("Access-Control-Allow-Credentials", "true")
// 放行所有OPTIONS方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
c.Next()
}
}
-196
View File
@@ -1,196 +0,0 @@
package middleware
import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
"time"
"github.com/go-kratos/kratos/v2/metadata"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
"github.com/go-kratos/kratos/v2/transport/http"
pkgjwt "kra/pkg/jwt"
)
const (
// metadata keys for claims
mdKeyUserID = "x-md-user-id"
mdKeyUsername = "x-md-username"
mdKeyAuthorityID = "x-md-authority-id"
mdKeyUUID = "x-md-uuid"
mdKeyClaims = "x-md-claims"
)
var (
ErrMissingToken = errors.New("未登录或非法访问")
ErrInvalidToken = errors.New("无效的token")
ErrBlacklistJWT = errors.New("您的帐户异地登陆或令牌失效")
)
// BlacklistChecker JWT黑名单检查接口
type BlacklistChecker interface {
IsBlacklist(jwt string) bool
}
// JWTAuthConfig JWT认证配置
type JWTAuthConfig struct {
JWT *pkgjwt.JWT
BlacklistChecker BlacklistChecker
UseMultipoint bool
SetRedisJWT func(token, username string) error
}
// JWTAuth JWT认证中间件
func JWTAuth(cfg JWTAuthConfig) middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
if tr, ok := transport.FromServerContext(ctx); ok {
token := extractToken(tr)
if token == "" {
return nil, ErrMissingToken
}
// 检查黑名单
if cfg.BlacklistChecker != nil && cfg.BlacklistChecker.IsBlacklist(token) {
// 清除token
if ht, ok := tr.(http.Transporter); ok {
clearToken(ht)
}
return nil, ErrBlacklistJWT
}
claims, err := cfg.JWT.ParseToken(token)
if err != nil {
if ht, ok := tr.(http.Transporter); ok {
clearToken(ht)
}
if errors.Is(err, pkgjwt.ErrTokenExpired) {
return nil, errors.New("登录已过期,请重新登录")
}
return nil, ErrInvalidToken
}
// 将claims存入metadata
claimsJSON, _ := json.Marshal(claims)
ctx = metadata.AppendToClientContext(ctx,
mdKeyUserID, strconv.FormatUint(uint64(claims.BaseClaims.ID), 10),
mdKeyUsername, claims.Username,
mdKeyAuthorityID, strconv.FormatUint(uint64(claims.AuthorityID), 10),
mdKeyUUID, claims.UUID,
mdKeyClaims, string(claimsJSON),
)
// 检查是否需要刷新token
if cfg.JWT.NeedRefresh(claims) {
newToken, newClaims, err := cfg.JWT.CreateTokenByOldToken(token, *claims)
if err == nil {
if ht, ok := tr.(http.Transporter); ok {
ht.ReplyHeader().Set("new-token", newToken)
ht.ReplyHeader().Set("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
setToken(ht, newToken, int(time.Until(newClaims.ExpiresAt.Time).Seconds()/60))
}
// 多点登录记录新JWT
if cfg.UseMultipoint && cfg.SetRedisJWT != nil {
_ = cfg.SetRedisJWT(newToken, newClaims.Username)
}
}
}
}
return handler(ctx, req)
}
}
}
// JWTAuthSimple 简化版JWT认证中间件(不含黑名单检查)
func JWTAuthSimple(j *pkgjwt.JWT) middleware.Middleware {
return JWTAuth(JWTAuthConfig{JWT: j})
}
// extractToken 从请求中提取token
func extractToken(tr transport.Transporter) string {
// 优先从Authorization头获取
auth := tr.RequestHeader().Get("Authorization")
if auth != "" {
if strings.HasPrefix(auth, "Bearer ") {
return strings.TrimPrefix(auth, "Bearer ")
}
return auth
}
// 从x-token头获取
token := tr.RequestHeader().Get("x-token")
if token != "" {
return token
}
return ""
}
// clearToken 清除token
func clearToken(ht http.Transporter) {
ht.ReplyHeader().Set("x-token", "")
}
// setToken 设置token到cookie
func setToken(ht http.Transporter, token string, maxAge int) {
// 通过header设置cookie信息,让前端处理
ht.ReplyHeader().Set("x-token", token)
}
// GetClaims 从context获取claims
func GetClaims(ctx context.Context) (*pkgjwt.CustomClaims, bool) {
if md, ok := metadata.FromServerContext(ctx); ok {
claimsStr := md.Get(mdKeyClaims)
if claimsStr != "" {
var claims pkgjwt.CustomClaims
if err := json.Unmarshal([]byte(claimsStr), &claims); err == nil {
return &claims, true
}
}
}
return nil, false
}
// GetUserID 从context获取用户ID
func GetUserID(ctx context.Context) uint {
if md, ok := metadata.FromServerContext(ctx); ok {
if idStr := md.Get(mdKeyUserID); idStr != "" {
if id, err := strconv.ParseUint(idStr, 10, 64); err == nil {
return uint(id)
}
}
}
return 0
}
// GetUsername 从context获取用户名
func GetUsername(ctx context.Context) string {
if md, ok := metadata.FromServerContext(ctx); ok {
return md.Get(mdKeyUsername)
}
return ""
}
// GetAuthorityID 从context获取角色ID
func GetAuthorityID(ctx context.Context) uint {
if md, ok := metadata.FromServerContext(ctx); ok {
if idStr := md.Get(mdKeyAuthorityID); idStr != "" {
if id, err := strconv.ParseUint(idStr, 10, 64); err == nil {
return uint(id)
}
}
}
return 0
}
// GetToken 从context获取token
func GetToken(ctx context.Context) string {
if tr, ok := transport.FromServerContext(ctx); ok {
return extractToken(tr)
}
return ""
}
-133
View File
@@ -1,133 +0,0 @@
package middleware
import (
"bytes"
"context"
"encoding/json"
"io"
"time"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
kratoshttp "github.com/go-kratos/kratos/v2/transport/http"
)
// LogLayout 日志layout
type LogLayout struct {
Time time.Time `json:"time"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Path string `json:"path"`
Query string `json:"query,omitempty"`
Body string `json:"body,omitempty"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Error string `json:"error,omitempty"`
Cost time.Duration `json:"cost"`
Source string `json:"source"`
}
// LoggerConfig 日志中间件配置
type LoggerConfig struct {
// Filter 用户自定义过滤
Filter func(ctx context.Context, path string) bool
// FilterKeyword 关键字过滤
FilterKeyword func(layout *LogLayout) bool
// AuthProcess 鉴权处理
AuthProcess func(ctx context.Context, layout *LogLayout)
// Print 日志处理
Print func(LogLayout)
// Source 服务唯一标识
Source string
}
// Logger 日志中间件
func Logger(cfg LoggerConfig) middleware.Middleware {
if cfg.Print == nil {
cfg.Print = func(layout LogLayout) {
v, _ := json.Marshal(layout)
log.Info(string(v))
}
}
if cfg.Source == "" {
cfg.Source = "Kratos"
}
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
var (
path string
query string
body string
ip string
userAgent string
)
if tr, ok := transport.FromServerContext(ctx); ok {
path = tr.Operation()
if header := tr.RequestHeader(); header != nil {
userAgent = header.Get("User-Agent")
}
if ht, ok := tr.(kratoshttp.Transporter); ok {
r := ht.Request()
query = r.URL.RawQuery
ip = getClientIP(r)
// 读取body(仅在未过滤时)
if cfg.Filter == nil || !cfg.Filter(ctx, path) {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
body = string(bodyBytes)
}
}
}
// 检查是否需要过滤
if cfg.Filter != nil && cfg.Filter(ctx, path) {
return handler(ctx, req)
}
start := time.Now()
reply, err := handler(ctx, req)
cost := time.Since(start)
layout := LogLayout{
Time: time.Now(),
Path: path,
Query: query,
Body: body,
IP: ip,
UserAgent: userAgent,
Cost: cost,
Source: cfg.Source,
}
if err != nil {
layout.Error = err.Error()
}
// 处理鉴权信息
if cfg.AuthProcess != nil {
cfg.AuthProcess(ctx, &layout)
}
// 关键字过滤
if cfg.FilterKeyword != nil {
cfg.FilterKeyword(&layout)
}
// 输出日志
cfg.Print(layout)
return reply, err
}
}
}
// DefaultLogger 默认日志中间件
func DefaultLogger() middleware.Middleware {
return Logger(LoggerConfig{
Source: "Kratos",
})
}
-155
View File
@@ -1,155 +0,0 @@
package middleware
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
kratoshttp "github.com/go-kratos/kratos/v2/transport/http"
"gorm.io/gorm"
)
const maxBodySize = 1024
// OperationRecord 操作记录
type OperationRecord struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time `gorm:"index"`
Ip string `gorm:"column:ip;comment:请求ip"`
Method string `gorm:"column:method;comment:请求方法"`
Path string `gorm:"column:path;comment:请求路径"`
Status int `gorm:"column:status;comment:请求状态"`
Latency time.Duration `gorm:"column:latency;comment:延迟"`
Agent string `gorm:"column:agent;comment:代理"`
ErrorMessage string `gorm:"column:error_message;comment:错误信息"`
Body string `gorm:"type:text;column:body;comment:请求Body"`
Resp string `gorm:"type:text;column:resp;comment:响应Body"`
UserID int `gorm:"column:user_id;comment:用户id"`
}
func (OperationRecord) TableName() string {
return "sys_operation_records"
}
// OperationLog 操作日志中间件
func OperationLog(db *gorm.DB, logger log.Logger) middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
var (
body string
userID int
ip string
method string
path string
agent string
)
if tr, ok := transport.FromServerContext(ctx); ok {
path = tr.Operation()
if header := tr.RequestHeader(); header != nil {
method = header.Get(":method")
agent = header.Get("User-Agent")
}
// 获取HTTP请求信息
if ht, ok := tr.(kratoshttp.Transporter); ok {
r := ht.Request()
ip = getClientIP(r)
if r.Method != http.MethodGet {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
if len(bodyBytes) > maxBodySize {
body = "[超出记录长度]"
} else {
body = string(bodyBytes)
}
} else {
query := r.URL.RawQuery
query, _ = url.QueryUnescape(query)
params := parseQuery(query)
bodyBytes, _ := json.Marshal(params)
body = string(bodyBytes)
}
// 检查是否为文件上传
if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
body = "[文件]"
}
}
}
// 获取用户ID
if claims, ok := GetClaims(ctx); ok {
userID = int(claims.BaseClaims.ID)
} else {
// 尝试从header获取
if tr, ok := transport.FromServerContext(ctx); ok {
if ht, ok := tr.(kratoshttp.Transporter); ok {
if idStr := ht.Request().Header.Get("x-user-id"); idStr != "" {
if id, err := strconv.Atoi(idStr); err == nil {
userID = id
}
}
}
}
}
start := time.Now()
reply, err := handler(ctx, req)
latency := time.Since(start)
// 记录响应
var resp string
if reply != nil {
respBytes, _ := json.Marshal(reply)
if len(respBytes) > maxBodySize {
resp = "[超出记录长度]"
} else {
resp = string(respBytes)
}
}
// 记录错误
var errMsg string
status := 200
if err != nil {
errMsg = err.Error()
status = 500
}
// 保存记录
record := OperationRecord{
Ip: ip,
Method: method,
Path: path,
Status: status,
Latency: latency,
Agent: agent,
ErrorMessage: errMsg,
Body: body,
Resp: resp,
UserID: userID,
}
if db != nil {
go func() {
if err := db.Create(&record).Error; err != nil {
log.NewHelper(logger).Error("create operation record error:", err)
}
}()
}
return reply, err
}
}
}
-108
View File
@@ -1,108 +0,0 @@
package middleware
import (
"context"
"fmt"
"time"
"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
kratoshttp "github.com/go-kratos/kratos/v2/transport/http"
"github.com/redis/go-redis/v9"
)
// LimitConfig 限流配置
type LimitConfig struct {
// GenerationKey 根据业务生成key
GenerationKey func(ctx context.Context, path string) string
// CheckOrMark 检查函数
CheckOrMark func(ctx context.Context, key string, expire int, limit int) error
// Expire key过期时间(秒)
Expire int
// Limit 周期内限制次数
Limit int
}
// RateLimit 限流中间件
func RateLimit(cfg LimitConfig) middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
var key string
if tr, ok := transport.FromServerContext(ctx); ok {
path := tr.Operation()
key = cfg.GenerationKey(ctx, path)
}
if err := cfg.CheckOrMark(ctx, key, cfg.Expire, cfg.Limit); err != nil {
return nil, errors.New(429, "RATE_LIMIT", err.Error())
}
return handler(ctx, req)
}
}
}
// DefaultGenerationKey 默认生成key(基于IP)
func DefaultGenerationKey(ctx context.Context, path string) string {
if tr, ok := transport.FromServerContext(ctx); ok {
if ht, ok := tr.(kratoshttp.Transporter); ok {
return "Limit_" + getClientIP(ht.Request())
}
}
return "Limit_unknown"
}
// NewRedisCheckOrMark 创建基于Redis的检查函数
func NewRedisCheckOrMark(rdb *redis.Client) func(ctx context.Context, key string, expire int, limit int) error {
return func(ctx context.Context, key string, expire int, limit int) error {
if rdb == nil {
return nil
}
return SetLimitWithTime(ctx, rdb, key, limit, time.Duration(expire)*time.Second)
}
}
// SetLimitWithTime 设置访问次数
func SetLimitWithTime(ctx context.Context, rdb *redis.Client, key string, limit int, expiration time.Duration) error {
count, err := rdb.Exists(ctx, key).Result()
if err != nil {
return err
}
if count == 0 {
// key不存在,创建并设置过期时间
pipe := rdb.TxPipeline()
pipe.Incr(ctx, key)
pipe.Expire(ctx, key, expiration)
_, err = pipe.Exec(ctx)
return err
}
// key存在,检查次数
times, err := rdb.Get(ctx, key).Int()
if err != nil {
return err
}
if times >= limit {
// 获取剩余时间
ttl, err := rdb.PTTL(ctx, key).Result()
if err != nil {
return fmt.Errorf("请求太过频繁,请稍后再试")
}
return fmt.Errorf("请求太过频繁, 请 %s 后尝试", ttl.String())
}
return rdb.Incr(ctx, key).Err()
}
// DefaultRateLimit 默认限流中间件
func DefaultRateLimit(rdb *redis.Client, expire, limit int) middleware.Middleware {
return RateLimit(LimitConfig{
GenerationKey: DefaultGenerationKey,
CheckOrMark: NewRedisCheckOrMark(rdb),
Expire: expire,
Limit: limit,
})
}
-55
View File
@@ -1,55 +0,0 @@
package middleware
import (
"context"
"net"
"os"
"runtime/debug"
"strings"
"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware"
)
// Recovery panic恢复中间件
func Recovery(logger log.Logger) middleware.Middleware {
helper := log.NewHelper(logger)
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
defer func() {
if rerr := recover(); rerr != nil {
// 检查是否为断开的连接
var brokenPipe bool
if ne, ok := rerr.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
errStr := strings.ToLower(se.Error())
if strings.Contains(errStr, "broken pipe") ||
strings.Contains(errStr, "connection reset by peer") {
brokenPipe = true
}
}
}
stack := string(debug.Stack())
if brokenPipe {
helper.Errorw(
"msg", "[Recovery from panic]",
"error", rerr,
"broken_pipe", true,
)
} else {
helper.Errorw(
"msg", "[Recovery from panic]",
"error", rerr,
"stack", stack,
)
}
err = errors.InternalServer("PANIC", "服务器内部错误")
}
}()
return handler(ctx, req)
}
}
}
-53
View File
@@ -1,53 +0,0 @@
package middleware
import (
"context"
"time"
"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/middleware"
)
// Timeout 超时中间件
func Timeout(timeout time.Duration) middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// 使用 buffered channel 避免 goroutine 泄漏
type result struct {
reply interface{}
err error
}
done := make(chan result, 1)
panicChan := make(chan interface{}, 1)
go func() {
defer func() {
if p := recover(); p != nil {
select {
case panicChan <- p:
default:
}
}
}()
reply, err := handler(ctx, req)
select {
case done <- result{reply: reply, err: err}:
default:
}
}()
select {
case p := <-panicChan:
panic(p)
case r := <-done:
return r.reply, r.err
case <-ctx.Done():
return nil, errors.GatewayTimeout("TIMEOUT", "请求超时")
}
}
}
}