This commit is contained in:
2025-06-30 16:44:06 +08:00
parent 4186cd0caf
commit c8a81d0f49
840 changed files with 1389 additions and 403165 deletions

View File

@@ -2,56 +2,51 @@ package mid
import (
"context"
"net/http"
"time"
"management/internal/erpserver/model/system"
"management/internal/pkg/know"
"management/internal/pkg/session"
"management/internal/tasks"
"github.com/drhin/logger"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/hibiken/asynq"
"go.uber.org/zap"
)
// Audit 改造后的中间件
func Audit(sess session.Manager, log *logger.Logger, task tasks.TaskDistributor) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
start := time.Now()
func Audit(log *logger.Logger, task tasks.TaskDistributor) gin.HandlerFunc {
return func(ctx *gin.Context) {
start := time.Now()
log.Info(start.Format(time.DateTime))
ctx.Next()
user, err := sess.GetUser(ctx, know.StoreName)
if err != nil {
log.Error("获取用户会话失败", err)
}
payload := GetUser(ctx)
if payload == nil {
return
}
next.ServeHTTP(w, r)
if payload.ID == uuid.Nil {
return
}
if user.ID == 0 {
return
}
logs := &tasks.PayloadConsumeAuditLog{
//AuditLog: system.NewAuditLog(r, payload.Email, payload.OS, payload.Browser, start, time.Now()),
}
payload := &tasks.PayloadConsumeAuditLog{
AuditLog: system.NewAuditLog(r, user.Email, user.OS, user.Browser, start, time.Now()),
}
opts := []asynq.Option{
asynq.MaxRetry(10),
asynq.ProcessIn(1 * time.Second),
asynq.Queue(tasks.QueueCritical),
}
opts := []asynq.Option{
asynq.MaxRetry(10),
asynq.ProcessIn(1 * time.Second),
asynq.Queue(tasks.QueueCritical),
}
c, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
c, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
if err := task.DistributeTaskConsumeAuditLog(c, payload, opts...); err != nil {
log.Error("distribute task failed", err,
zap.String("type", "audit"),
zap.Any("payload", payload),
)
}
})
if err := task.DistributeTaskConsumeAuditLog(c, logs, opts...); err != nil {
log.Error("distribute task failed", err,
zap.String("type", "audit"),
zap.Any("payload", payload),
)
}
}
}

View File

