1. 基于角色的访问控制框架

Grbac是一个快速,优雅和简洁的RBAC框架。它支持增强的通配符并使用Radix树匹配HTTP请求。令人惊奇的是,您可以在任何现有的数据库和数据结构中轻松使用它。

项目地址:https://github.com/storyicon/grbac

grbac的作用是确保指定的资源只能由指定的角色访问。请注意,grbac不负责存储鉴权规则和分辨“当前请求发起者具有哪些角色”,更不负责角色的创建、分配等。这意味着您应该首先配置规则信息,并提供每个请求的发起者具有的角色。

grbac将HostPathMethod的组合视为Resource,并将Resource绑定到一组角色规则(称为Permission)。只有符合这些规则的用户才能访问相应的Resource

读取鉴权规则的组件称为Loader。grbac预置了一些Loader,你也可以通过实现func()(grbac.Rules,error)来根据你的设计来自定义Loader,并通过grbac.WithLoader加载它。

1.1. 1. 最常见的用例

下面是最常见的用例,它使用gin,并将grbac包装成了一个中间件。通过这个例子,你可以很容易地知道如何在其他http框架中使用grbac(比如echoirisace等):

  1. package main
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "github.com/storyicon/grbac"
  5. "net/http"
  6. "time"
  7. )
  8. func LoadAuthorizationRules() (rules grbac.Rules, err error) {
  9. // 在这里实现你的逻辑
  10. // ...
  11. // 你可以从数据库或文件加载授权规则
  12. // 但是你需要以 grbac.Rules 的格式返回你的身份验证规则
  13. // 提示:你还可以将此函数绑定到golang结构体
  14. return
  15. }
  16. func QueryRolesByHeaders(header http.Header) (roles []string,err error){
  17. // 在这里实现你的逻辑
  18. // ...
  19. // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。
  20. return roles, err
  21. }
  22. func Authorization() gin.HandlerFunc {
  23. // 在这里,我们通过“grbac.WithLoader”接口使用自定义Loader功能
  24. // 并指定应每分钟调用一次LoadAuthorizationRules函数以获取最新的身份验证规则。
  25. // Grbac还提供一些现成的Loader:
  26. // grbac.WithYAML
  27. // grbac.WithRules
  28. // grbac.WithJSON
  29. // ...
  30. rbac, err := grbac.New(grbac.WithLoader(LoadAuthorizationRules, time.Minute))
  31. if err != nil {
  32. panic(err)
  33. }
  34. return func(c *gin.Context) {
  35. roles, err := QueryRolesByHeaders(c.Request.Header)
  36. if err != nil {
  37. c.AbortWithError(http.StatusInternalServerError, err)
  38. return
  39. }
  40. state, _ := rbac.IsRequestGranted(c.Request, roles)
  41. if !state.IsGranted() {
  42. c.AbortWithStatus(http.StatusUnauthorized)
  43. return
  44. }
  45. }
  46. }
  47. func main(){
  48. c := gin.New()
  49. c.Use(Authorization())
  50. // 在这里通过c.Get、c.Post等函数绑定你的API
  51. // ...
  52. c.Run(":8080")
  53. }

1.2. 2. 概念

这里有一些关于grbac的概念。这很简单,你可能只需要三分钟就能理解。

1.2.1. 2.1. Rule

  1. // Rule即规则,用于定义Resource和Permission之间的关系
  2. type Rule struct {
  3. // ID决定了Rule的优先级。
  4. // ID值越大意味着Rule的优先级越高。
  5. // 当请求被多个规则同时匹配时,grbac将仅使用具有最高ID值的规则。
  6. // 如果有多个规则同时具有最大的ID,则将随机使用其中一个规则。
  7. ID int `json:"id"`
  8. *Resource
  9. *Permission
  10. }

如你所见,Rule由三部分组成:IDResourcePermission。 “ID”确定规则的优先级。 当请求同时满足多个规则时(例如在通配符中), grbac将选择具有最高ID的那个,然后使用其权限定义进行身份验证。 如果有多个规则同时具有最大的ID,则将随机使用其中一个规则(所以请避免这种情况)。

