在Keycloak中實現授權,首先需要了解與授權相關的一些概念。授權,簡單地說就是某個(些)使用者或者某個(些)使用者組(Policy),是否具有對某個資源(Resource)具有某種操作(Scope)的許可權(Permission)。所以,授權是一種許可權管理,它建立在認證的基礎上:使用者首先要完成認證(Authentication),才能談授權(Authorization)。在討論認證與授權的文章或論壇裡,往往用Authn代表認證(Authentication),而用Authz代表授權(Authorization)。
Keycloak中的授權模型
在上面這段描述中,我已經將幾個重要的概念用黑體字標註了。或許會有這樣的疑問:使用者/使用者組不應該是User/Group嗎?在談授權的時候,至少也應該是角色(Role)吧,比如我們熟悉的基於角色的訪問控制(RBAC),裡面就是角色,怎麼會是策略(Policy)呢?在回答這個問題前,還是先看一下Keycloak中的授權模型:
這個模型中,包含了幾個重要的概念:
- 資源(Resource):資源是應用程式中能夠被訪問的物件。假設有個有關天氣預報的API,它的URL是http://localhost:5678/WeatherForecast,那麼,在這臺資源伺服器上,URI /WeatherForecast就是一個資源的地址,它表示一個跟天氣預報相關的API端點資源
- 操作(Scope):其實Scope並不翻譯為“操作”,這裡我使用“操作”來表示Scope,是因為在授權的場景中,Scope就是定義針對資源的一些“操作”。比如:對於上面的天氣預報API資源,我們可以有獲取資源的操作,也可以有更新資源的操作(比如,讓氣象員根據其它科學資料來調整某地的天氣預報)。於是,在定義Scope的時候,可以用“weatherforecast.read”、“weatherforecast.update”這樣的名字來命名
- 對於某個資源,它可以宣告自己所需要的操作,例如,在RESTful API中,/WeatherForecast這個API可以有讀取(read/HTTP GET)的操作,也可以有更新(update/HTTP PATCH)的操作,那麼,就可以在這個API資源上宣告weatherforecast.read和weatherforecast.update這兩個Scope
- 一個使用者組(Group)可以包含多個子組,一個組下可以有多個使用者(User),一個使用者又可以屬於多個使用者組。對於一個使用者或者一個組而言,它可以扮演多種角色(Role),而一個角色又可以被賦予多個使用者或者多個使用者組,這些都是耳熟能詳的RBAC授權的基本概念,就不多說明了
- 策略(Policy)可以理解為滿足某種條件的資源訪問者(可以是使用者或使用者組),所以,Policy定義的是條件:角色就是一種條件,表示“被賦予某種角色”的條件。基於角色的策略實現的訪問控制,就是RBAC。當然條件不僅僅只有角色,使用者滿足某個條件也可以成為一種策略,比如要求某個使用者年齡大於18歲。除此之外,策略是可以被聚合的,聚合策略的投票結果(允許還是拒絕),取決於被聚合的策略以及投票的方式(是所有被聚合策略都允許,結果才被允許,還是隻要有一個投票為“允許”,整個聚合策略的結果就是“允許”),比如要求使用者是年齡大於18歲(User Policy)的系統管理員(Role Policy)。上圖中只簡單列了幾個繼承於Policy的子類用以示意,Keycloak所支援的策略型別不止這些
- 許可權(Permission)是資源(Resource)或者操作(Scope)與策略(Policy)之間的關聯關係。在Keycloak中,許可權分為兩種:基於資源的許可權和基於操作的許可權。表達的語義是:符合某些策略的訪問者對指定的資源或者操作可以訪問
理解了這些概念後,在Keycloak中實現授權並不困難。
演練:在Keycloak中實現授權
還是以Weather API為例,設定這樣的業務場景:
- 服務供應商(Service Provider)釋出/WeatherForecast API供外部訪問
- 在企業應用(Client)裡有三個使用者:super,daxnet,nobody
- 在企業應用裡有兩個使用者組:administrators,users
- 在企業應用裡定義了兩個使用者角色:administrator,regular user
- super使用者同時屬於users和administrators組,daxnet屬於users組,nobody部署於任何組
- administrators組被賦予了administrator角色,users組被賦予了regular user角色
- 對於/WeatherForecast API,它支援兩種操作:GET /WeatherForecast,用以返回天氣預報資料;PATCH /WeatherForecast,用以調整天氣預報資料
- 擁有administrator角色的使用者/組,具有PATCH操作的許可權;擁有regular user角色但沒有administrator角色的使用者/組,具有GET操作的許可權;沒有任何角色的使用者,就沒有訪問/WeatherForecast API的許可權
這個業務場景也可以用下面的圖來表述:
首先,在Keycloak中新建一個名為aspnetcoreauthz的Realm,在這個Realm下,新建三個User,分別是super,daxnet和nobody;然後新建兩個Group:administrators和users,將super使用者放到administrators組和users組裡,並將daxnet使用者放入users組裡。
然後,新建一個名為weatherapiclient的Client,在weatherapiclient的頁面裡,點選Roles選項卡,建立兩個名為administrator和regular user的角色,然後回到Groups裡,選中administrators組,在Role mapping中,將administrator角色賦予該組:
用同樣的方法,將regular user角色賦予users組。
現在進入Authorization選項卡,點選Scopes選項卡,然後點選Create authorization scope按鈕:
在Create authorization scope頁面中,Name欄位輸入weather.read,用同樣的方法,新建另一個Scope,名稱為weather.update。然後點選Resources選項卡,並點選Create resource按鈕,建立API resource:
在Create resource頁面,新建名為weather-api的資源,填入如下欄位,然後點選Save按鈕儲存:
回到Authorization標籤頁,點選Policies標籤頁,點選Create client policy按鈕,在彈出的對話方塊中,選擇Role,表示需要建立一個基於角色的策略。在Create role policy頁面,新建一個名為require-admin-policy的策略,在Roles部分,點選Add roles按鈕,選擇weatherapiclient下的administrator角色,然後點選Save按鈕儲存:
用同樣的方法建立require-registered-user策略,並將regular user作為角色加入。接下來開始建立許可權實體(Permission)。在Authorization選項卡里,點選Permission選項卡,然後點選Create permission,然後選擇Create scope-based permission。在Create scope-based permission頁面,建立一個名為weather-view-permission的Permission,Authorization scopes選擇weather.read,Policies選擇require-registered-user,這裡的語義已經很明白了:執行weather.read操作,需要require-registered-user策略,也就是要讀取天氣預報資訊,就需要已註冊使用者。點選Save按鈕儲存即可。
用同樣的方法建立另一個名為weather-modify-permission的Permission,Authorization scopes為weather.update,Policies為require-admin-policy。
接下來,就可以測試許可權的設定是否正確了。仍然在Authorization選項卡下,點選Evaluate選項卡,在Identity Information部分,Users裡選擇super:
然後點選Evaluate按鈕,之後就可以看到,weather-modify-permission和weeather-view-permission均投票為Permit,表示該使用者具有兩者許可權:
如果點選Show authorization data,則在彈出的Authorization data對話方塊中,可以看到token裡已經包含了授權資訊(authorization Claim):
{
"exp": 1712996185,
"iat": 1712995885,
"jti": "4f1178f2-5e8b-41e4-b726-da9120d77baa",
"aud": "weatherapiclient",
"sub": "44bbfc3a-16a0-499a-aae9-a2aa36219d33",
"typ": "Bearer",
"azp": "weatherapiclient",
"session_state": "2b228dd4-38c8-4002-bc11-b35ecd109a63",
"acr": "1",
"allowed-origins": [
"/*"
],
"realm_access": {
"roles": [
"default-roles-aspnetcoreauthz",
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"weatherapiclient": {
"roles": [
"administrator",
"regular user"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"authorization": {
"permissions": [
{
"scopes": [
"weather.update",
"weather.read"
],
"rsid": "f6fd1d6f-3bfd-44a1-a6fb-a1fb49769ac9",
"rsname": "weather-api"
}
]
},
"scope": "email profile",
"sid": "2b228dd4-38c8-4002-bc11-b35ecd109a63",
"email_verified": false,
"name": "Admin User",
"groups": [
"/administrators",
"/users"
],
"preferred_username": "super",
"given_name": "Admin",
"family_name": "User",
"email": "super@abc.com"
}
換一個使用者,如果選擇daxnet,可以看到,weather-view-permission為Permit,而weather-modify-permission為Deny:
再將使用者換為nobody測試一下,發現兩個Permission的結果都為Deny:
透過token API端點請求授權資訊
要使用OpenID Connect的token API端點獲得某個使用者的授權資訊,需要首先得到Bearer token:
然後,使用這個Bearer token,再次呼叫token API,注意此時的grant_type為 urn:ietf:params:oauth:grant-type:uma-ticket
,audience為Client ID,即weatherapiclient:
在jwt.io中解碼第二步生成的這個access_token,就可以拿到授權資訊了: