RBAC (Role Based Access Control, 基于角色的访问控制)

准备基础的 Gin 项目

新建 gin-rbac 文件夹

mkdir gin-rbac

进入该目录, 初始化项目, 获取依赖

go mod init gin-rbac
go get -u github.com/gin-gonic/gin
go get -u github.com/casbin/casbin/v2
go get -u github.com/gin-contrib/authz

准备 Casbin

gin-rbac 目录下新建 auth 目录, 在该目录下

新建 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

新建 policy.csv:

p, admin, data1, read
p, admin, data1, write
p, data_entry, data1, write
p, guest, data1, read

g, alice, admin
g, bob, data_entry
g, charlie, guest

gin-rbac 目录下新建 casbin_test.go 并测试:

package main
import (
	"testing"
	"github.com/casbin/casbin/v2"
)
func TestEnforce(t *testing.T) {
	e, _ := casbin.NewEnforcer("auth/model.conf", "auth/policy.csv")
	sub := "alice"
	obj := "data1"
	act := "read"
	if res, _ := e.Enforce(sub, obj, act); res {
        // 允许 alice read data1
		t.Log("alice can read data1")
	} else {
		// 拒绝请求
		t.Log("alice can NOT read data1")
	}
	// alice can read data1

	sub = "bob"
	obj = "data1"
	act = "read"
	if res, _ := e.Enforce(sub, obj, act); res {
		t.Log("bob can read data1")
	} else {
		t.Log("bob can NOT read data1")
	}
	// bob can NOT read data1

	sub = "bob"
	obj = "data1"
	act = "write"
	if res, _ := e.Enforce(sub, obj, act); res {
		t.Log("bob can write data1")
	} else {
		t.Log("bob can NOT write data1")
	}
	// bob can write data1
}

中间件 gin-contrib/authz

github 主页 给出的例子

package main
import (
  "net/http"
  "github.com/casbin/casbin/v2"
  "github.com/gin-contrib/authz"
  "github.com/gin-gonic/gin"
)
func main() {
  e := casbin.NewEnforcer("authz_model.conf", "authz_policy.csv")
  router := gin.New()
  router.Use(authz.NewAuthorizer(e))
}

一旦认证失败就返回 HTTP 403.

查看源码 https://github.com/gin-contrib/authz/blob/master/authz.go

得知其用到了 Basic 认证, 其中关键代码:

func (a *BasicAuthorizer) CheckPermission(r *http.Request) bool {
	user := a.GetUserName(r) // 从 Basic 认证中拿到用户名
	method := r.Method       // http 请求方法, 例如 GET / POST
	path := r.URL.Path		 // 请求路径, 例如 /data1/read /data1/write
	allowed, err := a.enforcer.Enforce(user, path, method)
	if err != nil {
		panic(err)
	}
	return allowed
}

整合 Casbin 和 Gin

新建 auth/authz_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

新建 auth/authz_policy.csv:

p, admin, /data1/read, GET
p, admin, /data1/write, POST
p, data_entry, /data1/write, POST
p, guest, /data1/read, GET

g, alice, admin
g, bob, data_entry
g, charlie, guest

新建 gin-rbac/main.go:

package main
import (
	"github.com/casbin/casbin/v2"
	"github.com/gin-contrib/authz"
	"github.com/gin-gonic/gin"
)
func main() {
	r := gin.Default()
	enforcer, err := casbin.NewEnforcer("auth/authz_model.conf", "auth/authz_policy.csv")
	if err != nil {
		panic(err)
	}
	r.Use(authz.NewAuthorizer(enforcer))
	r.GET("/data1/read", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "You can read data1"})
	})
	r.POST("/data1/write", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "You can write data1"})
	})
	r.Run()
}

使用 httpie 发送请求:

  • alice 有读和写权限, 因为她的角色是 admin:

    http -v -a alice:alice GET 'localhost:8080/data1/read'
    
    GET /data1/read HTTP/1.1
    Accept: */*
    Accept-Encoding: gzip, deflate
    Authorization: Basic YWxpY2U6YWxpY2U=
    Connection: keep-alive
    Host: localhost:8080
    User-Agent: HTTPie/2.6.0
    
    
    
    HTTP/1.1 200 OK
    Content-Length: 32
    Content-Type: application/json; charset=utf-8
    Date: Mon, 09 Oct 2023 09:04:41 GMT
    
    {
        "message": "You can read data1"
    }
    
  • someone 是匿名用户, 他什么权限都没有

    http -v -a someone:xx GET 'localhost:8080/data1/read'
    
    GET /data1/read HTTP/1.1
    Accept: */*
    Accept-Encoding: gzip, deflate
    Authorization: Basic c29tZW9uZTp4eA==
    Connection: keep-alive
    Host: localhost:8080
    User-Agent: HTTPie/2.6.0
    
    
    
    HTTP/1.1 403 Forbidden
    Content-Length: 0
    Date: Mon, 09 Oct 2023 09:05:52 GMT
    
  • bob 只有写权限, 他的角色是 data_entry

    http -v -a bob:bob GET 'localhost:8080/data1/read'
    
    GET /data1/read HTTP/1.1
    Accept: */*
    Accept-Encoding: gzip, deflate
    Authorization: Basic Ym9iOmJvYg==
    Connection: keep-alive
    Host: localhost:8080
    User-Agent: HTTPie/2.6.0
    
    
    
    HTTP/1.1 403 Forbidden
    Content-Length: 0
    Date: Mon, 09 Oct 2023 09:06:32 GMT
    
    http -v -a bob:bob POST 'localhost:8080/data1/write'
    
    Accept: */*
    Accept-Encoding: gzip, deflate
    Authorization: Basic Ym9iOmJvYg==
    Connection: keep-alive
    Content-Length: 0
    Host: localhost:8080
    User-Agent: HTTPie/2.6.0
    
    
    
    HTTP/1.1 200 OK
    Content-Length: 33
    Content-Type: application/json; charset=utf-8
    Date: Mon, 09 Oct 2023 09:07:21 GMT
    
    {
        "message": "You can write data1"
    }
    

参考