下面有一个非常简单的例子:

  1. #Rule
  2. - id: 0
  3. # Resource
  4. host: "*"
  5. path: "**"
  6. method: "*"
  7. # Permission
  8. authorized_roles:
  9. - "*"
  10. forbidden_roles: []
  11. allow_anyone: false
  12. #Rule
  13. - id: 1
  14. # Resource
  15. host: domain.com
  16. path: "/article"
  17. method: "{DELETE,POST,PUT}"
  18. # Permission
  19. authorized_roles:
  20. - editor
  21. forbidden_roles: []
  22. allow_anyone: false

在以yaml格式编写的此配置文件中,ID=0 的规则表明任何具有任何角色的人都可以访问所有资源。 但是ID=1的规则表明只有editor可以对文章进行增删改操作。 这样,除了文章的操作只能由editor访问之外,任何具有任何角色的人都可以访问所有其他资源。

1.2.2. 2.2. Resource

  1. type Resource struct {
  2. // Host 定义资源的Host,允许使用增强的通配符。
  3. Host string `json:"host"`
  4. // Path 定义资源的Path,允许使用增强的通配符。
  5. Path string `json:"path"`
  6. // Method 定义资源的Method,允许使用增强的通配符。
  7. Method string `json:"method"`
  8. }

Resource用于描述Rule适用的资源。 当执行IsRequestGranted(c.Request,roles)时,grbac首先将当前的Request与所有Rule中的Resources匹配。

Resource的每个字段都支持增强的通配符

1.2.3. 2.3. Permission

  1. // Permission用于定义权限控制信息
  2. type Permission struct {
  3. // AuthorizedRoles定义允许访问资源的角色
  4. // 支持的类型: 非空字符串,*
  5. // *: 意味着任何角色,但访问者应该至少有一个角色,
  6. // 非空字符串:指定的角色
  7. AuthorizedRoles []string `json:"authorized_roles"`
  8. // ForbiddenRoles 定义不允许访问指定资源的角色
  9. // ForbiddenRoles 优先级高于AuthorizedRoles
  10. // 支持的类型:非空字符串,*
  11. // *: 意味着任何角色,但访问者应该至少有一个角色,
  12. // 非空字符串:指定的角色
  13. //
  14. ForbiddenRoles []string `json:"forbidden_roles"`
  15. // AllowAnyone的优先级高于 ForbiddenRoles、AuthorizedRoles
  16. // 如果设置为true,任何人都可以通过验证。
  17. // 请注意,这将包括“没有角色的人”。
  18. AllowAnyone bool `json:"allow_anyone"`
  19. }

“Permission”用于定义绑定到的“Resource”的授权规则。 这是易于理解的,当请求者的角色符合“Permission”的定义时,他将被允许访问Resource,否则他将被拒绝访问。

为了加快验证的速度,Permission中的字段不支持“增强的通配符”。 在AuthorizedRolesForbiddenRoles中只允许*表示所有。

1.2.4. 2.4. Loader

Loader用于加载Rule。 grbac预置了一些加载器,你也可以通过实现func()(grbac.Rules, error) 来自定义加载器并通过 grbac.WithLoader 加载它。

method description
WithJSON(path, interval) 定期从json文件加载规则配置
WithYaml(path, interval) 定期从yaml文件加载规则配置
WithRules(Rules) grbac.Rules加载规则配置
WithAdvancedRules(loader.AdvancedRules) 以一种更紧凑的方式定义Rule,并使用loader.AdvancedRules加载
WithLoader(loader func()(Rules, error), interval) 使用自定义函数定期加载规则

interval定义了Rules的重载周期。 当interval <0时,grbac会放弃周期加载Rules配置; 当interval∈[0,1s)时,grbac会自动将interval设置为5s;

1.3. 3. 其他例子

这里有一些简单的例子,可以让你更容易理解grbac的工作原理。 虽然grbac在大多数http框架中运行良好,但很抱歉我现在只使用gin,所以如果下面的例子中有一些缺陷,请告诉我。

1.3.1. 3.1. gin && grbac.WithJSON

