diff --git a/go.mod b/go.mod index 186b348..baf16f7 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.23.3 require ( github.com/gin-gonic/gin v1.10.0 github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.23.0 gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.12 ) @@ -14,6 +16,7 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -34,6 +37,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -46,7 +50,6 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect diff --git a/internal/handler/admin/handler.go b/internal/handler/admin/handler.go index 3e52196..8c992d4 100644 --- a/internal/handler/admin/handler.go +++ b/internal/handler/admin/handler.go @@ -2,24 +2,21 @@ package admin import ( - "time" - "github.com/gin-gonic/gin" "portal-mini-go/internal/middleware" "portal-mini-go/internal/pkg/errors" "portal-mini-go/internal/pkg/response" - "portal-mini-go/internal/pkg/token" + authsvc "portal-mini-go/internal/service/admin/auth" ) // Handler 后台请求处理。 type Handler struct { - jwtSecret string - tokenTTL time.Duration + authSvc *authsvc.Service } -func NewHandler(jwtSecret string, tokenTTL time.Duration) *Handler { - return &Handler{jwtSecret: jwtSecret, tokenTTL: tokenTTL} +func NewHandler(authSvc *authsvc.Service) *Handler { + return &Handler{authSvc: authSvc} } type LoginReq struct { @@ -27,32 +24,42 @@ type LoginReq struct { Password string `json:"password" binding:"required"` } -// Login 后台登录(示例固定账号,生产应接 DB + 密码哈希校验)。 +// Login 后台登录:校验账号密码后签发 JWT。 func (h *Handler) Login(c *gin.Context) { var req LoginReq if err := c.ShouldBindJSON(&req); err != nil { c.Error(errors.ErrBadRequest) return } - if req.Username != "admin" || req.Password != "admin123" { - c.Error(errors.ErrUnauthorized) - return - } - t, err := token.Generate(h.jwtSecret, "1", h.tokenTTL) + t, err := h.authSvc.Login(c.Request.Context(), authsvc.LoginReq{Username: req.Username, Password: req.Password}) if err != nil { - c.Error(errors.Wrap(50000, "gen token", err)) + c.Error(err) return } response.OK(c, gin.H{"token": t}) } -// Info 受保护接口示例,需携带有效 Bearer token。 +// Info 受保护接口:返回当前管理员的角色、权限与可见菜单树。 func (h *Handler) Info(c *gin.Context) { - response.OK(c, gin.H{"admin_id": middleware.AdminID(c)}) + id, ok := middleware.AdminIDUint(c) + if !ok { + c.Error(errors.ErrUnauthorized) + return + } + info, err := h.authSvc.Info(c.Request.Context(), id) + if err != nil { + c.Error(err) + return + } + response.OK(c, info) } -// Register 注册后台路由。g 已带 /api/admin 前缀与 JWT 鉴权中间件。 +// Register 注册后台鉴权后路由(当前为 /info)。g 已带 JWT 鉴权中间件。 func Register(g *gin.RouterGroup, h *Handler) { - g.POST("/login", h.Login) g.GET("/info", h.Info) } + +// RegisterLogin 注册公开登录接口(不经过 JWT 鉴权),挂在 /api/admin 前缀下。 +func RegisterLogin(g *gin.RouterGroup, h *Handler) { + g.POST("/login", h.Login) +} diff --git a/internal/handler/admin/miniapp/miniapp_handler.go b/internal/handler/admin/miniapp/miniapp_handler.go index 79ebecf..f1e3e61 100644 --- a/internal/handler/admin/miniapp/miniapp_handler.go +++ b/internal/handler/admin/miniapp/miniapp_handler.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" + "portal-mini-go/internal/middleware" "portal-mini-go/internal/pkg/errors" "portal-mini-go/internal/pkg/response" miniappsvc "portal-mini-go/internal/service/admin/miniapp" @@ -106,14 +107,15 @@ func parseID(c *gin.Context) (uint64, bool) { return id, true } -// Register 注册后台小程序管理路由。g 已带 /api/admin 前缀与 JWT 鉴权中间件。 +// Register 注册后台小程序管理路由。g 已带 /api/admin 前缀与 JWT 鉴权中间件; +// 读接口挂 mini:view,写接口挂 mini:manage。 func Register(g *gin.RouterGroup, h *Handler) { rg := g.Group("/miniapps") { - rg.POST("", h.Create) - rg.PUT("/:id", h.Update) - rg.GET("/:id", h.Get) - rg.GET("", h.List) - rg.DELETE("/:id", h.Delete) + rg.POST("", middleware.RequirePerm(middleware.PermMiniManage), h.Create) + rg.PUT("/:id", middleware.RequirePerm(middleware.PermMiniManage), h.Update) + rg.GET("/:id", middleware.RequirePerm(middleware.PermMiniView), h.Get) + rg.GET("", middleware.RequirePerm(middleware.PermMiniView), h.List) + rg.DELETE("/:id", middleware.RequirePerm(middleware.PermMiniManage), h.Delete) } } diff --git a/internal/handler/admin/user/user_handler.go b/internal/handler/admin/user/user_handler.go index 650ecbe..ab6ac74 100644 --- a/internal/handler/admin/user/user_handler.go +++ b/internal/handler/admin/user/user_handler.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" + "portal-mini-go/internal/middleware" "portal-mini-go/internal/pkg/errors" "portal-mini-go/internal/pkg/response" usersvc "portal-mini-go/internal/service/admin/user" @@ -115,13 +116,14 @@ func parseID(c *gin.Context) (uint64, bool) { return id, true } -// Register 注册后台用户管理路由。g 已带 /api/admin 前缀与 JWT 鉴权中间件。 +// Register 注册后台用户管理路由。g 已带 /api/admin 前缀与 JWT 鉴权中间件; +// 读接口挂 user:view,写接口挂 user:manage。 func Register(g *gin.RouterGroup, h *Handler) { rg := g.Group("/users") { - rg.GET("", h.List) - rg.GET("/:id", h.Get) - rg.PUT("/:id", h.Update) - rg.POST("/:id/status", h.SetStatus) + rg.GET("", middleware.RequirePerm(middleware.PermUserView), h.List) + rg.GET("/:id", middleware.RequirePerm(middleware.PermUserView), h.Get) + rg.PUT("/:id", middleware.RequirePerm(middleware.PermUserManage), h.Update) + rg.POST("/:id/status", middleware.RequirePerm(middleware.PermUserManage), h.SetStatus) } } diff --git a/internal/middleware/auth_admin.go b/internal/middleware/auth_admin.go index 14668f1..01274fa 100644 --- a/internal/middleware/auth_admin.go +++ b/internal/middleware/auth_admin.go @@ -1,6 +1,7 @@ package middleware import ( + "strconv" "strings" "github.com/gin-gonic/gin" @@ -32,7 +33,7 @@ func AdminAuth(jwtSecret string) gin.HandlerFunc { } } -// AdminID 从 gin.Context 取出已认证的后台管理员 ID。 +// AdminID 从 gin.Context 取出已认证的后台管理员 ID(字符串形式,与 JWT 载荷一致)。 func AdminID(c *gin.Context) string { if v, ok := c.Get(adminIDCtxKey); ok { if s, ok := v.(string); ok { @@ -41,3 +42,17 @@ func AdminID(c *gin.Context) string { } return "" } + +// AdminIDUint 从 gin.Context 取出已认证的后台管理员 ID 并解析为 uint64。 +// 解析失败返回 (0, false),调用方应按“未认证”处理。 +func AdminIDUint(c *gin.Context) (uint64, bool) { + s := AdminID(c) + if s == "" { + return 0, false + } + id, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, false + } + return id, true +} diff --git a/internal/router/router.go b/internal/router/router.go index d0646b9..2bbf829 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -3,6 +3,9 @@ package router import ( + "context" + "log/slog" + "os" "time" "github.com/gin-gonic/gin" @@ -10,6 +13,7 @@ import ( adminhdl "portal-mini-go/internal/handler/admin" miniapphdl "portal-mini-go/internal/handler/admin/miniapp" + rbachdl "portal-mini-go/internal/handler/admin/rbac" userhdl "portal-mini-go/internal/handler/admin/user" app1hdl "portal-mini-go/internal/handler/miniapp/app1" "portal-mini-go/internal/middleware" @@ -17,9 +21,12 @@ import ( "portal-mini-go/internal/pkg/response" "portal-mini-go/internal/pkg/session" "portal-mini-go/internal/pkg/wechat" + adminrepo "portal-mini-go/internal/repository/admin" miniapprepo "portal-mini-go/internal/repository/miniapp" "portal-mini-go/internal/repository/user" + authsvc "portal-mini-go/internal/service/admin/auth" miniappsvc "portal-mini-go/internal/service/admin/miniapp" + adminrbac "portal-mini-go/internal/service/admin/rbac" usersvc "portal-mini-go/internal/service/admin/user" app1svc "portal-mini-go/internal/service/miniapp/app1" ) @@ -48,29 +55,65 @@ func New(cfg *config.Config, db *gorm.DB, store session.Store) *gin.Engine { { app1Svc := app1svc.New(userRepo, miniRepo, wx, store) app1hdl.Register(ma, app1hdl.NewHandler(app1Svc), middleware.MiniUserAuth(store)) - // 新增小程序时,在此依次注册 app2..app10: - // app2Svc := app2svc.New(userRepo, miniRepo, wx, store) - // app2hdl.Register(ma, app2hdl.NewHandler(app2Svc)) } - // 后台管理分组:JWT 鉴权。 + // 后台 RBAC 基础设施:仓储、缓存、权限 Provider、种子初始化。 + adminUserRepo := adminrepo.NewUserRepository(db) + adminRoleRepo := adminrepo.NewRoleRepository(db) + adminPermRepo := adminrepo.NewPermissionRepository(db) + permCache := adminrbac.NewMemoryCache(5 * time.Minute) + provider := adminrbac.NewProvider(adminUserRepo, adminRoleRepo, adminPermRepo, permCache) + if err := provider.Seed(context.Background(), + seedDefault("ADMIN_SEED_USERNAME", "admin"), + seedDefault("ADMIN_SEED_PASSWORD", "Admin@123456"), + ); err != nil { + slog.Error("rbac seed", slog.Any("err", err)) + } + + // 后台管理:登录为公开接口,其余均经 JWT 鉴权 + 权限加载(注入角色/权限集到 context)。 ad := r.Group("/api/admin") - ad.Use(middleware.AdminAuth(cfg.Admin.JWTSecret)) { - adminhdl.Register(ad, adminhdl.NewHandler(cfg.Admin.JWTSecret, parseTTL(cfg.Admin.TokenTTL))) + authSvc := authsvc.New(adminUserRepo, provider, cfg.Admin.JWTSecret, parseTTL(cfg.Admin.TokenTTL)) + adminHandler := adminhdl.NewHandler(authSvc) + // 登录公开,不挂 JWT 鉴权中间件。 + adminhdl.RegisterLogin(ad, adminHandler) - // 小程序管理:被后台纳管的微信小程序配置(增删改查)。 - miniSvc := miniappsvc.New(miniRepo) - miniapphdl.Register(ad, miniapphdl.NewHandler(miniSvc)) + // 鉴权后分组:AdminAuth + PermissionLoader。 + authAd := ad.Group("") + authAd.Use(middleware.AdminAuth(cfg.Admin.JWTSecret)) + authAd.Use(middleware.PermissionLoader(provider)) + { + adminhdl.Register(authAd, adminHandler) - // 用户管理:各小程序端用户(列表、详情、编辑、禁用/启用)。 - userSvc := usersvc.New(userRepo) - userhdl.Register(ad, userhdl.NewHandler(userSvc)) + // RBAC 管理接口(自身受 PermAdminManage / PermRoleManage / PermPermManage 守卫)。 + rbacHandler := rbachdl.NewHandler( + adminrbac.NewAdminService(adminUserRepo, provider), + adminrbac.NewRoleService(adminRoleRepo, provider), + adminrbac.NewPermissionService(adminPermRepo, provider), + ) + rbachdl.Register(authAd, rbacHandler) + + // 小程序管理:被后台纳管的微信小程序配置(增删改查)。 + miniSvc := miniappsvc.New(miniRepo) + miniapphdl.Register(authAd, miniapphdl.NewHandler(miniSvc)) + + // 用户管理:各小程序端用户(列表、详情、编辑、禁用/启用)。 + userSvc := usersvc.New(userRepo) + userhdl.Register(authAd, userhdl.NewHandler(userSvc)) + } } return r } +// seedDefault 读取环境变量,缺失时回退到默认值(仅用于首次种子初始化)。 +func seedDefault(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + func parseTTL(s string) time.Duration { if d, err := time.ParseDuration(s); err == nil { return d