- 定义了请求的结构,即请求包含的参数。
- 这里的 `r = sub, obj, act` 表示请求由三个要素组成:`sub`(主体,例如用户或角色)、`obj`(访问的对象,例如资源或 URL)、和 `act`(操作,例如读、写)。
**[policy_definition]**:
- 定义了权限策略的结构,表示策略包含的参数。
p = sub, obj, act
表示策略同样包含主体、对象和操作三部分。每条策略通过匹配这些属性来决定权限。
**[role_definition]**:
- 定义了角色的继承关系,用于角色访问控制(RBAC)。
g = _, _
表示使用角色继承的关系。例如,g(alice, admin)
表示用户 alice
是 admin
角色的成员,继承该角色的权限。
**[policy_effect]**:
- 定义策略的效果,说明在多条策略匹配时如何处理。
e = some(where (p.eft == allow))
表示当某条策略生效(即 p.eft
等于 allow
)时,允许请求通过。some(where (...))
的语法意味着只要有一条策略允许访问即可。
**[matchers]**:
- 定义了请求和策略如何匹配,以生成访问决策。
- `m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")` 规则的含义:
g(r.sub, p.sub)
:检查请求的主体 r.sub
是否具有策略中的角色 p.sub
。
keyMatch(r.obj, p.obj)
:支持 URL 模式匹配(如路径前缀匹配),判断请求对象 r.obj
是否匹配策略中的对象 p.obj
。
(r.act == p.act || p.act == "*")
:检查请求的操作 r.act
是否与策略中的操作 p.act
相符,或者策略允许任意操作(`p.act == “*”)。
authz_policy.csv文件如下
1 2 3 4
| p, moderator, /admin.v1.AdminService/GetModeratorBoard, * p, api_admin, /admin.v1.AdminService/*, * g, admin, api_admin
|
-
**`p, moderator, /admin.v1.AdminService/GetModeratorBoard, *`**
1. 这是一个权限策略 (`p`)。
1. `moderator` 角色可以访问 `/admin.v1.AdminService/GetModeratorBoard` 路径。
1. 最后一个参数是 `*`,表示对该路径上的任意操作(如 GET、POST 等)都允许。
-
**`p, api_admin, /admin.v1.AdminService/*, *`**
1. 另一个权限策略 (`p`)。
1. `api_admin` 角色可以访问 `/admin.v1.AdminService/` 下的所有路径(使用了通配符 `*`)。
1. 同样,`*` 表示允许对这些路径上的所有操作。
-
**`g, admin, api_admin`**
1. 这是一个角色继承关系 (`g`)。
1. `admin` 角色继承了 `api_admin` 角色的权限,因此拥有 `api_admin` 角色所允许的所有权限。
- 另一个权限策略 (
p
)。
api_admin
角色可以访问 /admin.v1.AdminService/
下的所有路径(使用了通配符 *
)。
- 同样,
*
表示允许对这些路径上的所有操作。
参考:Go 每日一库之 casbin 以及 golang微服务框架Kratos实现鉴权 - Casbin
可以尝试启动这个casbin项目,体会一下
项目需要依赖Consul和Jaeger
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
| docker pull bitnami/consul:latest
docker run -itd \ --name consul-server-standalone \ -p 8300:8300 \ -p 8500:8500 \ -p 8600:8600/udp \ -e CONSUL_BIND_INTERFACE='eth0' \ -e CONSUL_AGENT_MODE=server \ -e CONSUL_ENABLE_UI=true \ -e CONSUL_BOOTSTRAP_EXPECT=1 \ -e CONSUL_CLIENT_LAN_ADDRESS=0.0.0.0 \ bitnami/consul:latest
docker pull jaegertracing/all-in-one:latest
docker run -d \ --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 14250:14250 \ -p 9411:9411 \ jaegertracing/all-in-one:latest
|
启动后端,配置如下
访问swagger-ui:http://localhost:8800/q/swagger-ui
启动前端
访问前端界面:http://localhost:8081
casbin提供多种不同的适配器,支持从配置文件,MySQL存储加载和存储策略规则。
casbin在很多语言中都有支持的middleware中间件用于提供casbin的鉴权功能。
比如 https://github.com/go-kratos/examples/blob/main/casbin/app/admin/internal/server/http.go 中使用的就是 tx7do/kratos-casbin 中间件来实现的casbin鉴权,代码如下
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
| // NewMiddleware 创建中间件 func NewMiddleware(ac *conf.Auth, logger log.Logger) http.ServerOption { m, _ := model.NewModelFromFile("/Users/lintong/coding/go/examples/casbin/app/admin/configs/authz/authz_model.conf") a := fileAdapter.NewAdapter("/Users/lintong/coding/go/examples/casbin/app/admin/configs/authz/authz_policy.csv")
return http.Middleware( recovery.Recovery(), tracing.Server(), logging.Server(logger), selector.Server( jwt.Server( func(token *jwtV4.Token) (interface{}, error) { return []byte(ac.ApiKey), nil }, jwt.WithSigningMethod(jwtV4.SigningMethodHS256), ), casbinM.Server( casbinM.WithCasbinModel(m), casbinM.WithCasbinPolicy(a), casbinM.WithSecurityUserCreator(myAuthz.NewSecurityUser), ), ). Match(NewWhiteListMatcher()).Build(), ) }
|
而 SecurityUser 于创建Jwt的令牌,以及后面Casbin解析和存取权鉴相关的数据,需要实现其如下方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| type SecurityUser interface { // ParseFromContext parses the user from the context. ParseFromContext(ctx context.Context) error
// GetSubject returns the subject of the token. GetSubject() string
// GetObject returns the object of the token. GetObject() string
// GetAction returns the action of the token. GetAction() string // GetDomain returns the domain of the token. GetDomain() string }
|
代码如下
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| package authz
import ( "context" "errors" "fmt"
"github.com/go-kratos/kratos/v2/middleware/auth/jwt" "github.com/go-kratos/kratos/v2/transport" jwtV4 "github.com/golang-jwt/jwt/v4" authzM "github.com/tx7do/kratos-casbin/authz" )
const ( ClaimAuthorityId = "authorityId" )
type SecurityUser struct { Path string Method string AuthorityId string Domain string }
func NewSecurityUser() authzM.SecurityUser { return &SecurityUser{} }
func (su *SecurityUser) ParseFromContext(ctx context.Context) error { if claims, ok := jwt.FromContext(ctx); ok { su.AuthorityId = claims.(jwtV4.MapClaims)[ClaimAuthorityId].(string) } else { return errors.New("jwt claim missing") }
if header, ok := transport.FromServerContext(ctx); ok { su.Path = header.Operation() su.Method = "*" } else { return errors.New("jwt claim missing") }
return nil }
func (su *SecurityUser) GetSubject() string { return su.AuthorityId }
func (su *SecurityUser) GetObject() string { return su.Path }
func (su *SecurityUser) GetAction() string { return su.Method }
func (su *SecurityUser) GetDomain() string { return su.Domain }
|
参考:https://tx7do.github.io/docs/kratos_auth_authz.html
3.casbin和gin集成
1.basic auth+casbin
下面举一个gin框架集成basic auth和casbin框架的例子
basic auth集成参考:https://gin-gonic.com/docs/examples/using-basicauth-middleware/
首先添加一个basic auth的middleware
1 2 3 4 5 6 7 8 9
| // basic auth authMiddleware := gin.BasicAuth(gin.Accounts{ "foo": "bar", //用户名:密码 "austin": "1234", "lena": "hello2", "admin": "admin", }) authGroup := r.Group("/api/v1", authMiddleware)
|
再基于basic auth添加casbin,参考:https://github.com/gin-contrib/authz
1 2
| go get github.com/gin-contrib/authz
|
注意这个库需要基于basic auth
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func NewEnforcer() (*casbin.Enforcer, error) { enforcer, err := casbin.NewEnforcer("./conf/authz_model.conf", "./conf/authz_policy.csv") if err != nil { panic(err) } return enforcer, nil }
// casbin enforcer, err := auth.NewEnforcer() if err != nil { panic(err) } authGroup.Use(authz.NewAuthorizer(enforcer))
// handler UserRouters(userHandler, authGroup)
|
其中userHandler中有如下接口用于获取user
authz_model.conf访问控制模型配置文件,其中keyMatch用于匹配/api/v1/user/*的path,参考:https://casbin.org/docs/function/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| [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) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
|
authz_policy.csv权限策略文件
1 2 3 4 5 6
| p, admin, /api/v1/user/*, * p, admin, /api/v1/user, POST p, foo, /api/v1/user/1, GET p, austin, /api/v1/user, POST p, lena, /api/v1/user, PUT
|
admin用户拥有创建user的权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| curl -i 'http://localhost:18080/api/v1/user' \ --header 'Content-Type: application/json' \ --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ --data-raw '{ "username": "test", "email":"test@test" }' HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sat, 23 Nov 2024 15:48:38 GMT Content-Length: 98
{"code":200,"msg":"create user success","data":{"id":10025,"username":"test","email":"test@test"}}%
|
foo用户只有访问id=1的用户的权限,访问id=2会返回403
1 2 3 4 5 6 7
| curl -I -X GET 'http://localhost:18080/api/v1/user/2' \ --header 'Authorization: Basic Zm9vOmJhcg==' \ --header 'Content-Type: application/json' HTTP/1.1 403 Forbidden Date: Sun, 17 Nov 2024 16:21:01 GMT Content-Length: 0
|
2.使用GORM adapter
如果使用MySQL存储policy的话,可以使用**gorm-adapter**,参考:https://casbin.org/zh/docs/adapters/ 以及 https://dev.to/maxwellhertz/tutorial-integrate-gin-with-cabsin-56m0
1 2
| go get github.com/casbin/gorm-adapter/v3
|
并在enforce中从csv文件的权限策略适配器,修改成gorm的适配器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| func NewEnforcer(adapter *gormadapter.Adapter) (*casbin.Enforcer, error) { //enforcer, err := casbin.NewEnforcer("./conf/authz_model.conf", "./conf/authz_policy.csv") enforcer, err := casbin.NewEnforcer("./conf/authz_model.conf", adapter) if err != nil { panic(err) } return enforcer, nil }
func NewGormAdapter(conf *viper.Viper) (*gormadapter.Adapter, error) { dsn := conf.GetString("db.mysql.dsn") print(dsn) adapter, err := gormadapter.NewAdapter("mysql", dsn, true) if err != nil { panic(err) } return adapter, nil }
|
运行代码后,会自动在MySQL数据库中创建一张名为casbin_rule的表,如下
表定义应为(id int primary key, ptype varchar, v0 varchar, v1 varchar, v2 varchar, v3 varchar, v4 varchar, v5 varchar)。
唯一键索引应在ptype,v0,v1,v2,v3,v4,v5列上建立。
使用如下代码可以添加policy
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 42 43 44
| package auth
import ( "fmt" "gin-template/internal/config" zaplog "gin-template/internal/logger" "github.com/spf13/viper" "go.uber.org/zap" "testing" )
var ( conf *viper.Viper zap_logger *zap.Logger )
func init() { conf = config.InitConfig() zap_logger = zaplog.InitLogger(conf) }
func TestNewGormAdapter(t *testing.T) { adapter, err := NewGormAdapter(conf) if err != nil { fmt.Println(err) return } enforcer, err := NewEnforcer(adapter) if err != nil { fmt.Println(err) return } _, err = enforcer.AddPolicy("jack", "data1", "write") if err != nil { fmt.Println(err) return } _, err = enforcer.AddPolicy("jack", "data2", "write") if err != nil { fmt.Println(err) return } }
|
参考:https://github.com/casbin/gorm-adapter
如果要移除policy
1 2 3 4 5 6 7 8 9 10 11 12 13
| // 删除policy _, err = enforcer.RemovePolicy("jack", "data3", "write") if err != nil { fmt.Println(err) return } // 删除index=0位置的值等于jack的policy _, err = enforcer.RemoveFilteredPolicy(0, "jack") if err != nil { fmt.Println(err) return }
|
3.刷新权限policy
当权限policy变更了之后,为了保证鉴权结果的正确,需要即使加载最新的权限policy,方法有如下几种
1.定时任务调用 LoadPolicy 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // 开启定时自动加载策略 go func() { ticker := time.NewTicker(10 * time.Second) // 每隔 10 秒加载一次 defer ticker.Stop() for range ticker.C { err := enforcer.LoadPolicy() if err != nil { // 记录错误日志 fmt.Println("Failed to load policy:", err.Error()) } else { fmt.Println("Policy reloaded successfully.") } } }()
|
2.手动调用 LoadPolicy 方法
``在要鉴权之前,调用如下代码
3.使用 Watcher 机制
可以使用watcher来通知每个实例的enforcer更新权限策略,参考:https://casbin.org/docs/watchers/
这里以 https://github.com/casbin/redis-watcher 为例,使用这个redis-watcher需要依赖golang redis v9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // casbin watcher watcher, _ := rediswatcher.NewWatcher("localhost:3306", rediswatcher.WatcherOptions{ Options: redis.Options{ Network: "tcp", Password: "", }, Channel: "/casbin", // Only exists in test, generally be true IgnoreSelf: false, }) err := enforcer.SetWatcher(watcher) if err != nil { zap.L().Error(err.Error()) } err = watcher.SetUpdateCallback(func(msg string) { println(msg) enforcer.LoadPolicy() }) if err != nil { zap.L().Error(err.Error()) }
|
其中updateCallback是回调函数,这里打印一下权限变更的msg
写一个测试用例添加一下policy,这里也设置了watcher,会将权限变更的信息写到redis中
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
| func TestNewGormAdapter(t *testing.T) { adapter, err := NewGormAdapter(conf) if err != nil { fmt.Println(err) return } enforcer, err := NewEnforcer(adapter) if err != nil { fmt.Println(err) return } watcher, _ := rediswatcher.NewWatcher("localhost:3306", rediswatcher.WatcherOptions{ Options: redis.Options{ Network: "tcp", Password: "", }, Channel: "/casbin", // Only exists in test, generally be true IgnoreSelf: false, }) err = enforcer.SetWatcher(watcher)
if err != nil { zap.L().Error(err.Error()) } _, err = enforcer.AddPolicy("jack", "data1", "write") if err != nil { fmt.Println(err) return } _, err = enforcer.AddPolicy("jack", "data2", "write") if err != nil { fmt.Println(err) return } }
|
查看原先的日志,获取到2次policy变更的消息
1 2 3
| {"Method":"UpdateForAddPolicy","ID":"a14b5075-bd4c-43fa-b987-8e500d9fcf46","Sec":"p","Ptype":"p","OldRule":null,"OldRules":null,"NewRule":["jack","data1","write"],"NewRules":null,"FieldIndex":0,"FieldValues":null} {"Method":"UpdateForAddPolicy","ID":"a14b5075-bd4c-43fa-b987-8e500d9fcf46","Sec":"p","Ptype":"p","OldRule":null,"OldRules":null,"NewRule":["jack","data2","write"],"NewRules":null,"FieldIndex":0,"FieldValues":null}
|
4.casbin管理界面
casbin生态提供了admin-portal用于模型管理和策略管理,参考:https://casbin.org/zh/docs/admin-portal
casdoor就是其中之一(别用casbin-hub这个项目,已经过时),官网:https://casdoor.org/
clone项目
1 2
| git clone https://github.com/casdoor/casdoor
|
修改配置文件app.conf中的数据库地址:dataSourceName
启动后端
后端会自动在MySQL中创建一个casdoor的数据库
启动前端
1 2 3 4
| cd web yarn install yarn start
|
参考:https://casdoor.org/docs/basic/server-installation/
界面如下,默认的账号密码是admin 123
在casbin管理中可以添加casbin的model,adapter和enforcer
添加casbin model模型,即模型控制文件,比如上面的 authz_model.conf
添加casbin adapter适配器,需要指定MySQL表的名字,这里指定table的名字为casbin_rule,和GORM adapter默认创建的table名字保持一致
这个当然也可以选择和casdoor不同的MySQL database
添加casbin enforcer执行器,需要指定model和adapter,同时在enforcer中添加policy,如下
对应MySQL表中已经添加了该policy