如果你想在JSON文件中编写配置文件,你可以通过grbac.WithJSON(filepath,interval)加载它,filepath是你的json文件路径,并且grbac将每隔interval重新加载一次文件。 。

  1. [
  2. {
  3. "id": 0,
  4. "host": "*",
  5. "path": "**",
  6. "method": "*",
  7. "authorized_roles": [
  8. "*"
  9. ],
  10. "forbidden_roles": [
  11. "black_user"
  12. ],
  13. "allow_anyone": false
  14. },
  15. {
  16. "id":1,
  17. "host": "domain.com",
  18. "path": "/article",
  19. "method": "{DELETE,POST,PUT}",
  20. "authorized_roles": ["editor"],
  21. "forbidden_roles": [],
  22. "allow_anyone": false
  23. }
  24. ]

以上是“JSON”格式的身份验证规则示例。它的结构基于grbac.Rules

  1. func QueryRolesByHeaders(header http.Header) (roles []string,err error){
  2. // 在这里实现你的逻辑
  3. // ...
  4. // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。
  5. return roles, err
  6. }
  7. func Authentication() gin.HandlerFunc {
  8. rbac, err := grbac.New(grbac.WithJSON("config.json", time.Minute * 10))
  9. if err != nil {
  10. panic(err)
  11. }
  12. return func(c *gin.Context) {
  13. roles, err := QueryRolesByHeaders(c.Request.Header)
  14. if err != nil {
  15. c.AbortWithError(http.StatusInternalServerError, err)
  16. return
  17. }
  18. state, err := rbac.IsRequestGranted(c.Request, roles)
  19. if err != nil {
  20. c.AbortWithStatus(http.StatusInternalServerError)
  21. return
  22. }
  23. if !state.IsGranted() {
  24. c.AbortWithStatus(http.StatusUnauthorized)
  25. return
  26. }
  27. }
  28. }
  29. func main(){
  30. c := gin.New()
  31. c.Use(Authentication())
  32. // 在这里通过c.Get、c.Post等函数绑定你的API
  33. // ...
  34. c.Run(":8080")
  35. }

1.3.2. 3.2. echo && grbac.WithYaml

如果你想在YAML文件中编写配置文件,你可以通过grbac.WithYAML(file,interval)加载它,file是你的yaml文件路径,并且grbac将每隔一个interval重新加载一次文件。

  1. #Rule
  2. - id: 0
  3. # Resource
  4. host: "*"
  5. path: "**"
  6. method: "*"
  7. # Permission
  8. authorized_roles:
  9. - "*"
  10. forbidden_roles: []
  11. allow_anyone: false
  12. #Rule
  13. - id: 1
  14. # Resource
  15. host: domain.com
  16. path: "/article"
  17. method: "{DELETE,POST,PUT}"
  18. # Permission
  19. authorized_roles:
  20. - editor
  21. forbidden_roles: []
  22. allow_anyone: false

以上是“YAML”格式的认证规则的示例。它的结构基于grbac.Rules

  1. func QueryRolesByHeaders(header http.Header) (roles []string,err error){
  2. // 在这里实现你的逻辑
  3. // ...
  4. // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。
  5. return roles, err
  6. }
  7. func Authentication() echo.MiddlewareFunc {
  8. rbac, err := grbac.New(grbac.WithYAML("config.yaml", time.Minute * 10))
  9. if err != nil {
  10. panic(err)
  11. }
  12. return func(echo.HandlerFunc) echo.HandlerFunc {
  13. return func(c echo.Context) error {
  14. roles, err := QueryRolesByHeaders(c.Request().Header)
  15. if err != nil {
  16. c.NoContent(http.StatusInternalServerError)
  17. return nil
  18. }
  19. state, err := rbac.IsRequestGranted(c.Request(), roles)
  20. if err != nil {
  21. c.NoContent(http.StatusInternalServerError)
  22. return nil
  23. }
  24. if state.IsGranted() {
  25. return nil
  26. }
  27. c.NoContent(http.StatusUnauthorized)
  28. return nil
  29. }
  30. }
  31. }
  32. func main(){
  33. c := echo.New()
  34. c.Use(Authentication())
  35. // 在这里通过c.Get、c.Post等函数绑定你的API
  36. // ...
  37. }

