在前面5篇博客中介紹了OAuth2和OIDC(OpenId Connect),其做用是受權和認證。那麼當咱們獲得OAuth2的Access Token或者OIDC的Id Token以後,咱們的資源服務如何來驗證這些token是否有權限來執行對資源的某一項操做呢?好比我有一個API,/books,它具備以下5個操做:html
POST /books | 添加一本書 |
GET /books/{id} | 獲取一本書 |
PUT /books/{id} | 更新一本書 |
DELETE /books/{id} | 刪除一本書 |
GET /books | 獲取書的列表 |
其僞代碼以下:git
[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和傳統的基於角色的權限控制是如何處理控制這些資源的操做。github
咱們都知道OAuth2的最終產物是提供給咱們一個Access Token,而這個Access Token中包含了一個Scope的字段,這個字段表明的是受權服務器或者資源擁有者授予第三方客戶端容許操做資源服務器的哪些資源的範圍。這裏有一點須要注意的是,這個受權過程能夠有資源擁有着的參與(Authorization Code,Implicit,Resource Owner Password Credentials Grant),也能夠沒有他的參與(Client Credentials Grant)。那麼基於上述的books的資源,咱們能夠定義一個 user_manager 的Scope,來控制對books的五個操做的權限控制。那麼Books的基於Scope的權限控制看起來就像是這樣的:web
[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擁有user_manager這個Scope(無論他是OAuth2的哪個受權方式頒發的,咱們的最終代碼部分只認Scope),那麼對這些API的調用就是被容許的,不然視爲無權操做。apache
關於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的這些操做。ui
其實不止以上兩種,好比在Asp.Net Core中有內置的這些受權控制組件:spa
1 [Authorize(Policy = "AtLeast21")] 2 public class AlcoholPurchaseController : Controller 3 { 4 public IActionResult Login() => View(); 5 6 public IActionResult Logout() => View(); 7 }
以上這些本質上和上面的基於Scope和基於Role的屬於同一種類型。咱們這樣作固然能夠工做,可是問題來了,它們直觀嗎,靈活嗎?繁瑣嗎?好用嗎?能知足咱們變化的需求嗎?總有着一種把簡單的事情搞複雜的感受。好比如今我增須要增長一個角色,超級管理員,那麼上述的代碼是否是須要咱們作出改變呢?3d
1 [HttpGet("")] 2 [Scope("book_manager")] 3 [Role("圖書管理員","超級管理員")] 4 public Book[] Get() { return null; }
再好比,如今須要增長一個Scope book_reader ,它只能執行讀取的操做,又要作出改變了吧。何況即便咱們把Scope和Role合二爲一了,仍是混亂不堪。code
那麼形成這些問題的根本緣由是什麼?答:不論是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。
以上是我本身的一些理解和思路,而後我發現了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/