rbac
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+55
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user