10

集成gin和casbin

 3 years ago
source link: http://www.hi-roy.com/2020/09/28/%E9%9B%86%E6%88%90gin%E5%92%8Ccasbin/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

集成gin和casbin

9月 28, 2020 发布在 Golang, 菜鸟翻译屋

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

如果你搜到这篇文章,那么什么是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注:下面的这个更常用。程序中判断如果角色是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

举个例子:

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', '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
// 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)
}
}

大功告成。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK