Try   HackMD
tags: keycloak Tool

六、多角色權限測試實作

在前面的章節中,我們詳細探討了Keycloak的基礎知識,這一Part稍微講一下如何實際運用Keycloak,結合角色權限設置,來實作不同User具有不同角色權限來使用API的功能。

  1. 至Keycloak服務設置Client、User與Role設置 (版本17.0.1 )
  2. 工具測試
  3. 撰寫AP Code

另外有點需要事先提醒注意一下,文章提供的Url格式! 例如文章提到

Url設定 : http://{your-keycloak-server}/realms/{your-realm-name}/protocol/openid-connect/token

http://{your-keycloak-server}/realms

這段BaseUrl還是需要照你實際的部屬去設定,比較準確地可以對應你的Clients List的Account的Base URL如下

image.png

一、至Keycloak服務設置Client、User與Role設置

新增Realm與Client設置可參考Keycloak串接Google Auth設置與Demo Code

  • Step1 : 左側欄點選Users
    • 點選Add user
    • 輸入username, 其餘資料看你是否想設置(User Enabled:On,Email Verified:Off)
    • 到Credentials設置password
    • 登出,瀏覽器Url輸入 (your-keycloak-host)/realms/(realm-name)/account/
    • 按登入輸入帳密看是否能登入,能登入表示創建User成功

備註:若你是使用Identity providers來串接第三方驗正,可以略過Step1建立User,使用者會在第一次登入的時候建立起來。

  • Step2 : 角色創建 (Role),Role在Keycloak最基礎的可以區分為Realm與Client兩個層級

    • 關於Realm Role設置
      • 點擊首頁的側邊Realm Role
      • 點擊 Add Role,然後輸入Role Name與描述創建即可

    • 關於Client Role設置
      • 點選你所建立AP要對應的Client
      • 點選Client 的 Tab (Roles)或者
      • 點擊 Add Role,然後輸入Role Name與描述創建即可

在設置Client與Realm Role差異在於在AccessToken顯示的時候不同,但無論你在哪一個地方設置的Role都可以將他與Group綁定。

以下是一個,AccessToken Decode後的範例,可以觀察到resource_access他的結構多了一層Client ID,而在realm_access就不會有Client ID的那一層(Realm Role會放在realm_access,而Client Role會放在resource_access),

Realm角色(Realm Roles)是全域性的它們在整個Realm範圍內都是有效的,如果你賦予某個用戶一個Realm角色,那麼這個用戶在整個Realm中的任何地方都會有這個角色的權限。這很適合那些通用的、跨多個應用程式的角色,例如「管理員」或「審核員」。

Client角色(Client Roles)是特定於某個客戶端(Client)的。客戶端通常對應於一個具體的應用程式或服務。當你創建一個Client角色時,只有在那個特定的客戶端範圍內,該角色才會生效。比如,你可能會有一個名為「客戶端A的編輯者」的角色,只有在客戶端A中,賦予了這個角色的用戶才有編輯的權限。

簡單來說,Realm角色更像是給予用戶一個Realm範圍內的通行證,而Client角色則像是特定應用程式的門禁卡。

但如果不這麼要求這兩種分類的權限職責,基本上設計這兩種Role都可以使用,差異在於階層的問題以及後續程式的處理。

{
"sub": "c34597a7-2ae7-43f0-a96e-d33205839d00",
"resource_access": {
             "google": {
                            "roles": [
                                        "TEST",
                                        "client_user",
                                        "client_admin"
                                     ]
                       },
            "account": {
                            "roles": [
                                        "manage-account",
                                        "manage-account-links",
                                        "view-profile"
                                     ]
                       }
           },
"email_verified": false,
"allowed-origins": [],
"iss": "http://localhost:8081/realms/google-so",
"typ": "Bearer",
"preferred_username": "XXXXXXXX@gmail.com",
"given_name": "XX",
"nonce": "3GBoFrYNDLNfgrjwuYEBsRjxpcN5s6LpgCL7Tg04kLw",
"sid": "e4085e0a-9892-4345-8a23-e7e04d84d477",
"aud": [],
"acr": "1",
"realm_access": {
        "roles": [
                    "default-roles-google-so",
                    "offline_access",
                    "admin",
                    "uma_authorization"
                 ]
        },
"azp": "test",
"auth_time": 1698890409,
"scope": "openid profile roles email",
"name": "XX X",
"exp": "2023-11-02T02:05:10Z",
"session_state": "e4085e0a-9892-4345-8a23-e7e04d84d477",
"iat": "2023-11-02T02:00:10Z",
"family_name": "X",
"jti": "fb64460b-efde-4bb9-a232-c8222abaeeb0",
"email": "XXXXXXXX@gmail.com"
}
  • Step3 : 將Role加入User 或者 以加入Group在把User加入Group中

    • 以User角度出發管理權限

      • 再次點擊Users,並點選你的User

      • 點擊Role Mapping Tab

      • 將你想要的Role加入(按Add selected)即可
      • 注意Assign Role可以選擇Filter by 『realm roles』或『Clients』

    • 以Group角度出發管理權限

      • 點擊創建Group

      • 點擊Role Mapping Tab

      • 點選Assign roles
      • 注意Assign Role可以選擇Filter by 『realm roles』或『Clients』
      • 將你要的Role加入

      • 將User加入Group中

二、Tool Test

平台User與Role設置完後,我們可以使用一些工具來跟平台索取Access Token,解析Token是否有正確的角色訊息。

a. Postman獲取User相關Token訊息

  • POST Url設定 : http://{your-keycloak-server}/realms/{your-realm-name}/protocol/openid-connect/token

  • Body選x-www-form-urlendoded,設置參考如下 (請替換掉value)

image.png

接著按Send會得到如下訊息

image.png

你可以從access token解析,就能看到相關Role訊息

image.png

三、撰寫AP Code (Demo Code)

接著進入寫AP串接Keycloak,這邊會紀錄一下c#跟java程式碼是如何撰寫去串接Keycloak服務

a.C# Demo (ASP.NET Core SSO參考)

1. Tech list

  • Net Core 6.0
  • Keycloak

2. 事件準備

2-1 Dependency

我們不用第三方套件,直接選用官方的Authentication OpenIdConnect套件,另外還需要Newtonsoft幫我們去解析token的json資料。

  • Microsoft.AspNetCore.Authentication.OpenIdConnect 6.0.24
  • Newtonsoft

2-2 配置appsettings.json

OpenIdConnect的配置,基本上會在DI與Middleware設置好,但設置過程中的一些設定建議放在json設定檔內。須設定項目如下,{}會根據你的實際環境情境而設置,實際設置範例可以直接參考Code。

"Keycloak": { "CookieName": "set-cookie-name", "ServerRealm": "http://{keycloak-server-url}/realms/{realm name}", "Metadata": "http://{keycloak-server-url}/realms/{realm name}/.well-known/openid-configuration", "ClientId": "{client-id}", "ClientSecret": "{client secret}", "TokenExchange": "http://{keycloak-server-url}/realms/{realm name}/protocol/openid-connect/token", "Audience": "{一般都account}", "RequireHttpsMetadata": "{false or true}", "SaveTokens": "true", "ResourceTag": "resource_access", "RoleTag": "roles" },

另外如果沒特別去修改Keycloak平台上的Client Scope的Roles Mappers,一般而言平台給的ResourceTag與RoleTag預設就會是resource_access與roles

image.png

3. 設置 DI

3-1 AddAuthentication

設定應用程式中身份驗證服務的入口。這裡它設定了兩個默認的方案,

  • DefaultScheme 設定: 當一個使用者成功登錄後,系統會創建一個Cookie存儲在使用者的瀏覽器中。這個Cookie包含了一個標識符,它可以在使用者之後的每次請求中被送回服務器,由服務器識別出這個使用者,這樣就不需要在每次請求時重新登錄。

  • DefaultChallengeScheme 設定 : 當應用程式中的某個資源或操作需要身份驗證,但當前使用者未通過驗證時,會使用此方案。"Challenge"是一個用於描述“要求使用者進行身份驗證”的行為。它告訴應用程式使用OpenID Connect協議來挑戰使用者。所以當使用者嘗試訪問一個需要登錄的頁面時,應用程式會重定向使用者到Keycloak的登錄頁面。

結合使用這兩種方案,當使用者訪問需要認證的資源時,應用程式會檢查Cookie來確定使用者是否已經登錄。如果Cookie不存在或無效,應用程式會通過OpenID Connect方案來挑戰使用者,這通常意味著將使用者重定向到認證服務(在這個案例中是Keycloak)的登錄頁面,讓使用者進行身份認證。

builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; })

3-2 AddCookie

配置了cookie身份認證的細節,比如登入路徑、cookie名稱、有效時間、安全策略。

  • LoginPath : 設定了當使用者試圖訪問一個需要身份驗證的資源,但他們未被驗證(即沒有有效的Cookie)時,應用程式應該將他們重定向到哪個路徑進行登錄。

  • options.Cookie.Name : 存放身份驗證票據的Cookie的名稱,這個名稱通常會從應用程式的配置文件(比如appsettings.json)中讀取。

  • options.Cookie.MaxAge : Cookie的最大有效期限。範例設為60分鐘,意味著一旦設置,這個Cookie在60分鐘後將會過期,除非這個時間被滑動過期機制重置。

  • options.Cookie.SecurePolicy : 決定了Cookie是否應該只在HTTPS請求中被發送。範例使用 SameAsRequest,意味著Cookie的安全策略會與發出請求的策略相同。如果請求是透過HTTPS發送的,那麼Cookie也會被標記為安全的,只能在之後的HTTPS請求中被發送。

  • options.SlidingExpiration : 滑動過期是指,每當系統收到帶有有效Cookie的請求時,系統會更新這個Cookie的過期時間。

.AddCookie(options => { options.LoginPath = "/api/user/Login"; options.Cookie.Name = builder.Configuration.GetSection("Keycloak")["CookieName"]; options.Cookie.MaxAge = TimeSpan.FromMinutes(60); options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.SlidingExpiration = true; })

3-3 AddOpenIdConnect

設定與OpenID Connect提供者(這裡是Keycloak)的通信。範例設定了Keycloak服務器的地址、客戶端ID和密鑰、回應類型等。此外,這個設定指示應用程式是否儲存令牌,以及是否要求HTTPS元數據

  • Authority : 指明 OpenID 提供者的地址。在這裡,這個地址會告訴你的應用程式在哪裡可以找到 Keycloak 伺服器的授權端點。

  • RequireHttpsMetadata : 應用程式在請求 Keycloak 伺服器的 metadata 時是否要求使用 HTTPS 連線。在生產環境中,這應該設置為 true 以保證安全,但在開發環境中,為了方便,有時會設置為 false。

  • ClientId 、 ClientSecret : Keycloak 中註冊你的應用程式時得到的憑證。ClientId 是應用程式的唯一標識,而 ClientSecret 是一個秘密鑰匙,用於在應用程式與 Keycloak 之間安全地通信。

  • ResponseType : 指定了使用授權碼流程(Authorization Code Flow),範例的Code設置是授權碼流程一種安全的方式,允許應用程式先取得一個授權碼,然後用它去換取一個 Access Token 和/或 ID Token。這個流程需要客戶端先送出一個登錄請求,然後使用者會在Keycloak登錄並授權,最後客戶端使用回傳的授權碼去換取令牌。

