zoukankan      html  css  js  c++  java
  • Graphql请求的RBAC权限控制

    概要

    Graphql 请求灵活性远非 RestFul 的请求能比, 但是 Graphql 的 Endpoint 一般都是统一的一个, 根据 body 中的请求内容和参数来决定返回何种数据.

    因此, 对于 Graphql 请求的权限控制不能像 RestFul 请求那样根据请求的 URL 和 method 来判断是否有权限. 关于 Graphql 的权限判断, 曾经困扰了好长一段时间, 一直没有找到合适的方式来判断.

    直至最近一段时间, 在 github.com/graphql-go 这个库中找到能够解析 graphql 请求的方法, 然后结合 casbin 的 rbac 模型, 才算是将 Graphql 权限问题的解决推进了一大步.

    实现方式

    实现 Graphql 的权限认证, 主要包含以下 3 个部分:

    1. adapter: 用来连接 graphql-engine 的 casbin adapter, 也就是可以将权限策略持久存储到数据库
    2. middleware: 基于 golang gin 框架的中间件, 拦截请求并判断其是否符合权限要求
    3. 权限 api: 提供操作权限的 API, 基于 RBAC 的, 所以只提供的基础的几个接口

    adapter

    casbin 默认是将权限策略存储在 csv 文件中的, 这只能在 demo 中用用, 实际系统用明显不合适. 因此, 我们需要写个 adapter, 将权限策略写入数据库.

    adapter 很简单, 只要仿照 casbin 已有的那些 adapter 实现相应的接口即可:

      1  package auth
      2  
      3  import (
      4    "runtime"
      5  
      6    imodel "illuminant/model"
      7  
      8    "github.com/casbin/casbin/v2/model"
      9    "github.com/casbin/casbin/v2/persist"
     10  )
     11  
     12  // Adapter represents the hasura graphql
     13  type Adapter struct{}
     14  
     15  // finalizer is the destructor for Adapter.
     16  func finalizer(a *Adapter) {}
     17  
     18  // NewAdapter is the constructor for Adapter.
     19  func NewAdapter() (*Adapter, error) {
     20    a := &Adapter{}
     21  
     22    // Call the destructor when the object is released.
     23    runtime.SetFinalizer(a, finalizer)
     24  
     25    return a, nil
     26  }
     27  
     28  func loadPolicyLine(line *imodel.CasbinRule, model model.Model) {
     29    lineText := line.PType
     30    if line.V0 != "" {
     31      lineText += ", " + line.V0
     32    }
     33    if line.V1 != "" {
     34      lineText += ", " + line.V1
     35    }
     36    if line.V2 != "" {
     37      lineText += ", " + line.V2
     38    }
     39    if line.V3 != "" {
     40      lineText += ", " + line.V3
     41    }
     42    if line.V4 != "" {
     43      lineText += ", " + line.V4
     44    }
     45    if line.V5 != "" {
     46      lineText += ", " + line.V5
     47    }
     48  
     49    persist.LoadPolicyLine(lineText, model)
     50  }
     51  
     52  // LoadPolicy loads policy from database.
     53  func (a *Adapter) LoadPolicy(model model.Model) error {
     54    lines, err := GetRules()
     55    if err != nil {
     56      return err
     57    }
     58  
     59    for _, line := range lines {
     60      loadPolicyLine(line, model)
     61    }
     62  
     63    return nil
     64  }
     65  
     66  func savePolicyLine(ptype string, rule []string) imodel.CasbinRule {
     67    line := imodel.CasbinRule{}
     68  
     69    line.PType = ptype
     70    if len(rule) > 0 {
     71      line.V0 = rule[0]
     72    }
     73    if len(rule) > 1 {
     74      line.V1 = rule[1]
     75    }
     76    if len(rule) > 2 {
     77      line.V2 = rule[2]
     78    }
     79    if len(rule) > 3 {
     80      line.V3 = rule[3]
     81    }
     82    if len(rule) > 4 {
     83      line.V4 = rule[4]
     84    }
     85    if len(rule) > 5 {
     86      line.V5 = rule[5]
     87    }
     88  
     89    return line
     90  }
     91  
     92  // SavePolicy saves policy to database.
     93  func (a *Adapter) SavePolicy(model model.Model) error {
     94    err := DeleteRules(imodel.CasbinRule{})
     95    if err != nil {
     96      return err
     97    }
     98  
     99    var lines = make([]imodel.CasbinRule, 0)
    100  
    101    for ptype, ast := range model["p"] {
    102      for _, rule := range ast.Policy {
    103        line := savePolicyLine(ptype, rule)
    104        lines = append(lines, line)
    105      }
    106    }
    107  
    108    for ptype, ast := range model["g"] {
    109      for _, rule := range ast.Policy {
    110        line := savePolicyLine(ptype, rule)
    111        lines = append(lines, line)
    112      }
    113    }
    114  
    115    return AddRules(lines)
    116  }
    117  
    118  // AddPolicy adds a policy rule to the storage.
    119  func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
    120    line := savePolicyLine(ptype, rule)
    121    return AddRule(line)
    122  }
    123  
    124  // RemovePolicy removes a policy rule from the storage.
    125  func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error {
    126    line := savePolicyLine(ptype, rule)
    127    return DeleteRules(line)
    128  }
    129  
    130  // RemoveFilteredPolicy removes policy rules that match the filter from the storage.
    131  func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
    132    line := imodel.CasbinRule{}
    133  
    134    line.PType = ptype
    135    filter := []string{}
    136    filter = append(filter, "p_type")
    137    if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) {
    138      line.V0 = fieldValues[0-fieldIndex]
    139      filter = append(filter, "v0")
    140    }
    141    if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) {
    142      line.V1 = fieldValues[1-fieldIndex]
    143      filter = append(filter, "v1")
    144    }
    145    if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) {
    146      line.V2 = fieldValues[2-fieldIndex]
    147      filter = append(filter, "v2")
    148    }
    149    if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) {
    150      line.V3 = fieldValues[3-fieldIndex]
    151      filter = append(filter, "v3")
    152    }
    153    if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) {
    154      line.V4 = fieldValues[4-fieldIndex]
    155      filter = append(filter, "v4")
    156    }
    157    if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) {
    158      line.V5 = fieldValues[5-fieldIndex]
    159      filter = append(filter, "v5")
    160    }
    161  
    162    return DeleteRules(line)
    163  }
    

    其中, GetRules, DeleteRules, AddRule, AddRules 是实际和 graphql-engin 交互的函数.

    middleware

    基于 golang gin 的中间件, 目的是将权限检查和业务 API 的职责分开, 便于开发和维护.

      1  package middleware
      2  
      3  import (
      4    "bytes"
      5    "illuminant/config"
      6    "illuminant/logger"
      7    "illuminant/middleware/auth"
      8    "illuminant/util"
      9    "io/ioutil"
     10    "strings"
     11  
     12    jwt "github.com/appleboy/gin-jwt/v2"
     13    "github.com/casbin/casbin/v2"
     14    "github.com/casbin/casbin/v2/model"
     15    "github.com/gin-gonic/gin"
     16  )
     17  
     18  // NewAuthorizer returns the authorizer, uses a Casbin enforcer as input
     19  func NewAuthorizer() gin.HandlerFunc {
     20    cnf := config.GetConfig()
     21    lg := logger.GetLogger()
     22    adp, err := auth.NewAdapter()
     23    if err != nil {
     24      lg.Err(err).Msg("casbin adapter error")
     25      panic(err)
     26    }
     27  
     28    m, err := model.NewModelFromString(cnf.Auth.RBACModel)
     29    if err != nil {
     30      lg.Err(err).Msg("casbin model from string error")
     31      panic(err)
     32    }
     33  
     34    e, err := casbin.NewEnforcer(m, adp)
     35    if err != nil {
     36      lg.Err(err).Msg("casbin enforcer error")
     37      panic(err)
     38    }
     39  
     40    a := &RBACAuthorizer{enforcer: e}
     41    return func(c *gin.Context) {
     42      if !a.CheckPermission(c) {
     43        a.RequirePermission(c)
     44        c.Abort()
     45      }
     46    }
     47  }
     48  
     49  // RBACAuthorizer stores the casbin handler
     50  type RBACAuthorizer struct {
     51    enforcer *casbin.Enforcer
     52  }
     53  
     54  // CheckPermission checks the user/method/path combination from the request.
     55  // Returns true (permission granted) or false (permission forbidden)
     56  func (a *RBACAuthorizer) CheckPermission(c *gin.Context) bool {
     57    lg := logger.GetLogger()
     58    claims := jwt.ExtractClaims(c)
     59  
     60    method := c.Request.Method
     61    path := c.Request.URL.Path
     62  
     63    // a.ReloadPermissions()
     64    if strings.Index(path, "/api/v1/graphql") < 0 {
     65      return a.RestFullPermission(claims["id"].(string), path, method)
     66    }
     67  
     68    // graphql api
     69    body, err := c.GetRawData()
     70    if err != nil {
     71      lg.Err(err).Msg("get body raw data")
     72      return false
     73    }
     74    c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
     75  
     76    return a.GraphqlPermission(claims["id"].(string), body)
     77  }
     78  
     79  // RequirePermission returns the 403 Forbidden to the client
     80  func (a *RBACAuthorizer) RequirePermission(c *gin.Context) {
     81    util.Fail(c, util.AUTH_PERMISSION_DENIED, "权限不足", nil)
     82  }
     83  
     84  func (a *RBACAuthorizer) ReloadPermissions() {
     85    a.enforcer.LoadPolicy()
     86  }
     87  
     88  func (a *RBACAuthorizer) RestFullPermission(userId, path, method string) bool {
     89    lg := logger.GetLogger()
     90  
     91    allowed, err := a.enforcer.Enforce(userId, path, method)
     92    if err != nil {
     93      lg.Err(err).Msg("RestFullPermission check error")
     94      return false
     95    }
     96  
     97    return allowed
     98  }
     99  
    100  func (a *RBACAuthorizer) GraphqlPermission(userId string, body []byte) bool {
    101    lg := logger.GetLogger()
    102  
    103    funcs, err := util.GetGraphqlFunc(body)
    104    if err != nil {
    105      lg.Err(err).Msg("GetGraphqlFunc error")
    106      return false
    107    }
    108  
    109    for _, f := range funcs {
    110      allowed, err := a.enforcer.Enforce(userId, f, "*")
    111      if err != nil {
    112        lg.Err(err).Msg("GraphqlPermission check error")
    113        return false
    114      }
    115  
    116      if !allowed {
    117        return allowed
    118      }
    119    }
    120  
    121    return true
    122  }
    

    这个中间件同时支持 RestFul 和 Graphql 的接口, 只要将相应的策略存入数据库即可.

    基础权限 API

    基础的 API 主要有 5 个:

    1. 获取某个角色的所有权限
    2. 给角色增加权限
    3. 给角色删除权限
    4. 给用户添加角色
    5. 给用户删除角色

    有这 5 个基础 API 之后, 基本就满足了管理 casbin 中 RBAC 策略的需求了.

    总结

    上面的功能是在我自己一个正在逐步完善的一个后端快速开发平台上是实现的, 所有代码都在那个平台上.
    即: illuminant的代码 以及 illuminant 的文档

    上面的代码位于: middleware 模块, middleware 模块下的 auth 模块 以及 controller 模块下的 auth_controller.go

  • 相关阅读:
    mysql 数据库信息常用命令
    Linux 目录详细介绍
    RPC调用和HTTP调用的区别
    maven 常见命令
    SpringCloud微服务常见组件理解
    Java实现CORS跨域请求
    Java 静态代理和动态代理例子
    Mabatis中#{}和${}的区别
    针对海量数据和高并发的主要解决方案
    SOAP webserivce 和 RESTful webservice 对比及区别
  • 原文地址:https://www.cnblogs.com/wang_yb/p/13886551.html
Copyright © 2011-2022 走看看