From 47e8139f47bf107ca9b2ac2b29a8afd602e6d248 Mon Sep 17 00:00:00 2001 From: yanghao05 Date: Mon, 6 Feb 2023 20:10:47 +0800 Subject: [PATCH] add rbac --- config.toml | 2 +- middleware/jwt.go | 41 ++++++++++++ middleware/rbac.go | 44 +++++++++++++ providers/jwt/jwt.go | 8 ++- providers/rbac/casbin.go | 135 +++++++++++++++++++++++++++++++++++++++ providers/rbac/rbac.go | 6 ++ 6 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 middleware/jwt.go create mode 100644 middleware/rbac.go create mode 100644 providers/rbac/casbin.go create mode 100644 providers/rbac/rbac.go diff --git a/config.toml b/config.toml index d2c360c..a34a005 100644 --- a/config.toml +++ b/config.toml @@ -1,5 +1,5 @@ [App] -Mode = "debug" +Mode = "debug" # develop, production [Captcha] KeyLong = 6 diff --git a/middleware/jwt.go b/middleware/jwt.go new file mode 100644 index 0000000..45b4c44 --- /dev/null +++ b/middleware/jwt.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "atom/providers/jwt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/rogeecn/gen" +) + +func JWTAuth(jwt *jwt.JWT) gin.HandlerFunc { + return func(c *gin.Context) { + // 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 + token := c.Request.Header.Get("Authorization") + if token == "" { + gen.NewBusError(http.StatusBadRequest, http.StatusBadRequest, "未登录或非法访问").JSON(c, false) + c.Abort() + return + } + + // parseToken 解析token包含的信息 + claims, err := jwt.ParseToken(token) + if err != nil { + gen.NewBusError(http.StatusBadRequest, http.StatusBadRequest, err.Error()).JSON(c, false) + c.Abort() + return + } + + // 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开 + // 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开 + + //if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 { + // _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token}) + // response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c) + // c.Abort() + //} + + c.Set("claims", claims) + c.Next() + } +} diff --git a/middleware/rbac.go b/middleware/rbac.go new file mode 100644 index 0000000..ad3c0a5 --- /dev/null +++ b/middleware/rbac.go @@ -0,0 +1,44 @@ +package middleware + +import ( + "atom/providers/config" + "atom/providers/jwt" + "atom/providers/rbac" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/rogeecn/gen" +) + +// Permission 拦截器 +func CheckPermission(config *config.Config, rbac rbac.IRbac, jwt *jwt.JWT) gin.HandlerFunc { + return func(c *gin.Context) { + if config.App.Mode != "production" { + c.Next() + return + } + + claim, err := jwt.GetClaims(c) + if err != nil { + gen.NewBusError(http.StatusBadRequest, http.StatusBadRequest, "Token 获取失败").JSON(c, false) + c.Abort() + return + } + + //获取请求的PATH + path := c.Request.URL.Path + + // 获取请求方法 + method := c.Request.Method + + // 获取用户的角色 + role := strconv.Itoa(int(claim.RoleID)) + + if rbac.Can(role, method, path) == false { + gen.NewBusError(http.StatusForbidden, http.StatusForbidden, "未登录或非法访问").JSON(c, false) + c.Abort() + return + } + } +} diff --git a/providers/jwt/jwt.go b/providers/jwt/jwt.go index a698b11..fc0f57d 100644 --- a/providers/jwt/jwt.go +++ b/providers/jwt/jwt.go @@ -5,6 +5,7 @@ import ( "atom/providers/config" "atom/providers/log" "errors" + "strings" "time" "github.com/gin-gonic/gin" @@ -26,6 +27,8 @@ type CustomClaims struct { jwt.RegisteredClaims } +const TOKEN_PREFIX = "Bearer " + type BaseClaims struct { UUID string UserID uint64 @@ -85,6 +88,7 @@ func (j *JWT) CreateTokenByOldToken(oldToken string, claims CustomClaims) (strin // 解析 token func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) { + tokenString = strings.TrimPrefix(tokenString, TOKEN_PREFIX) token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { return j.SigningKey, nil }) @@ -114,10 +118,10 @@ func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) { } func (j *JWT) GetClaims(c *gin.Context) (*CustomClaims, error) { - token := c.Request.Header.Get("x-token") + token := c.Request.Header.Get("Authorization") claims, err := j.ParseToken(token) if err != nil { - log.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在x-token且claims是否为规定结构") + log.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在 Authorization 且 Claims 为规定结构") } return claims, err } diff --git a/providers/rbac/casbin.go b/providers/rbac/casbin.go new file mode 100644 index 0000000..9b268f5 --- /dev/null +++ b/providers/rbac/casbin.go @@ -0,0 +1,135 @@ +package rbac + +import ( + "atom/container" + "atom/database/query" + "atom/providers/log" + "context" + "errors" + "strconv" + + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + _ "github.com/go-sql-driver/mysql" + "gorm.io/gorm" +) + +func init() { + if err := container.Container.Provide(NewCasbin); err != nil { + log.Fatal(err) + } +} + +type Casbin struct { + query *query.Query + enforcer *casbin.CachedEnforcer +} + +type CasbinInfo struct { + Path string `json:"path"` // 路径 + Method string `json:"method"` // 方法 +} + +func NewCasbin( + query *query.Query, + db *gorm.DB, +) (IRbac, error) { + cb := &Casbin{ + query: query, + } + + a, _ := gormadapter.NewAdapterByDB(db) + text := ` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act + ` + m, err := model.NewModelFromString(text) + if err != nil { + log.Error(err, "字符串加载模型失败!") + return nil, err + } + cb.enforcer, _ = casbin.NewCachedEnforcer(m, a) + + cb.enforcer.SetExpireTime(60 * 60) + _ = cb.enforcer.LoadPolicy() + return cb, nil +} + +func (cb *Casbin) Can(role, method, path string) bool { + return false +} + +func (cb *Casbin) Reload() error { + return nil +} + +func (cb *Casbin) Update(roleID uint, infos []CasbinInfo) error { + roleIdStr := strconv.Itoa(int(roleID)) + cb.Clear(0, roleIdStr) + + rules := [][]string{} + for _, v := range infos { + rules = append(rules, []string{roleIdStr, v.Path, v.Method}) + } + + success, _ := cb.enforcer.AddPolicies(rules) + if !success { + return errors.New("存在相同api,添加失败,请联系管理员") + } + + err := cb.enforcer.InvalidateCache() + if err != nil { + return err + } + + return nil +} + +func (cb *Casbin) UpdateApi(before, after CasbinInfo) error { + rule := cb.query.CasbinRule + + _, err := rule.WithContext(context.Background()). + Where(rule.V1.Eq(before.Path)). + Where(rule.V1.Eq(before.Method)). + UpdateSimple( + rule.V1.Value(after.Path), + rule.V2.Value(after.Method), + ) + if err != nil { + return err + } + + return cb.enforcer.InvalidateCache() +} + +// 获取权限列表 +func (cb *Casbin) GetPolicyPathByRoleID(roleID uint) (pathMaps []CasbinInfo) { + roleIdStr := strconv.Itoa(int(roleID)) + list := cb.enforcer.GetFilteredPolicy(0, roleIdStr) + for _, v := range list { + pathMaps = append(pathMaps, CasbinInfo{ + Path: v[1], + Method: v[2], + }) + } + return pathMaps +} + +// 清除匹配的权限 +func (cb *Casbin) Clear(v int, p ...string) bool { + success, _ := cb.enforcer.RemoveFilteredPolicy(v, p...) + return success +} diff --git a/providers/rbac/rbac.go b/providers/rbac/rbac.go new file mode 100644 index 0000000..1866fa6 --- /dev/null +++ b/providers/rbac/rbac.go @@ -0,0 +1,6 @@ +package rbac + +type IRbac interface { + Can(role, method, path string) bool + Reload() error +}