.AddOpenIdConnect(options => { options.Authority = builder.Configuration.GetSection("Keycloak")["ServerRealm"]; // 只推薦在開發環境使用 options.RequireHttpsMetadata = bool.Parse(builder.Configuration.GetSection("Keycloak")["RequireHttpsMetadata"]); options.ClientId = builder.Configuration.GetSection("Keycloak")["ClientId"]; options.ClientSecret = builder.Configuration.GetSection("Keycloak")["ClientSecret"]; options.ResponseType = OpenIdConnectResponseType.Code; // 這行如果你想從HttpContext獲取Token的話你就要加這個設定 options.SaveTokens = bool.Parse(builder.Configuration.GetSection("Keycloak")["SaveTokens"]); });

3-4 Event,Access Token 解析手動放入Claims

這段會在OpenID Connect令牌被驗證後執行。用來手動將從Keycloak獲取的access token中提取的聲明(claims)添加到用戶的身份證明中。這些聲明通常包括使用者角色和其他重要資料。步驟如下

  • 檢查 TokenEndpointResponse : 先檢查 context.TokenEndpointResponse 是否為 null。這是個安全檢查,以確保有一個回應可用,回應中應該包含了授權伺服器返回的令牌資訊。

  • 提取 Access Token : 從中提取 Access Token。

  • 檢查 Principal : 表示當前用戶的安全主體,包含用戶的身份資訊。如果 Principal 為 null,將不進行進一步操作。

  • 解析 Access Token : 使用了 JwtSecurityTokenHandler 來解析 JWT(Json Web Token)格式的 Access Token。

  • 從 JWT 提取資源存取聲明 : 尋找JWT中的特定聲明,這裡用 resourceName 參數來尋找。這通常是一個包含了用戶角色和許可權資訊的 JSON 物件。

  • 解析資源存取聲明 : 解析它的 JSON 結構來提取用戶的角色資訊。

  • 遍歷解析後的角色資訊,並將每個角色作為一個新的聲明(Claim)添加到 ClaimsIdentity 中。

通過這個流程,應用程式就能夠把從 Keycloak 來的 Access Token 轉換為應用程式能理解和利用的用戶身份資訊。可應用於用戶角色的訪問控制,並且是在用戶成功驗證後立即完成的。這樣的設計模式增強了安全性,同時也確保了用戶在不同服務之間的無縫體驗。

// Access Token 解析手動放入Claims options.Events = new OpenIdConnectEvents { OnTokenValidated = context => { // 檢查這裡是否有拿到 Access Token if (context.TokenEndpointResponse == null) { Console.WriteLine("TokenEndpointResponse is null!!!!!!"); return Task.CompletedTask; } var accessToken = context.TokenEndpointResponse.AccessToken; if (accessToken == null) { Console.WriteLine("AccessToken is null!!!!!!"); return Task.CompletedTask; } if (context.Principal == null) { Console.WriteLine("Principal is null!!!!!!"); return Task.CompletedTask; } var handler = new JwtSecurityTokenHandler(); var jsonToken = handler.ReadToken(accessToken) as JwtSecurityToken; var claimsIdentity = context.Principal.Identity as ClaimsIdentity; if (jsonToken == null || claimsIdentity == null) { Console.WriteLine("jsonToken or claimsIdentity is null!!!!!!"); return Task.CompletedTask; } var resourceName = builder.Configuration.GetSection("Keycloak")["ResourceTag"]; var resourceAccess = jsonToken.Claims.FirstOrDefault(c => c.Type == resourceName)?.Value; if(resourceAccess == null) { Console.WriteLine("resourceAccess is null!!!!!!"); return Task.CompletedTask; } var parsedResourceAccess = JObject.Parse(resourceAccess); if(parsedResourceAccess == null) { Console.WriteLine("parsedResourceAccess is null!!!!!!"); return Task.CompletedTask; } var clientID = builder.Configuration.GetSection("Keycloak")["ClientId"]; var roleTagName = builder.Configuration.GetSection("Keycloak")["RoleTag"]; var roles = parsedResourceAccess[clientID][roleTagName]; foreach (var role in roles) { claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role.ToString())); } return Task.CompletedTask; }, };

4 Middlewarw設置

DI設置完後,接著最主要就需要加入Authentication與Authorization的中介設置(順序不可對調)

  • app.UseHttpsRedirection() : 這一中間件開始處理對來自用戶請求的身份認證。它基於前面設定的身份認證方案(比如Cookie認證和OpenID Connect),確定用戶是否已經登入,並設定 HttpContext.User 屬性。這個步驟對於後面進行授權檢查是必須的。

  • app.UseAuthorization() : 在認證後,這個中間件負責檢查和執行授權政策。它根據你設定的政策(比如角色基於的授權),確認經過認證的用戶是否有權限執行他們請求的操作。如果用戶沒有足夠的權限,請求將被拒絕。

app.UseAuthentication(); app.UseAuthorization();

5 Controller角色權限設置

設置完成後,基本上你就可以在Controller設置,使用 Authorize 屬性來確保只有具有特定角色的經過認證的使用者可以訪問某個控制器或動作方法。在這個例子中,Read 方法被裝飾了 [Authorize(Roles = "admin,read")],這意味著只有當使用者具有 "admin" 或 "read" 這兩個角色中的任何一個時,才允許對這個方法進行HTTP GET請求。

此外,某個動作方法不應該被身份認證機制限制,可以使用AllowAnonymous屬性。

[Authorize(Roles = "admin,read")] [HttpGet(nameof(Read))] public IActionResult Read() { return Ok("Read OK"); } // 如果某個動作方法不應該被身份認證機制限制,可以使用AllowAnonymous屬性。 [AllowAnonymous] [HttpGet(nameof(Public))] public IActionResult Public() { // 這個方法無需任何身份認證和授權,任何人都可以訪問。 return Ok("This is a public endpoint, no roles required."); }

6 快速Demo操作

  • Keycloak平台在Client建置2個Client Role,一個read另一個write

  • Keycloak平台建立一個User,並配置read,write role給他

  • 設置Keycloak程式設定並啟動Demo Code

  • 開一個無痕瀏覽器

  • url輸入 https://localhost:7005/api/user/Login

  • 此時你會看到一個登入畫面

    • image.png
  • 輸入帳號密碼,就會看到token相關訊息

    • image.png
  • 接著url輸入 https://localhost:7005/api/role/getroles 就能看到角色清單

    • image.png
  • url 輸入https://localhost:7005/api/role/create 就會顯示建立成功

    • image.png
  • url 輸入https://localhost:7005/api/role/edit 基本上就會顯示一個AccessDenied的訊息

b.Java Demo

在這裡會簡單的範例,撰寫一下Java使用Spring相關套件如何與Keycloak對接,並且如何透過IdToken或者AccessToken解析對應Role到Spring Security中,並且使用Google OAuth2.0作為第三方登入的驗證。

1. Tech list

  • JDK 17
  • Spring Boot 3
  • Spring Security
  • Keycloak
  • Google OAuth2.0

2. 事前準備

2-1 Dependency

在一般建立一個Spring Web的基礎之外,我們要選用一下Spring Security的套件,因為我們會去建立OAuth的驗證方式,所以除了Spring Security核心以外,還要再加入Spring OAuth相關套件。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

2-2 配置application.properties 或 application.yml

Spring Security有針對OAuth相關提供一些自動化配置的參數設定,當這些配置好基本上我們的程序都可以和Keycloak溝通了,但在Role獲取時會有一些問題,Spring Security在做權限映射的是依照OCIDUserToken中的SCOPE來做映射,因此跟我們期望的realm_access或者resouce_access都不一致,後續會講到如何自己來Mapping。

基本上這些配置已經滿足我們這次Demo所需要的配置,如果需要其他相關配置如logout可以參考這個網址

https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html

另外Keycloak有一個網頁他可顯示他所有配置的Endpoint網址以及一些相關設定資訊,把下面網址的[keyclaokServer] 和[realm]置換成你的設定,然後用瀏覽器開啟就可以看到了。

http://[keycloakServer]/realms/[realm]/.well-known/openid-configuration

# OAuth2 Client settings
spring.security.oauth2.client.registration.keycloak.client-id=[Client ID]
spring.security.oauth2.client.registration.keycloak.client-secret=[Client Secret]
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code

# Spring Security oauth2 Endpoint
spring.security.oauth2.client.registration.keycloak.redirect-uri=http://[IP or Domain]/login/oauth2/code/keycloak
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,roles

## OAuth2 Provider settings for Keycloak
spring.security.oauth2.client.provider.keycloak.authorization-uri=[YOUR_KEYCLOAK_AUTHORIZATION_URI]
spring.security.oauth2.client.provider.keycloak.token-uri=[YOUR_KEYCLOAK_TOKEN_URI]
spring.security.oauth2.client.provider.keycloak.user-info-uri=[YOUR_KEYCLOAK_USER_INFO_URI]
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
spring.security.oauth2.client.provider.keycloak.jwk-set-uri=[YOUR_KEYCLOAK_JWK_SET_URI]
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=[YOUR_KEYCLOAK_JWK_SET_URI]

spring:
security:
oauth2:
  client:
    registration:
      keycloak:
        client-id: [Client ID]
        client-secret: [Client Secret]
        authorization-grant-type: authorization_code
        redirect-uri: http://[IP or Domain]/login/oauth2/code/keycloak
        scope: openid,profile,roles
  provider:
    keycloak:
      authorization-uri: [YOUR_KEYCLOAK_AUTHORIZATION_URI]
      token-uri: [YOUR_KEYCLOAK_TOKEN_URI]
      user-info-uri: [YOUR_KEYCLOAK_USER_INFO_URI]
      user-name-attribute: preferred_username
      jwk-set-uri: [YOUR_KEYCLOAK_JWK_SET_URI]
security:
oauth2:
  resourceserver:
    jwt:
      jwk-set-uri: [YOUR_KEYCLOAK_JWK_SET_URI]

3. 設定SecurityConfig - UserInfo Customizer

3-1 基於UserInfo進行Authorities客製化配置

在Spring login的過程中,他會透過『GrantedAuthoritiesMapper』來去配置我們的權限,當然之前有提到他會透過SCOPE的內容來幫我們配置,因此我們想要取得Keycloak的設定必須自己動手做。

首先建立我們的SecurityConfig基本的filter可能會長這樣

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(
        authorizeRequests ->
            authorizeRequests.mvcMatchers("/public").permitAll().anyRequest().authenticated());


    http.oauth2Login(Customizer.withDefaults());


    return http.build();
}
}

