原文 ,省略了一些无关的内容。
如果你搜到这篇文章,那么什么是gin以及casbin应该不用过多解释了。
项目结构 1 2 3 4 5 6 root/ main.go handler/ middleware/ config/ component/
初始化数据库和缓存 在component
目录下创建persistence.go
用于初始化,这里使用GORM
来处理数据库,BigCache
处理缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import ( "fmt" "github.com/allegro/bigcache" _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" "time" ) var ( DB *gorm.DB GlobalCache *bigcache.BigCache ) func init () { var err error DB, err = gorm.Open("mysql" , "your_db_url" ) if err != nil { panic (fmt.Sprintf("failed to connect to DB: %v" , err)) } GlobalCache, err = bigcache.NewBigCache(bigcache.DefaultConfig(30 * time.Minute)) if err != nil { panic (fmt.Sprintf("failed to initialize cahce: %v" , err)) } }
在这个示例中,我们使用数据库来存储casbin的polices,使用缓存存储登录用户信息。
配置Casbin Model Configuration File 首先,你也许会发现casbin中有些概念让你很困惑,比如Model Configuration File
。这里我不想讨论太多原理(因为我也不熟),直接举个例子,使用基于角色的权限控制(RBAC,Role-based access control)。所以首先在config
目录创建rbac_model.conf
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [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 = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
上面的文件定义了casbin如何判断用户拥有什么权限,例子中我们定义了5个字段:
r = sub, obj, act
定义了一个请求需要由3部分组成:sub=用户,obj=URL或资源,act=操作。p = sub, obj, act
定义了策略的格式,比如admin,dada,write
表示admin有data的写权限。e = some(where (p.eft == allow))
定义了用户可以做那些策略中定义准许他做的事。g = _, _
定义了角色的格式,例如bob,admin
表示用户bob是admin这个角色。m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
定义了鉴权时的流程,先检查用户角色,再检查用户访问的资源,最后检查用户行为。上面几个部分,仅1、2、3、5是必须的,如果不使用RBAC可以忽略4。
Roy注:下面的这个更常用。程序中判断如果角色是admin则直接传’admin’而非用户名,这样直接有所有权限了。 [matchers] m = g(r.sub, p.sub) == true \ && keyMatch2(r.obj, p.obj) == true \ && regexMatch(r.act, p.act) == true \ || r.sub == “admin” \ || keyMatch2(r.obj, “/login”) == true
Polices 举个例子:
1 2 3 4 5 p, user, data, read p, admin, data, read p, admin, data, write g, Alice, admin g, Bob, user
首先我们定义了3个策略:
user可以读取data admin可以写data admin可以读data 以及2个用户角色:
Alice属于admin Bob属于user 所以Alice有数据的所有权限而Bob只能读取数据。官网教程中casbin使用csv来简单的存储策略,这里我们使用数据库。casbin通常把表名命名为casbin_rule
,结构语句如下:
1 2 3 4 5 6 7 8 9 CREATE TABLE casbin_rule ( p_type VARCHAR (100 ), v0 VARCHAR (100 ), v1 VARCHAR (100 ), v2 VARCHAR (100 ) ); INSERT INTO casbin_rule VALUES ('p' , 'user' , 'resource' , 'read' );INSERT INTO casbin_rule(p_type, v0, v1) VALUES ('g' , 'Bob' , 'user' );
实现Gin的Handler 首先实现登录逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func Login (c *gin.Context) { username, password := c.PostForm("username" ), c.PostForm("password" ) u, err := uuid.NewRandom() if err != nil { log.Fatal(err) } sessionId := fmt.Sprintf("%s-%s" , u.String(), username) component.GlobalCache.Set(sessionId, []byte (username)) c.SetCookie("current_subject" , sessionId, 30 *60 , "/resource" , "" , false , true ) c.JSON(200 , component.RestResponse{Code: 1 , Message:username + " logged in successfully" }) }
如果登录成功,我们存储用户(或者叫sub)信息到缓存中,这里不要忘记将sessionId写回cookie中。casbin只负责鉴权不负责认证,所以我们要自己实现认证逻辑。接下来实现读、写逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func ReadResource (c *gin.Context) { c.JSON(200 , component.RestResponse{Code: 1 , Message: "read resource successfully" , Data: "resource" }) } func WriteResource (c *gin.Context) { c.JSON(200 , component.RestResponse{Code: 1 , Message: "write resource successfully" , Data: "resource" }) }
然后实现main.go
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 var ( router *gin.Engine ) func init () { router = gin.Default() corsConfig := cors.DefaultConfig() corsConfig.AllowAllOrigins = true corsConfig.AllowCredentials = true router.Use(cors.New(corsConfig)) router.POST("/user/login" , handler.Login) router.GET("/resource" , handler.ReadResource) router.POST("/resource" , handler.WriteResource) } func main () { defer component.DB.Close() err := router.Run(":8081" ) if err != nil { panic (fmt.Sprintf("failed to start gin engin: %v" , err)) } log.Println("application is now running..." ) }
一切就绪,接下来开始集成。
启用casbin策略 从数据库加载polices 第一个问题就是,我们如何从数据库动态加载策略?我们可以使用Casbin Adapters
,更精确的说我们使用的是Gorm Adapter
。首先进行初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func init () { adapter, err := gormadapter.NewAdapterByDB(component.DB) if err != nil { panic (fmt.Sprintf("failed to initialize casbin adapter: %v" , err)) } router = gin.Default() corsConfig := cors.DefaultConfig() corsConfig.AllowAllOrigins = true corsConfig.AllowCredentials = true router.Use(cors.New(corsConfig)) router.POST("/user/login" , handler.Login) router.GET("/resource" , handler.ReadResource) router.POST("/resource" , handler.WriteResource) }
显然的,在进行任何操作前都需要经过鉴权,所以更优雅的方式是使用gin提供的middlewares
和grouping routes
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 func Authorize (obj string , act string , adapter *gormadapter.Adapter) gin .HandlerFunc { return func (c *gin.Context) { val, existed := c.Get("current_subject" ) if !existed { c.AbortWithStatusJSON(401 , component.RestResponse{Message: "user hasn't logged in yet" }) return } ok, err := enforce(val.(string ), obj, act, adapter) if err != nil { log.Println(err) c.AbortWithStatusJSON(500 , component.RestResponse{Message: "error occurred when authorizing user" }) return } if !ok { c.AbortWithStatusJSON(403 , component.RestResponse{Message: "forbidden" }) return } c.Next() } } func enforce (sub string , obj string , act string , adapter *gormadapter.Adapter) (bool , error) { enforcer, err := casbin.NewEnforcer("config/rbac_model.conf" , adapter) if err != nil { return false , fmt.Errorf("failed to create casbin enforcer: %w" , err) } err = enforcer.LoadPolicy() if err != nil { return false , fmt.Errorf("failed to load policy from DB: %w" , err) } ok, err := enforcer.Enforce(sub, obj, act) return ok, err }
最后进行一些修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func init () { adapter, err := gormadapter.NewAdapterByDB(component.DB) if err != nil { panic (fmt.Sprintf("failed to initialize casbin adapter: %v" , err)) } router = gin.Default() corsConfig := cors.DefaultConfig() corsConfig.AllowAllOrigins = true corsConfig.AllowCredentials = true router.Use(cors.New(corsConfig)) router.POST("/user/login" , handler.Login) resource := router.Group("/api" ) { resource.GET("/resource" , middleware.Authorize("resource" , "read" , adapter), handler.ReadResource) resource.POST("/resource" , middleware.Authorize("resource" , "write" , adapter), handler.WriteResource) } }
大功告成。