@@ -2,53 +2,29 @@ package mid
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"management/internal/erpserver/model/dto"
v1 "management/internal/erpserver/service/v1"
"management/internal/pkg/know"
"management/internal/pkg/session"
"management/internal/pkg/token"
"github.com/gin-gonic/gin"
"github.com/patrickmn/go-cache"
"golang.org/x/sync/singleflight"
)
//// 高性能JSON库全局初始化
//var json = jsoniter.ConfigFastest
//
//// 使用jsoniter优化菜单结构体序列化
//func init() {
// jsoniter.RegisterTypeEncoderFunc("dto.OwnerMenuDto",
// func(ptr unsafe.Pointer, stream *jsoniter.Stream) {
// m := (*dto.OwnerMenuDto)(ptr)
// stream.WriteObjectStart()
// stream.WriteObjectField("id")
// stream.WriteUint(uint(m.ID))
// stream.WriteMore()
// stream.WriteObjectField("url")
// stream.WriteString(m.Url)
// stream.WriteMore()
// stream.WriteObjectField("parentId")
// stream.WriteUint(uint(m.ParentID))
// stream.WriteMore()
// stream.WriteObjectField("isList")
// stream.WriteBool(m.IsList)
// stream.WriteObjectEnd()
// }, nil)
//}
var publicRoutes = map[string]bool{
"/home.html": true,
"/dashboard": true,
"/system/menus": true,
"/upload/img": true,
"/upload/file": true,
"/upload/multi_files": true,
"/system/pear.json": true,
"/logout": true,
}
@@ -130,90 +106,111 @@ func getAllRoleIDs() []uint {
return []uint{1, 2, 3, 4, 5}
}
const (
authorizationHeaderKey = "authorization"
authorizationTypeBearer = "bearer"
authorizationPayloadKey = "authorization_payload"
)
func Authorize(
sess session.Manager,
tokenMaker token.Maker,
menuService v1.MenuService,
) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
path := r.URL.Path
) gin.HandlerFunc {
return func(ctx *gin.Context) {
// 登陆检查
user, err := sess.GetUser(ctx, know.StoreName)
if err != nil || user.ID == 0 {
http.Redirect(w, r, "/", http.StatusFound)
return
}
authorizationHeader := ctx.GetHeader(authorizationHeaderKey)
// 公共路由放行
if publicRoutes[path] {
ctx = setUser(ctx, user)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
if len(authorizationHeader) == 0 {
err := errors.New("authorization header is not provided")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
n1 := time.Now()
// 权限检查
var menus map[string]*dto.OwnerMenuDto
cacheKey := fmt.Sprintf("user_menus:%d", user.RoleID)
shardIndex := getShardIndex(uint(user.RoleID))
shard := menuCacheShards[shardIndex]
mutex := shardMutexes[shardIndex]
fields := strings.Fields(authorizationHeader)
if len(fields) < 2 {
err := errors.New("invalid authorization header format")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
// 1. 尝试无锁读缓存
if cachedMenus, found := shard.Get(cacheKey); found {
menus = cachedMenus.(map[string]*dto.OwnerMenuDto)
log.Printf("listByRoleIDToMap (from cache): %s", time.Since(n1).String())
} else {
// 2. 单飞机制防止缓存击穿
menusI, err, _ := flightGroup.Do(cacheKey, func() (interface{}, error) {
// 3. 双检锁机制
if cached, found := shard.Get(cacheKey); found {
return cached, nil
}
authorizationType := strings.ToLower(fields[0])
if authorizationType != authorizationTypeBearer {
err := fmt.Errorf("unsupported authorization type %s", authorizationType)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
// 4. 查询数据库获取菜单数据
maps, err := menuService.ListByRoleIDToMap(ctx, user.RoleID)
if err != nil {
return nil, err
}
accessToken := fields[1]
payload, err := tokenMaker.VerifyToken(accessToken, token.TypeAccessToken)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
// 5. 写入缓存(加锁避免重复写入)
mutex.Lock()
defer mutex.Unlock()
// 用户校验完毕, 开始校验权限
// 6. 再次检查避免其他goroutine已经写入
if cached, found := shard.Get(cacheKey); found {
return cached, nil
}
path := ctx.Request.URL.Path
// 7. 设置分级过期时间
expiration := getCacheExpiration(uint(user.RoleID))
shard.Set(cacheKey, maps, expiration)
return maps, nil
})
// 公共路由放行
if publicRoutes[path] {
ctx.Set(authorizationPayloadKey, payload)
ctx.Next()
return
}
if err != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return
// 权限检查
var menus map[string]*dto.OwnerMenuDto
cacheKey := fmt.Sprintf("user_menus:%d", payload.RoleID)
shardIndex := getShardIndex(uint(payload.RoleID))
shard := menuCacheShards[shardIndex]
mutex := shardMutexes[shardIndex]
// 1. 尝试无锁读缓存
if cachedMenus, found := shard.Get(cacheKey); found {
menus = cachedMenus.(map[string]*dto.OwnerMenuDto)
} else {
// 2. 单飞机制防止缓存击穿
menusI, err, _ := flightGroup.Do(cacheKey, func() (interface{}, error) {
// 3. 双检锁机制
if cached, found := shard.Get(cacheKey); found {
return cached, nil
}
menus = menusI.(map[string]*dto.OwnerMenuDto)
log.Printf("listByRoleIDToMap (from DB, then cached): %s", time.Since(n1).String())
}
if !hasPermission(menus, path) {
http.Error(w, "Forbidden", http.StatusForbidden)
// 4. 查询数据库获取菜单数据
maps, err := menuService.ListByRoleIDToMap(ctx, int32(payload.RoleID))
if err != nil {
return nil, err
}
// 5. 写入缓存(加锁避免重复写入)
mutex.Lock()
defer mutex.Unlock()
// 6. 再次检查避免其他goroutine已经写入
if cached, found := shard.Get(cacheKey); found {
return cached, nil
}
// 7. 设置分级过期时间
expiration := getCacheExpiration(uint(payload.RoleID))
shard.Set(cacheKey, maps, expiration)
return maps, nil
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
menus = menusI.(map[string]*dto.OwnerMenuDto)
}
cur := getCurrentMenus(menus, path)
if !hasPermission(menus, path) {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
ctx = setUser(ctx, user)
ctx = setCurMenus(ctx, cur)
next.ServeHTTP(w, r.WithContext(ctx))
})
setUser(ctx, payload)
ctx.Next()
}
}
@@ -222,25 +219,6 @@ func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool {
return ok
}
func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto {
var res []dto.OwnerMenuDto
menu, ok := data[path]
if !ok {
return res
}
for _, item := range data {
if menu.IsList {
if item.ParentID == menu.ID || item.ID == menu.ID {
res = append(res, *item)
}
} else {
if item.ParentID == menu.ParentID {
res = append(res, *item)
}
}
}
return res
func errorResponse(err error) gin.H {
return gin.H{"error": err.Error()}
}

View File

@@ -5,26 +5,27 @@ import (
"errors"
"management/internal/erpserver/model/dto"
"management/internal/erpserver/model/system"
"management/internal/pkg/sqldb"
"management/internal/pkg/token"
"github.com/a-h/templ"
"github.com/gin-gonic/gin"
)
type userKey struct{}
func setUser(ctx context.Context, usr system.AuthorizeUser) context.Context {
return context.WithValue(ctx, userKey{}, usr)
func setUser(ctx *gin.Context, payload *token.Payload) {
ctx.Set(authorizationPayloadKey, payload)
}
// GetUser returns the user from the context.
func GetUser(ctx context.Context) system.AuthorizeUser {
v, ok := ctx.Value(userKey{}).(system.AuthorizeUser)
if !ok {
return system.AuthorizeUser{}
func GetUser(ctx *gin.Context) *token.Payload {
value, exists := ctx.Get(authorizationHeaderKey)
if !exists {
return nil
}
return v
return value.(*token.Payload)
}
type menuKey struct{}