天天看點

使用casbin完成驗證授權.md

上一篇講了搭建一個身份認證系統,可以看到借助dex搭建一個安全可靠的身份認證系統并不是太難。本篇再講一下用

casbin

完成驗證授權。

什麼是驗證授權

授權(英語:Authorization)一般是指對資訊安全或計算機安全相關的資源定義與授予通路權限,尤指通路控制。動詞“授權”可指定義通路政策與接受通路。

授權

作為名詞,其代表的是在計算機系統中定義的資源通路權限。而

驗證授權

就是驗證計算機帳戶是否有資源的通路權限。

舉個栗子,假設現在有一本書

book1

,其擁有

read

,

write

的操作,那麼我們可以先定義以下

授權

  1. alice

    可以

    read

    書籍

    book1

  2. bob

    可以

    write

    書籍

    book1

  3. bob

    可以

    read

    書籍

    book1

現在來了一個使用者

alice

她想

write

書籍

book1

,這時調用驗證授權功能子產品的接口,驗證授權功能子產品根據上述

授權

規則可以快速判斷

alice

不可以

write

書籍

book1

;過一會兒又來了一個使用者

bob

他想

write

書籍

book1

,這時調用驗證授權系統的接口,驗證授權系統根據上述

授權

規則可以快速判斷

bob

可以

write

書籍

book1

可以看到

身份認證系統

強調地是安全可靠地得到計算機使用者的身份資訊,而

驗證授權

強調地是根據計算機的身份資訊、通路的資源、對資源的操作等給出一個Yes/No的答複。

常用授權模型

ACL

ACL

Access Control List

的縮寫,稱為通路控制清單. 定義了誰可以對某個資料進行何種操作. 關鍵資料模型有: 使用者, 權限.

ACL規則簡單, 也帶來一些問題: 資源的權限需要在使用者間切換的成本極大; 使用者數或資源的數量增長, 都會加劇規則維護成本;

典型應用

  1. 檔案系統

檔案系統的檔案或檔案夾定義某個賬号(user)或某個群組(group)對檔案(夾)的讀(read)/寫(write)/執行(execute)權限.

  1. 網絡通路

防火牆: 伺服器限制不允許指定機器通路其指定端口, 或允許特定指定伺服器通路其指定幾個端口.

RBAC

RBAC

Role-based access control

的縮寫, 稱為 基于角色的通路控制. 核心資料模型有: 使用者, 角色, 權限.

使用者具有角色, 而角色具有權限, 進而表達使用者具有權限.

由于有角色作為中間紐帶, 當新增使用者時, 隻需要為使用者賦予角色, 使用者即獲得角色所包含的所有權限.

RBAC

存在多個擴充版本,

RBAC0

RBAC1

RBAC2

RBAC3

。這些版本的詳細說明可以參數這裡。我們在實際項目中經常使用的是

RBAC1

,即帶有角色繼承概念的RBAC模型。

ABAC

ABAC

Attribute-based access control

的縮寫, 稱為基于屬性的通路控制.

權限和資源當時的狀态(屬性)有關, 屬性的值可以用于正向判斷(符合某種條件則通過), 也可以用于反向判斷(符合某種條件則拒絕):

典型應用

  1. 論壇的評論權限, 當文章是鎖定狀态時, 則不再允許繼續評論;
  2. Github 私有倉庫不允許其他人通路;
  3. 發帖者可以編輯/删除評論(如果是RBAC, 會為發帖者定義一個角色, 但是每個文章都要新增一條使用者/發帖角色的記錄);
  4. 微信聊天消息超過2分鐘則不再允許撤回;
  5. 12306 隻有實名認證後的賬号才能購票;
  6. 已過期的付費賬号将不再允許使用付費功能;

實作權限驗證

前面提到了多種不同的權限模型,要完全自研實作不同的權限模型還是挺麻煩的。幸好

casbin

出現,它将上述不同的模型抽象為自己的

PERM metamodel

,這個

PERM metamodel

隻包括

Policy

,

Effect

,

Request

,

Matchers

,隻通過這幾個模型對象的組合即可實作上述提到的多種權限模型,如果業務上需要切換權限模型,也隻需要配置一下

PERM metamodel

,并不需要大改權限模型相關的代碼,這一點真的很贊!!!

一個最簡單的

ACL

權限模型即可像下面這樣定義:

acl_simple_model.conf

# Request definition
[request_definition]
r = sub, obj, act

# Policy definition
[policy_definition]
p = sub, obj, act

# Policy effect
[policy_effect]
e = some(where (p.eft == allow))

# Matchers
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act           

複制

相應的授權規則可以像下面這樣定義:

acl_simple_policy.csv

p, alice, data1, read
p, bob, data2, write           

複制

這意味着

alice

可以

read

資源

data1

bob

可以

write

資源

data2

寫一個簡單的程式就可以完成該權限驗證:

package main

import (
	"fmt"
	"github.com/casbin/casbin/v2"
)

func main() {
	e, _ := casbin.NewSyncedEnforcer("acl_simple_model.conf", "acl_simple_policy.csv")
	sub := "alice"   // the user that wants to access a resource.
	obj := "data1"   // the resource that is going to be accessed.
	act := "read"    // the operation that the user performs on the resource.

	if passed, _ := e.Enforce(sub, obj, act); passed {
		// permit alice to read data1
		fmt.Println("Enforce policy passed.")
	} else {
		// deny the request, show an error
		fmt.Println("Enforce policy denied.")
	}
}           

複制

casbin模型詳解

casbin

官方其實已經提供了多種模型的定義及示例policy定義,見這裡。而且為了便于使用者了解診斷模型及policy,還給了個線上的editor,修改模型或policy時可以利用此工具。

從上面的示例可以看出基于

casbin

實作權限驗證,代碼很簡單,但

casbin

的模型定義及policy定義初看還是挺複雜的,這樣詳細了解一下。

casbin

的模型定義裡會出現4個部分:

[request_definition]

,

[policy_definition]

,

[policy_effect]

,

[matchers]

其中

[request_definition]

描述的是通路請求的定義,如下面的定義将通路請求的三個參數分别映射為

r.sub

r.obj

r.act

(注意并不是所有的通路請求一定是3個參數):

[request_definition]
r = sub, obj, act           

複制

同理

[policy_definition]

描述的是授權policy的定義,如下面的定義将每條授權policy分别映射為

p.sub

p.obj

p.act

(注意并不是所有的授權policy一定是3個參數,也不是必須隻有一條授權policy定義):

[policy_definition]
p = sub, obj, act           

複制

[matchers]

描述的是根據通路請求如何找到比對的授權policy,如下面的定義将根據

r.sub

r.obj

r.act

p.sub

p.obj

p.act

找到完全比對的授權policy:

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act           

複制

在寫

[matchers]

規則是還可以使用一些内置或自定義函數,參考這裡的文檔。

最後

[policy_effect]

描述如果找到比對的多條的授權policy,最終給出的驗證授權結果,如下面的定義說明隻要有一條比對的授權政策其

eft

allow

,則最終給出的驗證授權結果就是

允許

(注意每條授權policy預設的eft就是allow)。

[policy_effect]
e = some(where (p.eft == allow))           

複制

如果使用

RBAC

權限模型,可能還會使用

[role_definition]

,這個

[role_definition]

算是最複雜的了,其可以描述user-role之間的映射關系或resource-role之間的映射關系。這麼說比較抽象,還是舉例說明:

假設模型定義如下:

[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檔案如下:

p, data2_admin, data2, read
p, data2_admin, data2, write

g, alice, data2_admin           

複制

現在收到了授權請求

alice, data2, read

,這時

r.sub

alice

,根據

g = _, _

g(r.sub, p.sub)

,我們可以得出對應的

p.sub

可以為

data2_admin

,接下來再根據

r.obj == p.obj && r.act == p.act

,最終找到比對的授權policy規則為

p, data2_admin, data2, read

,最後根據

some(where (p.eft == allow))

規則,此時驗證授權的結果就應該是

allow

這裡

casbin

根據

r.sub

查找對應

p.sub

的過程還會考慮角色繼承。考慮以下授權policy檔案:

p, reader, data2, read
p, writer, data2, write

g, admin, reader
g, admin, writer
g, alice, admin           

複制

現在收到了授權請求

alice, data2, read

,這時

r.sub

alice

,根據

g = _, _

g(r.sub, p.sub)

,我們可以得出對應的

p.sub

可以為

admin

reader

writer

,接下來再根據

r.obj == p.obj && r.act == p.act

,最終找到比對的授權policy規則為

p, reader, data2, read

,最後根據

some(where (p.eft == allow))

規則,此時驗證授權的結果就應該是

allow

通過

[role_definition]

還可以定義resource-role之間的映射關系,見示例。

casbin

的模型大概就是上面描述的了,很多概念了解起來可能比較費勁,結合示例及在editor上做些實驗應該了解得更快一些。

casbin相關事項

  1. casbin

    的模型定義及授權policy定義還可以選擇儲存在其它存儲中,見Model Storage、Policy Storage、Adapters。
  2. casbin

    Enforcer

    對象還提供了一系列API接口管理授權policy規則,見Management API、RBAC API。
  3. 可以修改授權policy規則,那麼當多個驗證授權服務分布式部署時,必然要考慮某個服務修改了授權規則後,其它服務示例必須進行規則的同步。

    casbin

    也考慮到了這個需求,提供了Watchers機制,用于在觀察到授權規則發生變更時進行必要的回調,見Watchers。
  4. 在多線程環境下使用

    Enforcer

    對象的接口,必須使用

    casbin.NewSyncedEnforcer

    建立

    Enforcer

    ,另外還支援授權policy

    AutoLoad

    特性,見這裡。
  5. casbin

    預設是從授權policy檔案中加載角色及角色的繼承資訊的,也可以從其它外部資料源擷取這些資訊,見這裡。

casbin代碼分析

casbin

整體代碼很簡單,很多代碼都是模型定義及授權policy定義加載的邏輯,關鍵代碼隻有一個方法Enforce,見下面:

if !e.enabled {
		return true, nil
	}

	functions := make(map[string]govaluate.ExpressionFunction)
	for key, function := range e.fm {
		functions[key] = function
	}
	if _, ok := e.model["g"]; ok {
		for key, ast := range e.model["g"] {
			rm := ast.RM
			functions[key] = util.GenerateGFunction(rm)
		}
	}

	expString := e.model["m"]["m"].Value
	expression, err := govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
	if err != nil {
		return false, err
	}

	rTokens := make(map[string]int, len(e.model["r"]["r"].Tokens))
	for i, token := range e.model["r"]["r"].Tokens {
		rTokens[token] = i
	}
	pTokens := make(map[string]int, len(e.model["p"]["p"].Tokens))
	for i, token := range e.model["p"]["p"].Tokens {
		pTokens[token] = i
	}

	parameters := enforceParameters{
		rTokens: rTokens,
		rVals:   rvals,

		pTokens: pTokens,
	}
  if policyLen := len(e.model["p"]["p"].Policy); policyLen != 0 {
		policyEffects = make([]effect.Effect, policyLen)
		matcherResults = make([]float64, policyLen)
		if len(e.model["r"]["r"].Tokens) != len(rvals) {
			return false, errors.New(
				fmt.Sprintf(
					"invalid request size: expected %d, got %d, rvals: %v",
					len(e.model["r"]["r"].Tokens),
					len(rvals),
					rvals))
		}
		for i, pvals := range e.model["p"]["p"].Policy {
			// log.LogPrint("Policy Rule: ", pvals)
			if len(e.model["p"]["p"].Tokens) != len(pvals) {
				return false, errors.New(
					fmt.Sprintf(
						"invalid policy size: expected %d, got %d, pvals: %v",
						len(e.model["p"]["p"].Tokens),
						len(pvals),
						pvals))
			}

			parameters.pVals = pvals

			result, err := expression.Eval(parameters)
			// log.LogPrint("Result: ", result)

			if err != nil {
				return false, err
			}

			switch result := result.(type) {
			case bool:
				if !result {
					policyEffects[i] = effect.Indeterminate
					continue
				}
			case float64:
				if result == 0 {
					policyEffects[i] = effect.Indeterminate
					continue
				} else {
					matcherResults[i] = result
				}
			default:
				return false, errors.New("matcher result should be bool, int or float")
			}

			if j, ok := parameters.pTokens["p_eft"]; ok {
				eft := parameters.pVals[j]
				if eft == "allow" {
					policyEffects[i] = effect.Allow
				} else if eft == "deny" {
					policyEffects[i] = effect.Deny
				} else {
					policyEffects[i] = effect.Indeterminate
				}
			} else {
				policyEffects[i] = effect.Allow
			}

			if e.model["e"]["e"].Value == "priority(p_eft) || deny" {
				break
			}

		}
	}           

複制

這個代碼邏輯很清楚了,就是根據

[matchers]

[request_definition]

[policy_definition]

找到比對的

[policy_definition]

,再根據

[policy_effect]

最後得出最終的驗證授權結果。可以看到該處理邏輯裡大量地周遊了

e.model["r"]["r"].Tokens

e.model["p"]["p"].Tokens

e.model["p"]["p"].Policy

,當授權policy規則條數較多時,估計性能不會太好。但官方給了個性能測試報告,據說性能還可以,這個後面還須再驗證下。

為了優化性能,其實是可以将驗證授權操作的結果進行緩存,官方也提供了CachedEnforcer,目測邏輯沒問題,如果确實遇到性能瓶頸,可以考慮采用。

其它外部支援

一些開源愛好者為

casbin

貢獻了很多中間件元件,便于在多個程式設計語言中內建

casbin

進行權限驗證。

還有一些開源愛好者為

casbin

貢獻了模型管理及授權政策管理的web前端,如果覺得手工修改授權政策檔案不直覺的話,可以考慮采用。

還可以看到目前很多開源項目的權限驗證部分都是采用了

casbin

來實作的,例如harbor裡的rbac權限驗證。

還發現一個基于

casbin

實作的身份認證及驗證授權服務,這個例子以後可以好好參考一下。

自己研究

casbin

的示例項目。

參考

  1. https://github.com/isayme/blog/issues/34
  2. https://www.jianshu.com/p/b078abe9534f
  3. https://casbin.org/docs/en/overview
  4. https://casbin.org/docs/en/supported-models
  5. https://casbin.org/docs/en/syntax-for-models
  6. https://casbin.org/docs/en/rbac
  7. https://casbin.org/docs/en/model-storage
  8. https://casbin.org/docs/en/policy-storage
  9. https://casbin.org/docs/en/adapters
  10. https://casbin.org/docs/en/management-api
  11. https://casbin.org/docs/en/rbac-api
  12. https://casbin.org/docs/en/watchers
  13. https://casbin.org/docs/en/role-managers
  14. https://github.com/casbin/casbin
  15. https://casbin.org/docs/en/benchmark
  16. https://casbin.org/docs/en/middlewares
  17. https://casbin.org/docs/en/admin-portal
  18. https://casbin.org/docs/en/adopters
  19. https://github.com/Soontao/go-simple-api-gateway