上面這段程序,我們在使用的時候其實已經達到驗證的功能,可確保使用者是透過Keycloak去跟Google Oauth2驗證完後的結果,但這樣子我們調用ROLE出來是IdToken的SCOPE的樣子。

得到的權限會長這樣 -

這時候如果我們想透過UserInfo(IdToken)去取的權限,在Keycloak必須要做一點調整。

  • Stpe 1 調整Client scopes,點選roles

  • Stpe 2 點選Mappers

這時看到底下有client roles 和 realm roles,就要看你的Role是設定在client層級還是realm層級,然後給他點下去。

  • Stpe 3 點選進來後可以在下方看到,把add to userinfo給他選起來

這裡可以注意到他原先只有開啟的是add to access token,這也就是說為什麼我們可以在access token看得到這些我們自定義的規則了。

  • Stpe 4 調整程式

這裡的動作原理基本上就是在整個login chain中,我們去複寫掉他預設的GrantedAuthoritiesMapper,使她在映射權限的階段是由我們自定義的程式去實作。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private static final String REALM_ACCESS_CLAIM = "realm_access";
private static final String ROLES_CLAIM = "roles";

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(
        authorizeRequests ->
            authorizeRequests.mvcMatchers("/public").permitAll().anyRequest().authenticated());


    http.oauth2Login(oauth2Login -> 
                        oauth2Login.userInfoEndpoint(userInfoEndpoint -> 
                                                        userInfoEndpoint.userAuthoritiesMapper((this.userAuthoritiesMapper())
                                                        )
                        )
    );


    return http.build();
}

private GrantedAuthoritiesMapper userAuthoritiesMapper() {
    return (authorities) -> {
        Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

        authorities.forEach(authority -> {
            if (OidcUserAuthority.class.isInstance(authority)) {
                OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
                OidcIdToken idToken = oidcUserAuthority.getIdToken();
                OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
                Map<String, Object> realm = userInfo.getClaim(REALM_ACCESS_CLAIM);
                var roles = (Collection<String>) realm.get(ROLES_CLAIM);
                mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));

            }
        });

        return mappedAuthorities;
    };
}
Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
    return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
}

}

讓我們測試一下設定,我在Realm層級增加一個權限『ADMIN』。

然後來看看結果,現在就可以發現到我們的權限都是透過Realm去解析出來的了,若你要Client Access那麼就要增加解析resource_access的資訊。

4. 設定SecurityConfig - AccessToken Customizer

還記得在上面介紹到的keycloak需要設定權限要加入UserInfo,我們在程式中才能去使用UserInfo去映射我們的權限,但AccessToken就不用在Keycloak中設定,他預設的realm_access和clien_access(resource_access)本身就會預設加入AccesToken中,因此我們只要針對Spring Security的Login Filterr Chain進行客製化就好了。

4-1 基於AccessToken進行Authorities客製化配置

首先我們還是拿最基礎的樣板來做調整,因為在接下來調整的地方與前面提到的UserInfo不同。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(
        authorizeRequests ->
            authorizeRequests.mvcMatchers("/public").permitAll().anyRequest().authenticated());


    http.oauth2Login(Customizer.withDefaults());


    return http.build();
}
}