1.3.3. 3.3. iris && grbac.WithRules

如果你想直接在代码中编写认证规则,grbac.WithRules(rules)提供了这种方式,你可以像这样使用它:

  1. func QueryRolesByHeaders(header http.Header) (roles []string,err error){
  2. // 在这里实现你的逻辑
  3. // ...
  4. // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。
  5. return roles, err
  6. }
  7. func Authentication() iris.Handler {
  8. var rules = grbac.Rules{
  9. {
  10. ID: 0,
  11. Resource: &grbac.Resource{
  12. Host: "*",
  13. Path: "**",
  14. Method: "*",
  15. },
  16. Permission: &grbac.Permission{
  17. AuthorizedRoles: []string{"*"},
  18. ForbiddenRoles: []string{"black_user"},
  19. AllowAnyone: false,
  20. },
  21. },
  22. {
  23. ID: 1,
  24. Resource: &grbac.Resource{
  25. Host: "domain.com",
  26. Path: "/article",
  27. Method: "{DELETE,POST,PUT}",
  28. },
  29. Permission: &grbac.Permission{
  30. AuthorizedRoles: []string{"editor"},
  31. ForbiddenRoles: []string{},
  32. AllowAnyone: false,
  33. },
  34. },
  35. }
  36. rbac, err := grbac.New(grbac.WithRules(rules))
  37. if err != nil {
  38. panic(err)
  39. }
  40. return func(c context.Context) {
  41. roles, err := QueryRolesByHeaders(c.Request().Header)
  42. if err != nil {
  43. c.StatusCode(http.StatusInternalServerError)
  44. c.StopExecution()
  45. return
  46. }
  47. state, err := rbac.IsRequestGranted(c.Request(), roles)
  48. if err != nil {
  49. c.StatusCode(http.StatusInternalServerError)
  50. c.StopExecution()
  51. return
  52. }
  53. if !state.IsGranted() {
  54. c.StatusCode(http.StatusUnauthorized)
  55. c.StopExecution()
  56. return
  57. }
  58. }
  59. }
  60. func main(){
  61. c := iris.New()
  62. c.Use(Authentication())
  63. // 在这里通过c.Get、c.Post等函数绑定你的API
  64. // ...
  65. }

1.3.4. 3.4. ace && grbac.WithAdvancedRules

如果你想直接在代码中编写认证规则,grbac.WithAdvancedRules(rules)提供了这种方式,你可以像这样使用它:

  1. func QueryRolesByHeaders(header http.Header) (roles []string,err error){
  2. // 在这里实现你的逻辑
  3. // ...
  4. // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。
  5. return roles, err
  6. }
  7. func Authentication() ace.HandlerFunc {
  8. var advancedRules = loader.AdvancedRules{
  9. {
  10. Host: []string{"*"},
  11. Path: []string{"**"},
  12. Method: []string{"*"},
  13. Permission: &grbac.Permission{
  14. AuthorizedRoles: []string{},
  15. ForbiddenRoles: []string{"black_user"},
  16. AllowAnyone: false,
  17. },
  18. },
  19. {
  20. Host: []string{"domain.com"},
  21. Path: []string{"/article"},
  22. Method: []string{"PUT","DELETE","POST"},
  23. Permission: &grbac.Permission{
  24. AuthorizedRoles: []string{"editor"},
  25. ForbiddenRoles: []string{},
  26. AllowAnyone: false,
  27. },
  28. },
  29. }
  30. auth, err := grbac.New(grbac.WithAdvancedRules(advancedRules))
  31. if err != nil {
  32. panic(err)
  33. }
  34. return func(c *ace.C) {
  35. roles, err := QueryRolesByHeaders(c.Request.Header)
  36. if err != nil {
  37. c.AbortWithStatus(http.StatusInternalServerError)
  38. return
  39. }
  40. state, err := auth.IsRequestGranted(c.Request, roles)
  41. if err != nil {
  42. c.AbortWithStatus(http.StatusInternalServerError)
  43. return
  44. }
  45. if !state.IsGranted() {
  46. c.AbortWithStatus(http.StatusUnauthorized)
  47. return
  48. }
  49. }
  50. }
  51. func main(){
  52. c := ace.New()
  53. c.Use(Authentication())
  54. // 在这里通过c.Get、c.Post等函数绑定你的API
  55. // ...
  56. }

