美文网首页
golang casbin gorm 权限控制

golang casbin gorm 权限控制

作者: e0c52543163a | 来源:发表于2019-01-15 12:02 被阅读0次

    Iris + Casbin 权限控制实战

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

    Iris 中间件机制简介

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

    image

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

    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>

    相关文章

      网友评论

          本文标题:golang casbin gorm 权限控制

          本文链接:https://www.haomeiwen.com/subject/qeoedqtx.html