golang casbin gorm 权限控制

Iris + Casbin 权限控制实战

在木犀的 PaaS 云平台的设计中,需要有一个细粒度比较小的权限控制系统。不同用户对不同的资源,拥有不同的权限。土办法已经不管用了,我们需要更系统,更规范的权限控制系统。本文讲的就是如何将权限控制库 Casbin接入 Iris Web 框架。

Iris 中间件机制简介

Iris 这个框架是基于中间件的,和 Nodejs 的 Koa 和 Express 很像。所谓中间件机制,就是一个请求到达之后,会生成一个上下文信息,里面包含了这个请求的一些信息。然后我们依次调用中间件函数,把上下文对象作为参数传入。需要注意是中间件函数的调用是嵌套的,在中间件函数中我们可以调用 ctx.Next() 方法,进入下一个中间件函数。当最后一个中间件函数返回时,之前调用过的中间件会依次返回。这个数据流被形象的称为“洋葱模型”。

一个典型的中间件是这样的:

func middleware(ctx iris.Context) {
   // get info from context
    requestPath := ctx.Path()

   // set info with context
    ctx.Values().Set("info", shareInformation)

    // call next middleware
    ctx.Next()
}

我们可以在中间件中读取 ctx 结构,根据上面附带的信息,我们可以做一些针对性的事情。

一个常用的中间件场景就是访问控制。我们可以根据 ctx 上带的用户信息,来查看用户的权限,如果用户没有要访问的资源的权限,我们就拒绝这次访问。比如这样:

func auth(ctx iris.Context) {
   // check if user has permission
   if !c.Check(ctx.Request()) {
        ctx.StatusCode(http.StatusForbidden) // Status Forbiden
        ctx.StopExecution() 
        return
    }
    ctx.Next()
}

在本文的权限控制的场景下,中间件的作用就是在请求验证失败时,提前返回 403 状态码。

Casbin 简介

Casbin 是由北大的一位博士生主导开发的一个基于 Go 语言的权限控制库。支持 ACL,RBAC,ABAC 等常用的访问控制模型。

Casbin 的核心是一套基于 PERM metamodel (Policy, Effect, Request, Matchers) 的 DSL。Casbin 从用这种 DSL 定义的配置文件中读取访问控制模型,作为后续权限验证的基础。

一个典型的配置文件如下:

# Request definition
[request_definition]
r = sub, obj, act

# Policy definition
[policy_definition]
p = sub, obj, act

# Policy effect
[policy_effect]
e = some(where (p.eft == allow))

# Matchers
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

可以看到这个配置文件主要定义了 Request 和 Policy 的组成结构。Policy effect 和 Matchers 则灵活的多,可以包含一些自定义的表达式。

比如我们要加入一个名叫 root 的超级管理员,就可以这样写:

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"

又比如我们可以用正则匹配来判断权限是否 match:

[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)

Casbin 文档中有一节展示了 Casbin 所支持的权限控制模型的示例配置。我们可以根据那些例子来打造我们自己的权限控制模型。

有了权限控制模型,我们还需要权限规则。权限规则是若干行数据的集合。每行数据就是一条规则。权限规则的具体格式因权限模型中的定义而异,最简单的 ACL 模型的规则是这样的:

p, alice, data1, read
p, bob, data2, write

意思就是 alice 可以读 data1,bob 可以写 data2。

为 Casbin 适配 Iris 中间件

既然实现权限控制的最佳位置是中间件,我们就需要为 Casbin 写一个 Iris 中间件。社区里的 Casbin-iris 插件就是一个不错的例子,我们可以以这个插件为基础进行开发。

首先我们来看看这个中间件是如何使用的,这个插件有 Warpper 和 Middleware 两种用法。差别在于 Warpper 方法会在所有路由被调用。而 Middleware 让我们可以控制哪些路由启用权限控制。我们选择 Middleware 方式做示例。打开 casbin/_examples/middleware/main.go,其中核心的几行代码是这样的:

var Enforcer = casbin.NewEnforcer("casbinmodel.conf", "casbinpolicy.csv")

func newApp() *iris.Application {
    casbinMiddleware := casbinMiddleware.New(Enforcer)

    app := iris.New()
    app.Use(casbinMiddleware.ServeHTTP)

    app.Get("/", hi)

    app.Get("/dataset1/{p:path}", hi) // p, alice, /dataset1/*, GET

    app.Post("/dataset1/resource1", hi)

    app.Get("/dataset2/resource2", hi)
    app.Post("/dataset2/folder1/{p:path}", hi)

    app.Any("/dataset2/resource1", hi)

    return app
}

首先通过 NewEnforcer 方法初始化一个 Casbin Enforcer。NewEnforcer 方法接收两个参数,一个是访问控制模型文件的路径,一个是权限规则文件的路径。

然后调用 casbinMiddleware.New 方法,传入 Casbin Enforcer,进行一些初始化工作。最后调用 app.Use(casbinMiddleware.ServeHTTP),应用中间件。

看起来还挺简单的。但这里存在一个问题,这个中间件是如何拿到鉴权需要的用户信息的呢?这个过程对于开发者是不透明的。我们查看源码,发现里面有这样的代码:

// Username gets the username from the basicauth.
func Username(r *http.Request) string {
    username, _, _ := r.BasicAuth()
    return username
}

原来这个中间件假设请求通过 HTTP Basic Auth 方式进行认证。然后从请求的 headers 中获取认证信息。

但在实际生产中,我们认证用户身份的方式有很多种,最常见的就是通过 session 得知用户的身份,或者通过 token 这样的凭证来确定用户的身份。这个中间件如果要使用到生产中去,需要进行一些改动。

以下就是修改过的中间件,为了测试,我在 Username 函数中直接返回了用户名,后续使用时可以在这个函数里进行用户身份的获取。

package casbin

import (
    "net/http"

    "github.com/kataras/iris/context"

    "github.com/casbin/casbin"
)

func New(e *casbin.Enforcer) *Casbin {
    return &Casbin{enforcer: e}
}

func (c *Casbin) Wrapper() func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) {
    return func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) {
        if !c.Check(r) {
            w.WriteHeader(http.StatusForbidden)
            w.Write([]byte("403 Forbidden"))
            return
        }
        router(w, r)
    }
}

func (c *Casbin) ServeHTTP(ctx context.Context) {
    if !c.Check(ctx.Request()) {
        ctx.StatusCode(http.StatusForbidden) // Status Forbiden
        ctx.StopExecution()
        return
    }
    ctx.Next()
}

type Casbin struct {
    enforcer *casbin.Enforcer
}

// Check checks the username, request's method and path and
// returns true if permission grandted otherwise false.
func (c *Casbin) Check(r *http.Request) bool {
    username := Username(r)
    method := r.Method
    path := r.URL.Path
    return c.enforcer.Enforce(username, path, method)
}

// Username gets the username from the basicauth.
func Username(r *http.Request) string {
   // TODO: get username form db using userId in session or token
    return "alice"
}

大家可以用这里的代码、模型规则文件进行测试(将中间件替换为上面的版本)。如果我们 GET /dataset2/resource2 这个路径,就会返回 403。这说明中间件正常工作了。因为 alice 是没有 /dataset2/resource2 这个资源的 GET 权限的。

选择合适的访问控制模型

我选择了 RBAC with domains/tenants 这个模型作为木犀云平台的访问控制模型。PaaS 平台中有服务、应用等多种资源,所以需要按领域模型区分。用户中可以存在超级管理员等角色,所以需要角色。需要注意的是 Casbin 的 RBAC 中的角色其实是一种分组。比如我们可以定义一个叫 admin 的用户,这个用户对所有的资源都有权限规则存在,然后我们可以把其他用户和这个用户分为一组,那这些用户也都有了 admin 用户的权限。

[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act

这个模型定义中的 g 就是指 group。

示例规则如下:

p, admin, domain1, data1, read
p, admin, domain1, data1, write
p, admin, domain2, data2, read
p, admin, domain2, data2, write
g, alice, admin, domain1
g, bob, admin, domain2

如上所示,alice 和 bob 分别是 domian1 和 domain2 的管理员。

Casbin Policy 持久化

Casbin 的 policy 可以保存在一个 csv 文件中。也可以被持久化到数据库中。

所谓的“持久化到数据库中”的意思,就是在数据库中创建一个表,把行数据都存放到数据库中。

对于一个 Web 应用,我们想要的当然是后者。所以我们还需要将 Casbin 和数据库连接起来。

Casbin 支持 gorm 和 xorm 等等常见的 orm。我们以 gorm 为例,Canbin 的 gorm 适配库是 gorm-adapter

接入 gorm 并不是很复杂的事情,其实就是把 NewEnforcer 中的第二个 policy file 参数换成 gorm-adapter 的一个实例就可以。

// 自动创建一个数据库,叫 casbin
// 如果需要制定数据库名,可以这样 a := gormadapter.NewAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/abc", true)
var a = gormadapter.NewAdapter("mysql", "root:muxi@tcp(127.0.0.1:3306)/") 

var Enforcer = casbin.NewEnforcer("casbinmodel.conf", a)

然后我们可以调用 API 对规则进行添加和删除等等操作:

Enforcer.LoadPolicy()
Enforcer.AddPolicy("admin", "app", "/app/1", "GET")
Enforcer.AddGroupingPolicy("alice", "admin", "app")

这里踩了一个小坑,这个 gorm-adapter 的 README 里没有写 AddGroupingPolicy 这个 API。还是翻源码才看到的。

规则文件中的 p 对应 AddPolicy API,g 对应 AddGroupingPolicy API。

Warp it up

TODO: 把文中的示例代码整理到一个仓库中

一点感受:Casbin 的文档主要是 README 中的内容。Iris 的文档则主要要看 Example 这一节。有点 Example Driven Development 的感觉。虽然这么说,不过整体来说,这些资料还是可以覆盖到我们的使用场景的。

</article>

</main>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,478评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,825评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,482评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,726评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,633评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,018评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,168评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,320评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,264评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,288评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,995评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,587评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,909评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,284评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,862评论 2 339