loader.AdvancedRules试图提供一种比grbac.Rules更紧凑的定义鉴权规则的方法。

1.3.5. 3.5. gin && grbac.WithLoader

  1. func QueryRolesByHeaders(header http.Header) (roles []string,err error){
  2. // 在这里实现你的逻辑
  3. // ...
  4. // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。
  5. return roles, err
  6. }
  7. type MySQLLoader struct {
  8. session *gorm.DB
  9. }
  10. func NewMySQLLoader(dsn string) (*MySQLLoader, error) {
  11. loader := &MySQLLoader{}
  12. db, err := gorm.Open("mysql", dsn)
  13. if err != nil {
  14. return nil, err
  15. }
  16. loader.session = db
  17. return loader, nil
  18. }
  19. func (loader *MySQLLoader) LoadRules() (rules grbac.Rules, err error) {
  20. // 在这里实现你的逻辑
  21. // ...
  22. // 你可以从数据库或文件加载授权规则
  23. // 但是你需要以 grbac.Rules 的格式返回你的身份验证规则
  24. // 提示:你还可以将此函数绑定到golang结构体
  25. return
  26. }
  27. func Authentication() gin.HandlerFunc {
  28. loader, err := NewMySQLLoader("user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
  29. if err != nil {
  30. panic(err)
  31. }
  32. rbac, err := grbac.New(grbac.WithLoader(loader.LoadRules, time.Second * 5))
  33. if err != nil {
  34. panic(err)
  35. }
  36. return func(c *gin.Context) {
  37. roles, err := QueryRolesByHeaders(c.Request.Header)
  38. if err != nil {
  39. c.AbortWithStatus(http.StatusInternalServerError)
  40. return
  41. }
  42. state, err := rbac.IsRequestGranted(c.Request, roles)
  43. if err != nil {
  44. c.AbortWithStatus(http.StatusInternalServerError)
  45. return
  46. }
  47. if !state.IsGranted() {
  48. c.AbortWithStatus(http.StatusUnauthorized)
  49. return
  50. }
  51. }
  52. }
  53. func main(){
  54. c := gin.New()
  55. c.Use(Authorization())
  56. // 在这里通过c.Get、c.Post等函数绑定你的API
  57. // ...
  58. c.Run(":8080")
  59. }

1.4. 4. 增强的通配符

Wildcard支持的语法:

  1. pattern:
  2. { term }
  3. term:
  4. '*' 匹配任何非路径分隔符的字符串
  5. '**' 匹配任何字符串,包括路径分隔符.
  6. '?' 匹配任何单个非路径分隔符
  7. '[' [ '^' ] { character-range } ']'
  8. character class (must be non-empty)
  9. '{' { term } [ ',' { term } ... ] '}'
  10. c 匹配字符 c (c != '*', '?', '\\', '[')
  11. '\\' c 匹配字符 c
  12. character-range:
  13. c 匹配字符 c (c != '\\', '-', ']')
  14. '\\' c 匹配字符 c
  15. lo '-' hi 匹配字符 c for lo <= c <= hi

1.5. 5. 运行效率

  1. gos test -bench=.
  2. goos: linux
  3. goarch: amd64
  4. pkg: github.com/storyicon/grbac/pkg/tree
  5. BenchmarkTree_Query 2000 541397 ns/op
  6. BenchmarkTree_Foreach_Query 2000 1360719 ns/op
  7. PASS
  8. ok github.com/storyicon/grbac/pkg/tree 13.182s

测试用例包含1000个随机规则,“BenchmarkTree_Query”和“BenchmarkTree_Foreach_Query”函数分别测试四个请求:

  1. 541397/(4*1e9)=0.0001s

当有1000条规则时,每个请求的平均验证时间为“0.0001s”,这很快(大多数时间在通配符的匹配上)。

1.6. 6. 生产环境

grbac 已经被以下企业用于生产环境:

wallstreetcn