在前面5篇博客中介紹了OAuth2和OIDC(OpenId Connect),其作用是授權和認證。那么當我們得到OAuth2的Access Token或者OIDC的Id Token之后,我們的資源服務如何來驗證這些token是否有權限來執行對資源的某一項操作呢?比如我有一個API,/books,它具有如下5個操作:
POST /books | 添加一本書 |
GET /books/{id} | 獲取一本書 |
PUT /books/{id} | 更新一本書 |
DELETE /books/{id} | 刪除一本書 |
GET /books | 獲取書的列表 |
其偽代碼如下:
[Route("books")] public class BooksController : Controller { [HttpGet("")] public Book[] Get() { return null; } [HttpGet("{bookId}")] public Book Get(int bookId) { return null; } [HttpPost("")] public Book Post(Book book) { return null; } [HttpPut("{bookId}")] public Book Put(int bookId, Book book) { return null; } [HttpDelete("{bookId}")] public Book Delete(int bookId) { return null; } }
那么我們先看看基於OAuth2的Access Token,OIDC的Id Token和傳統的基於角色的權限控制是如何處理控制這些資源的操作。
1 OAuth2的Access Token之Scope
我們都知道OAuth2的最終產物是提供給我們一個Access Token,而這個Access Token中包含了一個Scope的字段,這個字段代表的是授權服務器或者資源擁有者授予第三方客戶端允許操作資源服務器的哪些資源的范圍。這里有一點需要注意的是,這個授權過程可以有資源擁有着的參與(Authorization Code,Implicit,Resource Owner Password Credentials Grant),也可以沒有他的參與(Client Credentials Grant)。那么基於上述的books的資源,我們可以定義一個 book_manager 的Scope,來控制對books的五個操作的權限控制。那么Books的基於Scope的權限控制看起來就像是這樣的:
[Route("books")] public class BooksController : Controller { [HttpGet("")] [Scope("book_manager")] public Book[] Get() { return null; } [HttpGet("{bookId}")] [Scope("book_manager")] public Book Get(int bookId) { return null; } [HttpPost("")] [Scope("book_manager")] public Book Post(Book book) { return null; } [HttpPut("{bookId}")] [Scope("book_manager")] public Book Put(int bookId, Book book) { return null; } [HttpDelete("{bookId}")] [Scope("book_manager")] public Book Delete(int bookId) { return null; } }
注意看紅色的部分,為每一個操作都添加了一個Scope的描述。如果Access Token擁有book_manager這個Scope(不管他是OAuth2的哪一個授權方式頒發的,我們的最終代碼部分只認Scope),那么對這些API的調用就是被允許的,否則視為無權操作。
2 OIDC的Id Token之sub
關於Id Token的用途以及其包含哪些信息請參考Id Token。Id Token和Access Token的不同之處在於它一定是包含某一個用戶的標識 sub ,但是沒有Scope,這是因為Id Token的用途是認證當前用戶是誰,所以用戶是必須存在的;由於僅僅是認證,則不會包含被認證用戶可以做哪些操作之類的授權相關的事情。那么針對Id Token,我們的API應該如何進行權限管控呢?通常的做法是使用傳統的基於校色的權限控制(Role Based Access Control)。其實現細節就不解釋了,它的模型大致是:一個實體(用戶或者組織)擁有一組角色,每一個角色代表着一組權限集合。感覺是不是和Scope很像呢,其實差不多。我們定義一個這樣的角色 圖書管理員 吧。這里是故意和Scope的命名區分開的,因為其來源不同,那么我們最終實現的時候也會是獨立開來的。
1 [Route("books")] 2 public class BooksController : Controller 3 { 4 [HttpGet("")] 5 [Scope("book_manager")] 6 [Role("圖書管理員")] 7 public Book[] Get() { return null; } 8 9 [HttpGet("{bookId}")] 10 [Scope("book_manager")] 11 [Role("圖書管理員")] 12 public Book Get(int bookId) { return null; } 13 14 [HttpPost("")] 15 [Scope("book_manager")] 16 [Role("圖書管理員")] 17 public Book Post(Book book) { return null; } 18 19 [HttpPut("{bookId}")] 20 [Scope("book_manager")] 21 [Role("圖書管理員")] 22 public Book Put(int bookId, Book book) { return null; } 23 24 [HttpDelete("{bookId}")] 25 [Scope("book_manager")] 26 [Role("圖書管理員")] 27 public Book Delete(int bookId) { return null; } 28 }
如果 sub 代表的用戶自身擁有或者其所屬的組織機構擁有(不管其是怎么組織管理的吧,最終我們可以知道這個用戶是否具有某一個角色) 圖書管理員 這個角色。則允許其訪問books的這些操作。
3 以上兩種方式的弊端在哪里?
其實不止以上兩種,比如在Asp.Net Core中有內置的這些授權控制組件:
1 [Authorize(Policy = "AtLeast21")] 2 public class AlcoholPurchaseController : Controller 3 { 4 public IActionResult Login() => View(); 5 6 public IActionResult Logout() => View(); 7 }
以上這些本質上和上面的基於Scope和基於Role的屬於同一種類型。我們這樣做當然可以工作,但是問題來了,它們直觀嗎,靈活嗎?繁瑣嗎?好用嗎?能滿足我們變化的需求嗎?總有着一種把簡單的事情搞復雜的感覺。比如現在我增需要增加一個角色,超級管理員,那么上述的代碼是不是需要我們做出改變呢?
1 [HttpGet("")] 2 [Scope("book_manager")] 3 [Role("圖書管理員","超級管理員")] 4 public Book[] Get() { return null; }
再比如,現在需要增加一個Scope book_reader ,它只能執行讀取的操作,又要做出改變了吧。況且即使我們把Scope和Role合二為一了,還是混亂不堪。
4 基於權限為最小粒度的解決方案
那么造成這些問題的根本原因是什么?答:不管是Scope還是Role它們體現的都是一個隱式的描述信息,而不是某一個具體的操作行為的描述信息。既然我們知道了其症結所在,那么怎么解決這個問題呢?原理很簡單,使用權限作為我們的最小單元,把Scope和Role等等還有其他的一些管理組織權限的概念都作為一個中間層,禁止它們出現在接口權限驗證的地方,而是僅作為管理組織Permission的手段存在。然后改造上面的代碼如下:
1 [Route("books")] 2 public class BooksController : Controller 3 { 4 [HttpGet("")] 5 [Permission("books.read")] 6 public Book[] Get() { return null; } 7 8 [HttpGet("{bookId}")] 9 [Permission("book.read")] 10 public Book Get(int bookId) { return null; } 11 12 [HttpPost("")] 13 [Permission("book.add")] 14 public Book Post(Book book) { return null; } 15 16 [HttpPut("{bookId}")] 17 [Permission("book.edit")] 18 public Book Put(int bookId, Book book) { return null; } 19 20 [HttpDelete("{bookId}")] 21 [Permission("book.delete")] 22 public Book Delete(int bookId) { return null; } 23 }
我們把每一個操作都定義一個權限Permission,不管你是Access Token的Scope,還是Role,都不會在這里出現。比如在檢查超級管理員是不是能操作的時候,我們可以直接放行(把這些檢查和我們對接口的操作權限的描述分開)。如果是名為book_reader的Scope的時候,我們讓book_reader只關聯books.read和book.read這兩個Permission,而這種關聯關系的管理,我們是可以通過數據存儲來維持的,也很方便的提供管理頁面來靈活的配置。而最終的代碼上關心的只是Permission。這種方式可以稱為Resource Based Access Control或者Permission Based Access Control。
5 Apache Shiro
以上是我自己的一些理解和思路,然后我發現了Apache Shiro這個項目,感覺就像是找到了組織,Apache Shiro走的更遠,而且為Permission定義了一套規則。強烈建議讀一讀https://shiro.apache.org/permissions.html這篇文檔。而.Net這邊就沒有這么好的福氣了,,,Asp.Net Core中的默認授權過濾器還是傳統的方式。
不過基於Asp.Net Core的Filter:IAuthorizationFilter,我們可以把這一整套授權控制方式給替換掉:使用代碼:https://github.com/linianhui/oidc.example/tree/master/src/web.oauth2.resources;Filters代碼:https://github.com/linianhui/oidc.example/tree/master/src/aspnetcore.filters.permissions。
從此和討厭的 [Authorize(Roles ="圖書管理員",Policy ="XXX")] 說再見。
以上只是個人的一些理解,如有錯誤,歡迎指正。
參考
強烈推薦:https://shiro.apache.org/permissions.html
https://stormpath.com/blog/new-rbac-resource-based-access-control
https://docs.microsoft.com/en-us/aspnet/core/security/authorization/