首先說明一下在權限這塊資訊,在Spring Security是綁定在UserInfo中,且AccessToken也會放在這裡面,因此在HttpSecurity有提供一個客製化的地方UserInfoEnpoint,這邊要去透過『OidcUserService』去調整我們的權限設定,基本上這相對於剛剛所示範的GrantedAuthoritiesMapper層級還要高一些。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(
        authorizeRequests ->
            authorizeRequests.mvcMatchers("/public").permitAll().anyRequest().authenticated());


        http.oauth2Login(oauth2 -> oauth2
            .userInfoEndpoint(userInfo -> userInfo
                    .oidcUserService(this.oidcUserService())
            )
        );


    return http.build();
}

private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
    final OidcUserService delegate = new OidcUserService();

    return (userRequest) -> {
        // Delegate to the default implementation for loading a user
        OidcUser oidcUser = delegate.loadUser(userRequest);
        OAuth2AccessToken accessToken = userRequest.getAccessToken();
        Jwt jwt = jwtDecoder.decode(accessToken.getTokenValue());

        return new DefaultOidcUser(extractResourceRoles(jwt), oidcUser.getIdToken(), oidcUser.getUserInfo());
    };
}

private Collection<GrantedAuthority> extractResourceRoles(Jwt jwt) {
    Map<String, Object> resourceAccess = jwt.getClaim("realm_access");
    Collection<String>resource;
    if (resourceAccess == null
            || (resource = (Collection<String>) resourceAccess.get("roles")) == null) {
        return Collections.emptySet();
    }

    return resource.stream()
            .map(role -> new SimpleGrantedAuthority(role))
            .collect(Collectors.toSet());
}

}

上述的程式中可以看到我們在OAuth2UserService就先把AccessToken進行解析,直接將權限組裝好回傳,使得後續程式就可以取得這些權限。

那在Keycloak就只要單純的去將ROLE設置給GROUP並且將使用者歸類在哪些GROUP中。

那看一下我們現在設定的權限規劃是這樣 -

ADMIN_GROUP

  • ROLE_ADMIN
  • ROLE_VIEWER
  • ROLE_EDITOR

EDITOR_GROUP

  • ROLE-VIEWER
  • ROLE_EDITOR

VIEWER_GROUP

  • ROLE_VIEWER

現在以ADMIN權限登入可以看到權限如下:

以EDITOR權限登入:

以VIEWER權限登入:

5. 以Annotation綁定API認證權限

在辛苦的搞懂上述的東西後,最終這些程式必須要依照權限來劃分可以訪問的API,因此我們在稍微調整一下程式。

將原本的SecurityConfig的@EnableWebSecurity改為@EnableMethodSecurity。

@Configuration
@EnableMethodSecurity
class SecurityConfig {

修改我們的Controller將權限綁在入口上,可以透過@PreAuthorize去告訴我們的Spring這個API需要什麼權限才可以訪問。

他的寫法有很多種方式,可以參考這個網址。

https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html

@GetMapping("/admin")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public ResponseEntity<String> admin(Authentication authentication) {
    return ResponseEntity.ok("This is Admin");
}

Reference

https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.
https://docs.spring.io/spring-security/reference/servlet/oauth2/login/advanced.html
https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html
https://developers.redhat.com/articles/2023/07/24/how-integrate-spring-boot-3-spring-security-and-keycloak