集成gin和casbin
原文,省略了一些无关的内容。
如果你搜到这篇文章,那么什么是gin以及casbin应该不用过多解释了。
项目结构
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
处理缓存:
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
:
[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
举个例子:
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
,结构语句如下:
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
首先实现登录逻辑
// 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只负责鉴权不负责认证,所以我们要自己实现认证逻辑。接下来实现读、写逻辑:
// 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
:
// 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
。首先进行初始化:
// 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提供的middlewares
和grouping routes
:
// 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
}
最后进行一些修改:
// 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)
}
}
大功告成。