Graphql请求的RBAC权限控制
概要
Graphql 请求灵活性远非 RestFul 的请求能比, 但是 Graphql 的 Endpoint 一般都是统一的一个, 根据 body 中的请求内容和参数来决定返回何种数据.
因此, 对于 Graphql 请求的权限控制不能像 RestFul 请求那样根据请求的 URL 和 method 来判断是否有权限. 关于 Graphql 的权限判断, 曾经困扰了好长一段时间, 一直没有找到合适的方式来判断.
直至最近一段时间, 在 github.com/graphql-go 这个库中找到能够解析 graphql 请求的方法, 然后结合 casbin 的 rbac 模型, 才算是将 Graphql 权限问题的解决推进了一大步.
实现方式
实现 Graphql 的权限认证, 主要包含以下 3 个部分:
- adapter: 用来连接 graphql-engine 的 casbin adapter, 也就是可以将权限策略持久存储到数据库
- middleware: 基于 golang gin 框架的中间件, 拦截请求并判断其是否符合权限要求
- 权限 api: 提供操作权限的 API, 基于 RBAC 的, 所以只提供的基础的几个接口
adapter
casbin 默认是将权限策略存储在 csv 文件中的, 这只能在 demo 中用用, 实际系统用明显不合适. 因此, 我们需要写个 adapter, 将权限策略写入数据库.
adapter 很简单, 只要仿照 casbin 已有的那些 adapter 实现相应的接口即可:
1 package auth
2
3 import (
4 "runtime"
5
6 imodel "illuminant/model"
7
8 "github.com/casbin/casbin/v2/model"
9 "github.com/casbin/casbin/v2/persist"
10 )
11
12 // Adapter represents the hasura graphql
13 type Adapter struct{}
14
15 // finalizer is the destructor for Adapter.
16 func finalizer(a *Adapter) {}
17
18 // NewAdapter is the constructor for Adapter.
19 func NewAdapter() (*Adapter, error) {
20 a := &Adapter{}
21
22 // Call the destructor when the object is released.
23 runtime.SetFinalizer(a, finalizer)
24
25 return a, nil
26 }
27
28 func loadPolicyLine(line *imodel.CasbinRule, model model.Model) {
29 lineText := line.PType
30 if line.V0 != "" {
31 lineText += ", " + line.V0
32 }
33 if line.V1 != "" {
34 lineText += ", " + line.V1
35 }
36 if line.V2 != "" {
37 lineText += ", " + line.V2
38 }
39 if line.V3 != "" {
40 lineText += ", " + line.V3
41 }
42 if line.V4 != "" {
43 lineText += ", " + line.V4
44 }
45 if line.V5 != "" {
46 lineText += ", " + line.V5
47 }
48
49 persist.LoadPolicyLine(lineText, model)
50 }
51
52 // LoadPolicy loads policy from database.
53 func (a *Adapter) LoadPolicy(model model.Model) error {
54 lines, err := GetRules()
55 if err != nil {
56 return err
57 }
58
59 for _, line := range lines {
60 loadPolicyLine(line, model)
61 }
62
63 return nil
64 }
65
66 func savePolicyLine(ptype string, rule []string) imodel.CasbinRule {
67 line := imodel.CasbinRule{}
68
69 line.PType = ptype
70 if len(rule) > 0 {
71 line.V0 = rule[0]
72 }
73 if len(rule) > 1 {
74 line.V1 = rule[1]
75 }
76 if len(rule) > 2 {
77 line.V2 = rule[2]
78 }
79 if len(rule) > 3 {
80 line.V3 = rule[3]
81 }
82 if len(rule) > 4 {
83 line.V4 = rule[4]
84 }
85 if len(rule) > 5 {
86 line.V5 = rule[5]
87 }
88
89 return line
90 }
91
92 // SavePolicy saves policy to database.
93 func (a *Adapter) SavePolicy(model model.Model) error {
94 err := DeleteRules(imodel.CasbinRule{})
95 if err != nil {
96 return err
97 }
98
99 var lines = make([]imodel.CasbinRule, 0)
100
101 for ptype, ast := range model["p"] {
102 for _, rule := range ast.Policy {
103 line := savePolicyLine(ptype, rule)
104 lines = append(lines, line)
105 }
106 }
107
108 for ptype, ast := range model["g"] {
109 for _, rule := range ast.Policy {
110 line := savePolicyLine(ptype, rule)
111 lines = append(lines, line)
112 }
113 }
114
115 return AddRules(lines)
116 }
117
118 // AddPolicy adds a policy rule to the storage.
119 func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
120 line := savePolicyLine(ptype, rule)
121 return AddRule(line)
122 }
123
124 // RemovePolicy removes a policy rule from the storage.
125 func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error {
126 line := savePolicyLine(ptype, rule)
127 return DeleteRules(line)
128 }
129
130 // RemoveFilteredPolicy removes policy rules that match the filter from the storage.
131 func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
132 line := imodel.CasbinRule{}
133
134 line.PType = ptype
135 filter := []string{}
136 filter = append(filter, "p_type")
137 if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) {
138 line.V0 = fieldValues[0-fieldIndex]
139 filter = append(filter, "v0")
140 }
141 if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) {
142 line.V1 = fieldValues[1-fieldIndex]
143 filter = append(filter, "v1")
144 }
145 if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) {
146 line.V2 = fieldValues[2-fieldIndex]
147 filter = append(filter, "v2")
148 }
149 if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) {
150 line.V3 = fieldValues[3-fieldIndex]
151 filter = append(filter, "v3")
152 }
153 if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) {
154 line.V4 = fieldValues[4-fieldIndex]
155 filter = append(filter, "v4")
156 }
157 if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) {
158 line.V5 = fieldValues[5-fieldIndex]
159 filter = append(filter, "v5")
160 }
161
162 return DeleteRules(line)
163 }
其中, GetRules, DeleteRules, AddRule, AddRules 是实际和 graphql-engin 交互的函数.
middleware
基于 golang gin 的中间件, 目的是将权限检查和业务 API 的职责分开, 便于开发和维护.
1 package middleware
2
3 import (
4 "bytes"
5 "illuminant/config"
6 "illuminant/logger"
7 "illuminant/middleware/auth"
8 "illuminant/util"
9 "io/ioutil"
10 "strings"
11
12 jwt "github.com/appleboy/gin-jwt/v2"
13 "github.com/casbin/casbin/v2"
14 "github.com/casbin/casbin/v2/model"
15 "github.com/gin-gonic/gin"
16 )
17
18 // NewAuthorizer returns the authorizer, uses a Casbin enforcer as input
19 func NewAuthorizer() gin.HandlerFunc {
20 cnf := config.GetConfig()
21 lg := logger.GetLogger()
22 adp, err := auth.NewAdapter()
23 if err != nil {
24 lg.Err(err).Msg("casbin adapter error")
25 panic(err)
26 }
27
28 m, err := model.NewModelFromString(cnf.Auth.RBACModel)
29 if err != nil {
30 lg.Err(err).Msg("casbin model from string error")
31 panic(err)
32 }
33
34 e, err := casbin.NewEnforcer(m, adp)
35 if err != nil {
36 lg.Err(err).Msg("casbin enforcer error")
37 panic(err)
38 }
39
40 a := &RBACAuthorizer{enforcer: e}
41 return func(c *gin.Context) {
42 if !a.CheckPermission(c) {
43 a.RequirePermission(c)
44 c.Abort()
45 }
46 }
47 }
48
49 // RBACAuthorizer stores the casbin handler
50 type RBACAuthorizer struct {
51 enforcer *casbin.Enforcer
52 }
53
54 // CheckPermission checks the user/method/path combination from the request.
55 // Returns true (permission granted) or false (permission forbidden)
56 func (a *RBACAuthorizer) CheckPermission(c *gin.Context) bool {
57 lg := logger.GetLogger()
58 claims := jwt.ExtractClaims(c)
59
60 method := c.Request.Method
61 path := c.Request.URL.Path
62
63 // a.ReloadPermissions()
64 if strings.Index(path, "/api/v1/graphql") < 0 {
65 return a.RestFullPermission(claims["id"].(string), path, method)
66 }
67
68 // graphql api
69 body, err := c.GetRawData()
70 if err != nil {
71 lg.Err(err).Msg("get body raw data")
72 return false
73 }
74 c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
75
76 return a.GraphqlPermission(claims["id"].(string), body)
77 }
78
79 // RequirePermission returns the 403 Forbidden to the client
80 func (a *RBACAuthorizer) RequirePermission(c *gin.Context) {
81 util.Fail(c, util.AUTH_PERMISSION_DENIED, "权限不足", nil)
82 }
83
84 func (a *RBACAuthorizer) ReloadPermissions() {
85 a.enforcer.LoadPolicy()
86 }
87
88 func (a *RBACAuthorizer) RestFullPermission(userId, path, method string) bool {
89 lg := logger.GetLogger()
90
91 allowed, err := a.enforcer.Enforce(userId, path, method)
92 if err != nil {
93 lg.Err(err).Msg("RestFullPermission check error")
94 return false
95 }
96
97 return allowed
98 }
99
100 func (a *RBACAuthorizer) GraphqlPermission(userId string, body []byte) bool {
101 lg := logger.GetLogger()
102
103 funcs, err := util.GetGraphqlFunc(body)
104 if err != nil {
105 lg.Err(err).Msg("GetGraphqlFunc error")
106 return false
107 }
108
109 for _, f := range funcs {
110 allowed, err := a.enforcer.Enforce(userId, f, "*")
111 if err != nil {
112 lg.Err(err).Msg("GraphqlPermission check error")
113 return false
114 }
115
116 if !allowed {
117 return allowed
118 }
119 }
120
121 return true
122 }
这个中间件同时支持 RestFul 和 Graphql 的接口, 只要将相应的策略存入数据库即可.
基础权限 API
基础的 API 主要有 5 个:
- 获取某个角色的所有权限
- 给角色增加权限
- 给角色删除权限
- 给用户添加角色
- 给用户删除角色
有这 5 个基础 API 之后, 基本就满足了管理 casbin 中 RBAC 策略的需求了.
总结
上面的功能是在我自己一个正在逐步完善的一个后端快速开发平台上是实现的, 所有代码都在那个平台上.
即: illuminant的代码 以及 illuminant 的文档
上面的代码位于: middleware 模块, middleware 模块下的 auth 模块 以及 controller 模块下的 auth_controller.go
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战