diff --git a/go.mod b/go.mod index ab41bab..6444e8d 100644 --- a/go.mod +++ b/go.mod @@ -52,8 +52,11 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 53edff9..26fa712 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,7 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -90,6 +91,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= @@ -109,6 +112,10 @@ github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOj github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= diff --git a/internal/erpserver/http.go b/internal/erpserver/http.go index 8a98cb8..ed1841f 100644 --- a/internal/erpserver/http.go +++ b/internal/erpserver/http.go @@ -46,8 +46,8 @@ func NewHTTPServer( r.Handle("/public/*", http.StripPrefix("/public", uploadServer)) r.Group(func(r chi.Router) { - r.Use(mi.NoSurf) // CSRF - r.Use(mi.NoSurfContext) // CSRF Store Context + //r.Use(mi.NoSurf) // CSRF + //r.Use(mi.NoSurfContext) // CSRF Store Context r.Use(mi.LoadSession(sm)) // Session r.Get("/", userHandler.Login) @@ -71,7 +71,7 @@ func NewHTTPServer( }) r.Route("/system", func(r chi.Router) { - r.Use(mi.Audit(sm, log)) + //r.Use(mi.Audit(sm, log)) r.Get("/menus", menuHandler.Menus) diff --git a/internal/pkg/mid/authorize_v4.go b/internal/pkg/mid/authorize_v4.go index c64da85..d8395db 100644 --- a/internal/pkg/mid/authorize_v4.go +++ b/internal/pkg/mid/authorize_v4.go @@ -1,121 +1,121 @@ package mid -import ( - "fmt" - "log" - "net/http" - "time" - - "management/internal/erpserver/model/dto" - v1 "management/internal/erpserver/service/v1" - "management/internal/pkg/know" - "management/internal/pkg/session" - - "github.com/patrickmn/go-cache" -) - -var publicRoutes = map[string]bool{ - "/home.html": true, - "/dashboard": true, - "/system/menus": true, - "/upload/img": true, - "/upload/file": true, - "/upload/multi_files": true, - "/pear.json": true, - "/logout": true, -} - -// 定义一个全局的go-cache实例 -var menuCache *cache.Cache - -func init() { - // 初始化go-cache,设置默认过期时间为5分钟,每10分钟清理一次过期项 - menuCache = cache.New(5*time.Minute, 10*time.Minute) -} - -func Authorize( - sess session.Manager, - 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 - - // 登陆检查 - user, err := sess.GetUser(ctx, know.StoreName) - if err != nil || user.ID == 0 { - http.Redirect(w, r, "/", http.StatusFound) - return - } - - // 公共路由放行 - if publicRoutes[path] { - ctx = setUser(ctx, user) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } - - n1 := time.Now() - // 权限检查 - var menus map[string]*dto.OwnerMenuDto - cacheKey := fmt.Sprintf("user_menus:%d", user.RoleID) // 使用用户RoleID作为缓存key - - // 尝试从内存缓存中获取菜单数据 - if cachedMenus, found := menuCache.Get(cacheKey); found { - menus = cachedMenus.(map[string]*dto.OwnerMenuDto) - log.Printf("listByRoleIDToMap (from cache): %s", time.Since(n1).String()) - - } else { - // 内存缓存未命中,从menuService获取,并存入内存缓存 - menus, err = menuService.ListByRoleIDToMap(ctx, user.RoleID) - if err != nil { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - menuCache.Set(cacheKey, menus, cache.DefaultExpiration) // 使用默认过期时间 - log.Printf("listByRoleIDToMap (from service, then cached): %s", time.Since(n1).String()) - } - - if !hasPermission(menus, path) { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - - cur := getCurrentMenus(menus, path) - - ctx = setUser(ctx, user) - ctx = setCurMenus(ctx, cur) - - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} - -func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool { - _, ok := menus[path] - 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 -} +//import ( +// "fmt" +// "log" +// "net/http" +// "time" +// +// "management/internal/erpserver/model/dto" +// v1 "management/internal/erpserver/service/v1" +// "management/internal/pkg/know" +// "management/internal/pkg/session" +// +// "github.com/patrickmn/go-cache" +//) +// +//var publicRoutes = map[string]bool{ +// "/home.html": true, +// "/dashboard": true, +// "/system/menus": true, +// "/upload/img": true, +// "/upload/file": true, +// "/upload/multi_files": true, +// "/pear.json": true, +// "/logout": true, +//} +// +//// 定义一个全局的go-cache实例 +//var menuCache *cache.Cache +// +//func init() { +// // 初始化go-cache,设置默认过期时间为5分钟,每10分钟清理一次过期项 +// menuCache = cache.New(5*time.Minute, 10*time.Minute) +//} +// +//func Authorize( +// sess session.Manager, +// 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 +// +// // 登陆检查 +// user, err := sess.GetUser(ctx, know.StoreName) +// if err != nil || user.ID == 0 { +// http.Redirect(w, r, "/", http.StatusFound) +// return +// } +// +// // 公共路由放行 +// if publicRoutes[path] { +// ctx = setUser(ctx, user) +// next.ServeHTTP(w, r.WithContext(ctx)) +// return +// } +// +// n1 := time.Now() +// // 权限检查 +// var menus map[string]*dto.OwnerMenuDto +// cacheKey := fmt.Sprintf("user_menus:%d", user.RoleID) // 使用用户RoleID作为缓存key +// +// // 尝试从内存缓存中获取菜单数据 +// if cachedMenus, found := menuCache.Get(cacheKey); found { +// menus = cachedMenus.(map[string]*dto.OwnerMenuDto) +// log.Printf("listByRoleIDToMap (from cache): %s", time.Since(n1).String()) +// +// } else { +// // 内存缓存未命中,从menuService获取,并存入内存缓存 +// menus, err = menuService.ListByRoleIDToMap(ctx, user.RoleID) +// if err != nil { +// http.Error(w, "Forbidden", http.StatusForbidden) +// return +// } +// menuCache.Set(cacheKey, menus, cache.DefaultExpiration) // 使用默认过期时间 +// log.Printf("listByRoleIDToMap (from service, then cached): %s", time.Since(n1).String()) +// } +// +// if !hasPermission(menus, path) { +// http.Error(w, "Forbidden", http.StatusForbidden) +// return +// } +// +// cur := getCurrentMenus(menus, path) +// +// ctx = setUser(ctx, user) +// ctx = setCurMenus(ctx, cur) +// +// next.ServeHTTP(w, r.WithContext(ctx)) +// }) +// } +//} +// +//func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool { +// _, ok := menus[path] +// 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 +//} diff --git a/internal/pkg/mid/authorize_v5.go b/internal/pkg/mid/authorize_v5.go new file mode 100644 index 0000000..5af056b --- /dev/null +++ b/internal/pkg/mid/authorize_v5.go @@ -0,0 +1,248 @@ +package mid + +import ( + "context" + "fmt" + "log" + "net/http" + "sync" + "time" + "unsafe" + + "management/internal/erpserver/model/dto" + v1 "management/internal/erpserver/service/v1" + "management/internal/pkg/know" + "management/internal/pkg/session" + + "github.com/json-iterator/go" + "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, + "/pear.json": true, + "/logout": true, +} + +// 分片缓存配置 +const cacheShards = 64 // 根据CPU核心数调整(建议核心数*4) +var ( + menuCacheShards [cacheShards]*cache.Cache + shardMutexes [cacheShards]*sync.Mutex + flightGroup singleflight.Group +) + +func init() { + // 初始化分片缓存 + for i := 0; i < cacheShards; i++ { + menuCacheShards[i] = cache.New(5*time.Minute, 10*time.Minute) + shardMutexes[i] = &sync.Mutex{} + } +} + +// 获取分片索引(基于角色ID) +func getShardIndex(roleID uint) int { + return int(roleID) % cacheShards +} + +// 缓存过期时间分级 +func getCacheExpiration(roleID uint) time.Duration { + // 这里可以添加业务逻辑区分高频/低频角色 + // 示例:高频角色缓存30分钟,低频10分钟 + if isHighFrequencyRole(roleID) { + return 30 * time.Minute + } + return 10 * time.Minute +} + +// 判断是否为高频角色(示例实现) +func isHighFrequencyRole(roleID uint) bool { + // 实际项目中,这里可以根据角色ID查询配置或历史访问频率 + return roleID < 100 // 假设ID小于100的角色是高频角色 +} + +// WarmUpMenuCache 缓存预热函数(在服务启动时调用) +func WarmUpMenuCache(menuService v1.MenuService) { + ctx := context.Background() + // 获取所有角色ID(这里需要实现getAllRoleIDs) + roleIDs := getAllRoleIDs() + + log.Printf("Starting cache warm-up for %d roles", len(roleIDs)) + + // 并发预热 + var wg sync.WaitGroup + wg.Add(len(roleIDs)) + + for _, roleID := range roleIDs { + go func(rid uint) { + defer wg.Done() + + shardIndex := getShardIndex(rid) + cacheKey := fmt.Sprintf("user_menus:%d", rid) + shard := menuCacheShards[shardIndex] + + // 预热数据 + if _, found := shard.Get(cacheKey); !found { + menus, err := menuService.ListByRoleIDToMap(ctx, int32(rid)) + if err == nil { + shard.Set(cacheKey, menus, getCacheExpiration(rid)) + } + } + }(roleID) + } + + wg.Wait() + log.Println("Menu cache warm-up completed") +} + +// 获取所有角色ID(需要实现) +func getAllRoleIDs() []uint { + // 实际项目中需要从数据库获取所有角色ID + // 这里返回一个示例ID列表 + return []uint{1, 2, 3, 4, 5} +} + +func Authorize( + sess session.Manager, + 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 + + // 登陆检查 + user, err := sess.GetUser(ctx, know.StoreName) + if err != nil || user.ID == 0 { + http.Redirect(w, r, "/", http.StatusFound) + return + } + + // 公共路由放行 + if publicRoutes[path] { + ctx = setUser(ctx, user) + next.ServeHTTP(w, r.WithContext(ctx)) + 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] + + // 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 + } + + // 4. 查询数据库获取菜单数据 + maps, err := menuService.ListByRoleIDToMap(ctx, user.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(user.RoleID)) + shard.Set(cacheKey, maps, expiration) + return maps, nil + }) + + if err != nil { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + 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) + return + } + + cur := getCurrentMenus(menus, path) + + ctx = setUser(ctx, user) + ctx = setCurMenus(ctx, cur) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool { + _, ok := menus[path] + 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 +}