原文,省略了一些无关的内容。

如果你搜到这篇文章,那么什么是gin以及casbin应该不用过多解释了。

项目结构

1
2
3
4
5
6
root/
main.go # entry point of application
handler/ # Gin handler functions
middleware/ # Gin middlewares
config/ # some configuration files like Casbin's rbac_model.conf
component/ # global components like GORM DB instance

初始化数据库和缓存

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() {
// Connect to DB
var err error
DB, err = gorm.Open("mysql", "your_db_url")
if err != nil {
panic(fmt.Sprintf("failed to connect to DB: %v", err))
}

// Initialize cache
GlobalCache, err = bigcache.NewBigCache(bigcache.DefaultConfig(30 * time.Minute)) // Set expire time to 30 mins
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个字段:

  1. r = sub, obj, act 定义了一个请求需要由3部分组成:sub=用户,obj=URL或资源,act=操作。
  2. p = sub, obj, act 定义了策略的格式,比如admin,dada,write表示admin有data的写权限。
  3. e = some(where (p.eft == allow)) 定义了用户可以做那些策略中定义准许他做的事。
  4. g = _, _ 定义了角色的格式,例如bob,admin表示用户bob是admin这个角色。
  5. m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 定义了鉴权时的流程,先检查用户角色,再检查用户访问的资源,最后检查用户行为。

上面几个部分,仅1、2、3、5是必须的,如果不使用RBAC可以忽略4。

Roy注:下面的这个更常用。
[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, “/auth”) == 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个策略:

  1. user可以读取data
  2. admin可以写data
  3. admin可以读data

以及2个用户角色:

  1. Alice属于admin
  2. 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', 'data', '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
// handler/user_handler.go

func Login(c *gin.Context) {
username, password := c.PostForm("username"), c.PostForm("password")
// Authentication
// blahblah...

// Generate random session id
u, err := uuid.NewRandom()
if err != nil {
log.Fatal(err)
}
sessionId := fmt.Sprintf("%s-%s", u.String(), username)
// Store current subject in cache
component.GlobalCache.Set(sessionId, []byte(username))
// Send cache key back to client in cookie
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
// handler/resource_handler.go

func ReadResource(c *gin.Context) {
// some stuff
// blahblah...

c.JSON(200, component.RestResponse{Code: 1, Message: "read resource successfully", Data: "resource"})
}

func WriteResource(c *gin.Context) {
// some stuff
// blahblah...

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
// main.go

var (
router *gin.Engine
)

func init() {
// Initialize gin router
router = gin.Default()
corsConfig := cors.DefaultConfig()
corsConfig.AllowAllOrigins = true
corsConfig.AllowCredentials = true
router.Use(cors.New(corsConfig)) // CORS configuraion
router.POST("/user/login", handler.Login)
router.GET("/resource", handler.ReadResource)
router.POST("/resource", handler.WriteResource)
}

func main() {
defer component.DB.Close()

// Start our application
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
// main.go

func init() {
// Initialize casbin adapter
adapter, err := gormadapter.NewAdapterByDB(component.DB)
if err != nil {
panic(fmt.Sprintf("failed to initialize casbin adapter: %v", err))
}

// Initialize gin router
router = gin.Default()
corsConfig := cors.DefaultConfig()
corsConfig.AllowAllOrigins = true
corsConfig.AllowCredentials = true
router.Use(cors.New(corsConfig)) // CORS configuraion
router.POST("/user/login", handler.Login)
router.GET("/resource", handler.ReadResource)
router.POST("/resource", handler.WriteResource)
}

显然的,在进行任何操作前都需要经过鉴权,所以更优雅的方式是使用gin提供的middlewaresgrouping 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
// middleware/access_control.go

// Authorize determines if current subject has been authorized to take an action on an object.
func Authorize(obj string, act string, adapter *gormadapter.Adapter) gin.HandlerFunc {
return func(c *gin.Context) {
// Get current user/subject
val, existed := c.Get("current_subject")
if !existed {
c.AbortWithStatusJSON(401, component.RestResponse{Message: "user hasn't logged in yet"})
return
}
// Casbin enforces policy
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) {
// Load model configuration file and policy store adapter
enforcer, err := casbin.NewEnforcer("config/rbac_model.conf", adapter)
if err != nil {
return false, fmt.Errorf("failed to create casbin enforcer: %w", err)
}
// Load policies from DB dynamically
err = enforcer.LoadPolicy()
if err != nil {
return false, fmt.Errorf("failed to load policy from DB: %w", err)
}
// Verify
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
// main.go

func init() {
// Initialize casbin adapter
adapter, err := gormadapter.NewAdapterByDB(component.DB)
if err != nil {
panic(fmt.Sprintf("failed to initialize casbin adapter: %v", err))
}

// Initialize Gin router
router = gin.Default()
corsConfig := cors.DefaultConfig()
corsConfig.AllowAllOrigins = true
corsConfig.AllowCredentials = true
router.Use(cors.New(corsConfig)) // CORS configuraion
router.POST("/user/login", handler.Login)
// Secure our API
resource := router.Group("/api")
{
resource.GET("/resource", middleware.Authorize("resource", "read", adapter), handler.ReadResource)
resource.POST("/resource", middleware.Authorize("resource", "write", adapter), handler.WriteResource)
}
}

大功告成。