項目地址: https://github.com/storyicon/grbac
Grbac是一個快速,優(yōu)雅和簡潔的RBAC框架樟氢。它支持增強的通配符并使用Radix樹匹配HTTP請求豺瘤。令人驚奇的是辆床,您可以在任何現(xiàn)有的數(shù)據(jù)庫和數(shù)據(jù)結(jié)構(gòu)中輕松使用它愿卸。
grbac的作用是確保指定的資源只能由指定的角色訪問灵临。請注意,grbac不負責(zé)存儲鑒權(quán)規(guī)則和分辨“當(dāng)前請求發(fā)起者具有哪些角色”趴荸,更不負責(zé)角色的創(chuàng)建儒溉、分配等。這意味著您應(yīng)該首先配置規(guī)則信息发钝,并提供每個請求的發(fā)起者具有的角色顿涣。
grbac將Host
波闹、Path
和Method
的組合視為Resource
,并將Resource
綁定到一組角色規(guī)則(稱為Permission
)园骆。只有符合這些規(guī)則的用戶才能訪問相應(yīng)的Resource
舔痪。
讀取鑒權(quán)規(guī)則的組件稱為Loader
。grbac預(yù)置了一些Loader
锌唾,你也可以通過實現(xiàn)func()(grbac.Rules,error)
來根據(jù)你的設(shè)計來自定義Loader
夺英,并通過grbac.WithLoader
加載它晌涕。
1. 最常見的用例
下面是最常見的用例,它使用gin
痛悯,并將grbac
包裝成了一個中間件余黎。通過這個例子,你可以很容易地知道如何在其他http框架中使用grbac
(比如echo
载萌,iris
惧财,ace
等):
package main
import (
"github.com/gin-gonic/gin"
"github.com/storyicon/grbac"
"net/http"
"time"
)
func LoadAuthorizationRules() (rules grbac.Rules, err error) {
// 在這里實現(xiàn)你的邏輯
// ...
// 你可以從數(shù)據(jù)庫或文件加載授權(quán)規(guī)則
// 但是你需要以 grbac.Rules 的格式返回你的身份驗證規(guī)則
// 提示:你還可以將此函數(shù)綁定到golang結(jié)構(gòu)體
return
}
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
// 在這里實現(xiàn)你的邏輯
// ...
// 這個邏輯可能是從請求的Headers中獲取token,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色扭仁。
return roles, err
}
func Authorization() gin.HandlerFunc {
// 在這里垮衷,我們通過“grbac.WithLoader”接口使用自定義Loader功能
// 并指定應(yīng)每分鐘調(diào)用一次LoadAuthorizationRules函數(shù)以獲取最新的身份驗證規(guī)則。
// Grbac還提供一些現(xiàn)成的Loader:
// grbac.WithYAML
// grbac.WithRules
// grbac.WithJSON
// ...
rbac, err := grbac.New(grbac.WithLoader(LoadAuthorizationRules, time.Minute))
if err != nil {
panic(err)
}
return func(c *gin.Context) {
roles, err := QueryRolesByHeaders(c.Request.Header)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
state, _ := rbac.IsRequestGranted(c.Request, roles)
if !state.IsGranted() {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
}
func main(){
c := gin.New()
c.Use(Authorization())
// 在這里通過c.Get乖坠、c.Post等函數(shù)綁定你的API
// ...
c.Run(":8080")
}
2. 概念
這里有一些關(guān)于grbac
的概念搀突。這很簡單,你可能只需要三分鐘就能理解熊泵。
2.1. Rule
// Rule即規(guī)則仰迁,用于定義Resource和Permission之間的關(guān)系
type Rule struct {
// ID決定了Rule的優(yōu)先級。
// ID值越大意味著Rule的優(yōu)先級越高顽分。
// 當(dāng)請求被多個規(guī)則同時匹配時徐许,grbac將僅使用具有最高ID值的規(guī)則。
// 如果有多個規(guī)則同時具有最大的ID卒蘸,則將隨機使用其中一個規(guī)則雌隅。
ID int `json:"id"`
*Resource
*Permission
}
如你所見,Rule
由三部分組成:ID
悬秉,Resource
和Permission
澄步。
“ID”確定規(guī)則的優(yōu)先級。
當(dāng)請求同時滿足多個規(guī)則時(例如在通配符中)和泌,
grbac
將選擇具有最高ID的那個村缸,然后使用其權(quán)限定義進行身份驗證。
如果有多個規(guī)則同時具有最大的ID武氓,則將隨機使用其中一個規(guī)則(所以請避免這種情況)梯皿。
下面有一個非常簡單的例子:
#Rule
- id: 0
# Resource
host: "*"
path: "**"
method: "*"
# Permission
authorized_roles:
- "*"
forbidden_roles: []
allow_anyone: false
#Rule
- id: 1
# Resource
host: domain.com
path: "/article"
method: "{DELETE,POST,PUT}"
# Permission
authorized_roles:
- editor
forbidden_roles: []
allow_anyone: false
在以yaml格式編寫的此配置文件中仇箱,ID=0 的規(guī)則表明任何具有任何角色的人都可以訪問所有資源。
但是ID=1的規(guī)則表明只有editor
可以對文章進行增刪改操作东羹。
這樣剂桥,除了文章的操作只能由editor
訪問之外,任何具有任何角色的人都可以訪問所有其他資源属提。
2.2. Resource
type Resource struct {
// Host 定義資源的Host权逗,允許使用增強的通配符。
Host string `json:"host"`
// Path 定義資源的Path冤议,允許使用增強的通配符斟薇。
Path string `json:"path"`
// Method 定義資源的Method,允許使用增強的通配符恕酸。
Method string `json:"method"`
}
Resource用于描述Rule適用的資源堪滨。
當(dāng)執(zhí)行IsRequestGranted(c.Request,roles)
時蕊温,grbac首先將當(dāng)前的Request
與所有Rule
中的Resources
匹配袱箱。
Resource的每個字段都支持增強的通配符
2.3. Permission
// Permission用于定義權(quán)限控制信息
type Permission struct {
// AuthorizedRoles定義允許訪問資源的角色
// 支持的類型: 非空字符串,*
// *: 意味著任何角色义矛,但訪問者應(yīng)該至少有一個角色发笔,
// 非空字符串:指定的角色
AuthorizedRoles []string `json:"authorized_roles"`
// ForbiddenRoles 定義不允許訪問指定資源的角色
// ForbiddenRoles 優(yōu)先級高于AuthorizedRoles
// 支持的類型:非空字符串,*
// *: 意味著任何角色症革,但訪問者應(yīng)該至少有一個角色筐咧,
// 非空字符串:指定的角色
//
ForbiddenRoles []string `json:"forbidden_roles"`
// AllowAnyone的優(yōu)先級高于 ForbiddenRoles、AuthorizedRoles
// 如果設(shè)置為true噪矛,任何人都可以通過驗證量蕊。
// 請注意,這將包括“沒有角色的人”艇挨。
AllowAnyone bool `json:"allow_anyone"`
}
“Permission”用于定義綁定到的“Resource”的授權(quán)規(guī)則残炮。
這是易于理解的,當(dāng)請求者的角色符合“Permission”的定義時缩滨,他將被允許訪問Resource势就,否則他將被拒絕訪問。
為了加快驗證的速度脉漏,Permission
中的字段不支持“增強的通配符”苞冯。
在AuthorizedRoles
和ForbiddenRoles
中只允許*
表示所有。
2.4. Loader
Loader用于加載Rule侧巨。 grbac預(yù)置了一些加載器舅锄,你也可以通過實現(xiàn)func()(grbac.Rules, error)
來自定義加載器并通過 grbac.WithLoader
加載它。
method | description |
---|---|
WithJSON(path, interval) | 定期從json 文件加載規(guī)則配置 |
WithYaml(path, interval) | 定期從yaml 文件加載規(guī)則配置 |
WithRules(Rules) | 從grbac.Rules 加載規(guī)則配置 |
WithAdvancedRules(loader.AdvancedRules) | 以一種更緊湊的方式定義Rule司忱,并使用loader.AdvancedRules 加載 |
WithLoader(loader func()(Rules, error), interval) | 使用自定義函數(shù)定期加載規(guī)則 |
interval
定義了Rules的重載周期皇忿。
當(dāng)interval <0
時畴蹭,grbac
會放棄周期加載Rules配置;
當(dāng)interval∈[0,1s)
時,grbac
會自動將interval
設(shè)置為5s
;
3. 其他例子
這里有一些簡單的例子鳍烁,可以讓你更容易理解grbac
的工作原理叨襟。
雖然grbac
在大多數(shù)http框架中運行良好,但很抱歉我現(xiàn)在只使用gin幔荒,所以如果下面的例子中有一些缺陷糊闽,請告訴我。
3.1. gin && grbac.WithJSON
如果你想在JSON
文件中編寫配置文件爹梁,你可以通過grbac.WithJSON(filepath墓怀,interval)
加載它,filepath
是你的json文件路徑卫键,并且grbac將每隔interval重新加載一次文件。 虱朵。
[
{
"id": 0,
"host": "*",
"path": "**",
"method": "*",
"authorized_roles": [
"*"
],
"forbidden_roles": [
"black_user"
],
"allow_anyone": false
},
{
"id":1,
"host": "domain.com",
"path": "/article",
"method": "{DELETE,POST,PUT}",
"authorized_roles": ["editor"],
"forbidden_roles": [],
"allow_anyone": false
}
]
以上是“JSON”格式的身份驗證規(guī)則示例莉炉。它的結(jié)構(gòu)基于grbac.Rules。
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
// 在這里實現(xiàn)你的邏輯
// ...
// 這個邏輯可能是從請求的Headers中獲取token碴犬,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色絮宁。
return roles, err
}
func Authentication() gin.HandlerFunc {
rbac, err := grbac.New(grbac.WithJSON("config.json", time.Minute * 10))
if err != nil {
panic(err)
}
return func(c *gin.Context) {
roles, err := QueryRolesByHeaders(c.Request.Header)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
state, err := rbac.IsRequestGranted(c.Request, roles)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if !state.IsGranted() {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
}
}
func main(){
c := gin.New()
c.Use(Authentication())
// 在這里通過c.Get、c.Post等函數(shù)綁定你的API
// ...
c.Run(":8080")
}
3.2. echo && grbac.WithYaml
如果你想在YAML
文件中編寫配置文件服协,你可以通過grbac.WithYAML(file绍昂,interval)
加載它,file
是你的yaml文件路徑偿荷,并且grbac將每隔一個interval重新加載一次文件窘游。
#Rule
- id: 0
# Resource
host: "*"
path: "**"
method: "*"
# Permission
authorized_roles:
- "*"
forbidden_roles: []
allow_anyone: false
#Rule
- id: 1
# Resource
host: domain.com
path: "/article"
method: "{DELETE,POST,PUT}"
# Permission
authorized_roles:
- editor
forbidden_roles: []
allow_anyone: false
以上是“YAML”格式的認證規(guī)則的示例。它的結(jié)構(gòu)基于grbac.Rules跳纳。
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
// 在這里實現(xiàn)你的邏輯
// ...
// 這個邏輯可能是從請求的Headers中獲取token忍饰,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色。
return roles, err
}
func Authentication() echo.MiddlewareFunc {
rbac, err := grbac.New(grbac.WithYAML("config.yaml", time.Minute * 10))
if err != nil {
panic(err)
}
return func(echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
roles, err := QueryRolesByHeaders(c.Request().Header)
if err != nil {
c.NoContent(http.StatusInternalServerError)
return nil
}
state, err := rbac.IsRequestGranted(c.Request(), roles)
if err != nil {
c.NoContent(http.StatusInternalServerError)
return nil
}
if state.IsGranted() {
return nil
}
c.NoContent(http.StatusUnauthorized)
return nil
}
}
}
func main(){
c := echo.New()
c.Use(Authentication())
// 在這里通過c.Get寺庄、c.Post等函數(shù)綁定你的API
// ...
}
3.3. iris && grbac.WithRules
如果你想直接在代碼中編寫認證規(guī)則艾蓝,grbac.WithRules(rules)
提供了這種方式,你可以像這樣使用它:
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
// 在這里實現(xiàn)你的邏輯
// ...
// 這個邏輯可能是從請求的Headers中獲取token斗塘,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色赢织。
return roles, err
}
func Authentication() iris.Handler {
var rules = grbac.Rules{
{
ID: 0,
Resource: &grbac.Resource{
Host: "*",
Path: "**",
Method: "*",
},
Permission: &grbac.Permission{
AuthorizedRoles: []string{"*"},
ForbiddenRoles: []string{"black_user"},
AllowAnyone: false,
},
},
{
ID: 1,
Resource: &grbac.Resource{
Host: "domain.com",
Path: "/article",
Method: "{DELETE,POST,PUT}",
},
Permission: &grbac.Permission{
AuthorizedRoles: []string{"editor"},
ForbiddenRoles: []string{},
AllowAnyone: false,
},
},
}
rbac, err := grbac.New(grbac.WithRules(rules))
if err != nil {
panic(err)
}
return func(c context.Context) {
roles, err := QueryRolesByHeaders(c.Request().Header)
if err != nil {
c.StatusCode(http.StatusInternalServerError)
c.StopExecution()
return
}
state, err := rbac.IsRequestGranted(c.Request(), roles)
if err != nil {
c.StatusCode(http.StatusInternalServerError)
c.StopExecution()
return
}
if !state.IsGranted() {
c.StatusCode(http.StatusUnauthorized)
c.StopExecution()
return
}
}
}
func main(){
c := iris.New()
c.Use(Authentication())
// 在這里通過c.Get、c.Post等函數(shù)綁定你的API
// ...
}
3.4. ace && grbac.WithAdvancedRules
如果你想直接在代碼中編寫認證規(guī)則馍盟,grbac.WithAdvancedRules(rules)
提供了這種方式于置,你可以像這樣使用它:
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
// 在這里實現(xiàn)你的邏輯
// ...
// 這個邏輯可能是從請求的Headers中獲取token,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色朽合。
return roles, err
}
func Authentication() ace.HandlerFunc {
var advancedRules = loader.AdvancedRules{
{
Host: []string{"*"},
Path: []string{"**"},
Method: []string{"*"},
Permission: &grbac.Permission{
AuthorizedRoles: []string{},
ForbiddenRoles: []string{"black_user"},
AllowAnyone: false,
},
},
{
Host: []string{"domain.com"},
Path: []string{"/article"},
Method: []string{"PUT","DELETE","POST"},
Permission: &grbac.Permission{
AuthorizedRoles: []string{"editor"},
ForbiddenRoles: []string{},
AllowAnyone: false,
},
},
}
auth, err := grbac.New(grbac.WithAdvancedRules(advancedRules))
if err != nil {
panic(err)
}
return func(c *ace.C) {
roles, err := QueryRolesByHeaders(c.Request.Header)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
state, err := auth.IsRequestGranted(c.Request, roles)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if !state.IsGranted() {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
}
func main(){
c := ace.New()
c.Use(Authentication())
// 在這里通過c.Get俱两、c.Post等函數(shù)綁定你的API
// ...
}
loader.AdvancedRules
試圖提供一種比grbac.Rules
更緊湊的定義鑒權(quán)規(guī)則的方法饱狂。
3.5. gin && grbac.WithLoader
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
// 在這里實現(xiàn)你的邏輯
// ...
// 這個邏輯可能是從請求的Headers中獲取token,并且根據(jù)token從數(shù)據(jù)庫中查詢用戶的相應(yīng)角色宪彩。
return roles, err
}
type MySQLLoader struct {
session *gorm.DB
}
func NewMySQLLoader(dsn string) (*MySQLLoader, error) {
loader := &MySQLLoader{}
db, err := gorm.Open("mysql", dsn)
if err != nil {
return nil, err
}
loader.session = db
return loader, nil
}
func (loader *MySQLLoader) LoadRules() (rules grbac.Rules, err error) {
// 在這里實現(xiàn)你的邏輯
// ...
// 你可以從數(shù)據(jù)庫或文件加載授權(quán)規(guī)則
// 但是你需要以 grbac.Rules 的格式返回你的身份驗證規(guī)則
// 提示:你還可以將此函數(shù)綁定到golang結(jié)構(gòu)體
return
}
func Authentication() gin.HandlerFunc {
loader, err := NewMySQLLoader("user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
rbac, err := grbac.New(grbac.WithLoader(loader.LoadRules, time.Second * 5))
if err != nil {
panic(err)
}
return func(c *gin.Context) {
roles, err := QueryRolesByHeaders(c.Request.Header)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
state, err := rbac.IsRequestGranted(c.Request, roles)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if !state.IsGranted() {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
}
func main(){
c := gin.New()
c.Use(Authorization())
// 在這里通過c.Get休讳、c.Post等函數(shù)綁定你的API
// ...
c.Run(":8080")
}
4. 增強的通配符
Wildcard
支持的語法:
pattern:
{ term }
term:
'*' 匹配任何非路徑分隔符的字符串
'**' 匹配任何字符串,包括路徑分隔符.
'?' 匹配任何單個非路徑分隔符
'[' [ '^' ] { character-range } ']'
character class (must be non-empty)
'{' { term } [ ',' { term } ... ] '}'
c 匹配字符 c (c != '*', '?', '\\', '[')
'\\' c 匹配字符 c
character-range:
c 匹配字符 c (c != '\\', '-', ']')
'\\' c 匹配字符 c
lo '-' hi 匹配字符 c for lo <= c <= hi
5. 運行效率
? gos test -bench=.
goos: linux
goarch: amd64
pkg: github.com/storyicon/grbac/pkg/tree
BenchmarkTree_Query 2000 541397 ns/op
BenchmarkTree_Foreach_Query 2000 1360719 ns/op
PASS
ok github.com/storyicon/grbac/pkg/tree 13.182s
測試用例包含1000個隨機規(guī)則尿孔,“BenchmarkTree_Query”和“BenchmarkTree_Foreach_Query”函數(shù)分別測試四個請求:
541397/(4*1e9)=0.0001s
當(dāng)有1000條規(guī)則時俊柔,每個請求的平均驗證時間為“0.0001s”,這很快(大多數(shù)時間在通配符的匹配上)活合。