HTMLTAG_65__HTMLTAG_66___1。後端整合概述___HTMLTAG_67__HTMLTAG_68
Keycloak 提供標準的 OAuth2/OIDC 機制,允許與任何支援 JWT 驗證的後端框架整合。在本文中,我們將整合 Java 生態系統中兩個最受歡迎的框架:Spring Boot 3 和 Quarkus.
___HTMLTAG_77__HTMLTAG_78___框架___HTMLTAG_79__HTMLTAG_80___函式庫___HTMLTAG_81__HTMLTAG_82___方法___HTMLTAG_83__HTMLTAG_84___ ___HTMLTAG_87__HTMLTAG_88___Spring Boot 3___HTMLTAG_89__HTMLTAG_90__HTMLTAG_91___spring-boot-starter-oauth2-resource-server___HTMLTAG_92__HTMLTAG_93__HTMLTAG_94___JML___GML4UUS____UTUUS_UUSUTUUUU感興趣感興趣 ___HTMLTAG_97__HTMLTAG_98___Quarkus___HTMLTAG_99__HTMLTAG_100__HTMLTAG_101___quarkus-oidc___HTMLTAG_102__HTMLTAG_103__HTMLTAG_104___HTMLTAG_102__HTMLTAG_103__HTMLTAG_104_____DC架構概述:
預編碼_0
HTMLTAG_111__HTMLTAG_112___2。 Spring Boot 3 + Keycloak 整合___HTMLTAG_113__HTMLTAG_114
HTMLTAG_115__HTMLTAG_116___2.1 Maven 依賴項___HTMLTAG_117__HTMLTAG_118
預編碼_1
___HTMLTAG_119__HTMLTAG_120___重要說明: 從 Keycloak 20+ 開始,專用 Spring Boot 適配器 (keycloak-spring-boot-starterML_1231141412370 __123__124123__12 123__124___123____124123。目前的標準方法是使用 Spring Security 的 spring-boot-starter-oauth2-resource-server.
HTMLTAG_129__HTMLTAG_130___2.2 Application.yml 設定___HTMLTAG_131__HTMLTAG_132
預編碼_2
屬性解釋:
___HTMLTAG_137__HTMLTAG_138___屬性___HTMLTAG_139__HTMLTAG_140___說明___HTMLTAG_141__HTMLTAG_142___ ___HTMLTAG_145__HTMLTAG_146__HTMLTAG_147___issuer-uri___HTMLTAG_148__HTMLTAG_149__HTMLTAG_150___Keycloak 領域的 URI,用於驗證 JWT 中的1____HT聲明___HTMLTAG_153__HTMLTAG_154___
___HTMLTAG_155__HTMLTAG_156__HTMLTAG_157___jwk-set-uri___HTMLTAG_158__HTMLTAG_159__HTMLTAG_160___端點包含用於驗證 JWT 簽章的公用金鑰 (JWKS)____HTMLTAG_16162162162162122_____
HTMLTAG_165__HTMLTAG_166___2.3 安全設定___HTMLTAG_167__HTMLTAG_168
預編碼_3
HTMLTAG_169__HTMLTAG_170___2.4 自訂 JwtAuthenticationConverter___HTMLTAG_171__HTMLTAG_172
Keycloak 根據特殊結構儲存 JWT 聲明中的角色。需要自訂轉換器來提取正確的角色:
預編碼_4
預編碼_5
角色映射的工作原理:
預編碼_6
HTMLTAG_177__HTMLTAG_178___2.5 具有 RBAC 的 REST 控制器___HTMLTAG_179__HTMLTAG_180
預編碼_7
HTMLTAG_181__HTMLTAG_182___2.6 CORS 設定___HTMLTAG_183__HTMLTAG_184
前端(React/Angular)呼叫後端API時,需要設定CORS:
預編碼_8
將 CORS 加入到 SecurityFilterChain:
預編碼_9
HTMLTAG_191__HTMLTAG_192___2.7 處理令牌過期___HTMLTAG_193__HTMLTAG_194
Spring Security 自動驗證 exp 聲明。當令牌過期時,伺服器傳回 HTTP 401:
預編碼_10
已在 SecurityFilterChain 中註冊:
// Trong SecurityConfig
@Autowired
private CustomAuthenticationEntryPoint authEntryPoint;
// Trong securityFilterChain()
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
.authenticationEntryPoint(authEntryPoint)
)
HTMLTAG_203__HTMLTAG_204___3。 Quarkus + Keycloak 整合___HTMLTAG_205__HTMLTAG_206
HTMLTAG_207__HTMLTAG_208___3.1 Quarkus OIDC 擴充___HTMLTAG_209__HTMLTAG_210
Quarkus 提供了 quarkus-oidc 與 Keycloak 整合的擴充:
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<!-- Optional: Keycloak Authorization Policy Enforcer -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
HTMLTAG_215__HTMLTAG_216___3.2 應用程式屬性___HTMLTAG_217__HTMLTAG_218
# src/main/resources/application.properties
# ===== OIDC Configuration =====
quarkus.oidc.auth-server-url=http://localhost:8080/realms/my-realm
quarkus.oidc.client-id=my-quarkus-client
quarkus.oidc.credentials.secret=my-client-secret
# Application type: service (Resource Server) hoặc web-app (OIDC login)
quarkus.oidc.application-type=service
# Token verification
quarkus.oidc.token.issuer=http://localhost:8080/realms/my-realm
quarkus.oidc.token.audience=my-quarkus-client
# Role mapping - Keycloak roles source
quarkus.oidc.roles.role-claim-path=realm_access/roles
quarkus.oidc.roles.source=accesstoken
# ===== HTTP Configuration =====
quarkus.http.port=8081
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:3000,http://localhost:4200
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
quarkus.http.cors.headers=Authorization,Content-Type
HTMLTAG_219__HTMLTAG_220___3.3 具有 @RolesAllowed 的 REST 資源___HTMLTAG_221__HTMLTAG_222
package com.example.resource;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Map;
import java.util.Set;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/api")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class DemoResource {
@Inject
SecurityIdentity securityIdentity;
@Inject
JsonWebToken jwt;
// ========== Public ==========
@GET
@Path("/public/health")
@PermitAll
public Response health() {
return Response.ok(Map.of("status", "UP")).build();
}
// ========== Authenticated ==========
@GET
@Path("/me")
@Authenticated
public Response getCurrentUser() {
return Response.ok(Map.of(
"username", jwt.getClaim("preferred_username"),
"email", jwt.getClaim("email"),
"roles", securityIdentity.getRoles(),
"token_id", jwt.getTokenID()
)).build();
}
// ========== Role-based ==========
@GET
@Path("/users")
@RolesAllowed({"USER", "ADMIN"})
public Response getUsers() {
return Response.ok(Map.of(
"message", "User list - USER and ADMIN roles"
)).build();
}
@POST
@Path("/users")
@RolesAllowed("ADMIN")
public Response createUser(Map<String, String> user) {
return Response.ok(Map.of(
"message", "User created",
"username", user.getOrDefault("username", "unknown")
)).build();
}
@GET
@Path("/admin/dashboard")
@RolesAllowed("ADMIN")
public Response adminDashboard() {
return Response.ok(Map.of(
"message", "Admin Dashboard - ADMIN only",
"identity", securityIdentity.getPrincipal().getName()
)).build();
}
}
HTMLTAG_223__HTMLTAG_224___3.4 多租用戶 OIDC 設定___HTMLTAG_225__HTMLTAG_226
Quarkus 支援需要連接多個 Keycloak 領域的 SaaS 系統的多租戶 OIDC:
# application.properties - Multi-tenant setup
# Default tenant
quarkus.oidc.auth-server-url=http://localhost:8080/realms/default-realm
quarkus.oidc.client-id=default-client
quarkus.oidc.application-type=service
# Tenant A
quarkus.oidc.tenant-a.auth-server-url=http://localhost:8080/realms/tenant-a
quarkus.oidc.tenant-a.client-id=tenant-a-client
quarkus.oidc.tenant-a.credentials.secret=tenant-a-secret
quarkus.oidc.tenant-a.application-type=service
# Tenant B
quarkus.oidc.tenant-b.auth-server-url=http://localhost:8080/realms/tenant-b
quarkus.oidc.tenant-b.client-id=tenant-b-client
quarkus.oidc.tenant-b.credentials.secret=tenant-b-secret
quarkus.oidc.tenant-b.application-type=service
package com.example.config;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TenantResolver;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CustomTenantResolver implements TenantResolver {
@Override
public String resolve(RoutingContext context) {
// Resolve tenant từ request path
String path = context.request().path();
if (path.startsWith("/api/tenant-a")) {
return "tenant-a";
}
if (path.startsWith("/api/tenant-b")) {
return "tenant-b";
}
// Hoặc resolve từ header
String tenantHeader = context.request().getHeader("X-Tenant-ID");
if (tenantHeader != null) {
return tenantHeader;
}
// Default tenant
return null;
}
}
HTMLTAG_229__HTMLTAG_230___3.5 Keycloak 授權策略執行者___HTMLTAG_231__HTMLTAG_232
使用 quarkus-keycloak-authorization 強制執行 Keycloak 授權服務策略:
# application.properties
quarkus.keycloak.policy-enforcer.enable=true
quarkus.keycloak.policy-enforcer.enforcement-mode=ENFORCING
# Policy paths
quarkus.keycloak.policy-enforcer.paths.users.path=/api/users/*
quarkus.keycloak.policy-enforcer.paths.users.enforcement-mode=ENFORCING
quarkus.keycloak.policy-enforcer.paths.admin.path=/api/admin/*
quarkus.keycloak.policy-enforcer.paths.admin.enforcement-mode=ENFORCING
quarkus.keycloak.policy-enforcer.paths.public.path=/api/public/*
quarkus.keycloak.policy-enforcer.paths.public.enforcement-mode=DISABLED
HTMLTAG_237__HTMLTAG_238___4。使用測試容器進行測試___HTMLTAG_239__HTMLTAG_240
HTMLTAG_241__HTMLTAG_242___4.1 Spring Boot + Testcontainers Keycloak___HTMLTAG_243__HTMLTAG_244
使用 testcontainers-keycloak 在整合測試中執行真正的 Keycloak:
package com.example;
import com.github.dasniko.testcontainers.keycloak.KeycloakContainer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class KeycloakIntegrationTest {
@Container
static KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:25.0")
.withRealmImportFile("test-realm.json");
@Autowired
MockMvc mockMvc;
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri",
() -> keycloak.getAuthServerUrl() + "/realms/test-realm");
registry.add("spring.security.oauth2.resourceserver.jwt.jwk-set-uri",
() -> keycloak.getAuthServerUrl()
+ "/realms/test-realm/protocol/openid-connect/certs");
}
static String adminToken;
static String userToken;
@BeforeAll
static void obtainTokens() {
// Lấy admin token
adminToken = getAccessToken("admin-user", "admin-pass");
// Lấy user token
userToken = getAccessToken("regular-user", "user-pass");
}
static String getAccessToken(String username, String password) {
// Sử dụng Keycloak Admin Client hoặc HTTP request
// để lấy token từ Keycloak container
String tokenEndpoint = keycloak.getAuthServerUrl()
+ "/realms/test-realm/protocol/openid-connect/token";
// HTTP POST to token endpoint
// grant_type=password&client_id=test-client
// &username=...&password=...
// Return access_token from response
// (Implementation chi tiết sử dụng RestTemplate hoặc WebClient)
return ""; // Placeholder
}
@Test
void publicEndpoint_shouldReturnOk() throws Exception {
mockMvc.perform(get("/api/public/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"));
}
@Test
void protectedEndpoint_withoutToken_shouldReturn401() throws Exception {
mockMvc.perform(get("/api/me"))
.andExpect(status().isUnauthorized());
}
@Test
void protectedEndpoint_withValidToken_shouldReturnOk() throws Exception {
mockMvc.perform(get("/api/me")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + userToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").exists());
}
@Test
void adminEndpoint_withUserToken_shouldReturn403() throws Exception {
mockMvc.perform(get("/api/admin/dashboard")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + userToken))
.andExpect(status().isForbidden());
}
@Test
void adminEndpoint_withAdminToken_shouldReturnOk() throws Exception {
mockMvc.perform(get("/api/admin/dashboard")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").exists());
}
}
HTMLTAG_249__HTMLTAG_250___4.2 測驗領域 JSON___HTMLTAG_251__HTMLTAG_252
建立檔案 src/test/resources/test-realm.json 匯入用於測試的領域:
{
"realm": "test-realm",
"enabled": true,
"clients": [
{
"clientId": "test-client",
"enabled": true,
"publicClient": true,
"directAccessGrantsEnabled": true,
"redirectUris": ["*"]
}
],
"roles": {
"realm": [
{ "name": "ADMIN", "composite": false },
{ "name": "USER", "composite": false }
]
},
"users": [
{
"username": "admin-user",
"enabled": true,
"credentials": [
{ "type": "password", "value": "admin-pass", "temporary": false }
],
"realmRoles": ["ADMIN", "USER"]
},
{
"username": "regular-user",
"enabled": true,
"credentials": [
{ "type": "password", "value": "user-pass", "temporary": false }
],
"realmRoles": ["USER"]
}
]
}
HTMLTAG_257__HTMLTAG_258___4.3 使用模擬 JWT 進行單元測試___HTMLTAG_259__HTMLTAG_260
對於沒有真正 Keycloak 的單元測試,請使用 @WithMockUser 或自訂 JWT:
預編碼_20
HTMLTAG_265__HTMLTAG_266___5。最佳實務與故障排除___HTMLTAG_267__HTMLTAG_268
HTMLTAG_269__HTMLTAG_270___5.1 最佳實務___HTMLTAG_271__HTMLTAG_272
___HTMLTAG_275__HTMLTAG_276___#___HTMLTAG_277__HTMLTAG_278___練習___HTMLTAG_279__HTMLTAG_280___說明___HTMLTAG_281__HTMLTAG_282___
___HTMLTAG_285__HTMLTAG_286___1___HTMLTAG_287__HTMLTAG_288___使用領域角色___HTMLTAG_289__HTMLTAG_290___優先權realm_access.rolesTAG_290___優先權realm_access。
___HTMLTAG_295__HTMLTAG_296___2___HTMLTAG_297__HTMLTAG_298___無狀態會話___HTMLTAG_299__HTMLTAG_300___一律使用 SessionCreationPolicy.STATELESSHTMLTAP. API___HTMLTAG_303__HTMLTAG_304___
___HTMLTAG_305__HTMLTAG_306___3___HTMLTAG_307__HTMLTAG_308___對 API 停用 CSRF___HTMLTAG_309__HTMLTAG_310___使用 JWT 不記名權杖時不需要 CSRF____HTMLTAG_311110G_31112G_3112
___HTMLTAG_313__HTMLTAG_314___4___HTMLTAG_315__HTMLTAG_316___Token驗證快取___HTMLTAG_317__HTMLTAG_318___Spring Security自緩存JWK集,無需為每個請求調用
___HTMLTAG_321__HTMLTAG_322___5___HTMLTAG_323__HTMLTAG_324___基於聲明的授權___HTMLTAG_325__HTMLTAG_326___使用 @PreAuthorize___進行複雜的邏輯雜項___HTMLTAG_329__HTMLTAG_330___
___HTMLTAG_331__HTMLTAG_332___6___HTMLTAG_333__HTMLTAG_334___錯誤處理___HTMLTAG_335__HTMLTAG_336___自訂AuthenticationEntryPointML_MLTAG_337___AuthenticationEntryPointML____338____3030____]
___HTMLTAG_341__HTMLTAG_342___7___HTMLTAG_343__HTMLTAG_344___測試覆蓋率___HTMLTAG_345__HTMLTAG_346___單元測試(模擬 JWT)和整合測試(測試容器)的組合___HTMLTAG_3478___MLTAG_3478___
HTMLTAG_351__HTMLTAG_352___5.2 常見問題疑慮___HTMLTAG_353__HTMLTAG_354
Lỗi: "An error occurred while attempting to decode the Jwt"
→ Kiểm tra issuer-uri có đúng realm URL không
→ Đảm bảo Keycloak server đang chạy và accessible
Lỗi: "Jwt expired"
→ Token đã hết hạn, client cần refresh token
→ Kiểm tra Access Token Lifespan trong Keycloak Realm Settings
Lỗi: "Access Denied" dù đúng role
→ Kiểm tra role name trong JWT có match với @PreAuthorize không
→ Debug: log SecurityContext.getAuthentication().getAuthorities()
→ Kiểm tra KeycloakRoleConverter có prefix "ROLE_" đúng không
Lỗi: "CORS error" khi gọi từ frontend
→ Kiểm tra CorsConfiguration có include frontend origin không
→ Đảm bảo Authorization header được allow
HTMLTAG_355__HTMLTAG_356___5.3 捲曲測試指令___HTMLTAG_357__HTMLTAG_358
# 1. Lấy Access Token từ Keycloak
ACCESS_TOKEN=$(curl -s -X POST \
"http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=my-client" \
-d "username=admin" \
-d "password=admin" \
| jq -r '.access_token')
echo $ACCESS_TOKEN
# 2. Decode JWT (kiểm tra claims)
echo $ACCESS_TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
# 3. Gọi public endpoint
curl -s http://localhost:8081/api/public/health | jq .
# 4. Gọi protected endpoint với token
curl -s http://localhost:8081/api/me \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# 5. Gọi admin endpoint
curl -s http://localhost:8081/api/admin/dashboard \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# 6. Gọi endpoint không có token (expect 401)
curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/api/me
HTMLTAG_359__HTMLTAG_360___6。摘要___HTMLTAG_361__HTMLTAG_362
___HTMLTAG_365__HTMLTAG_366___標準____HTMLTAG_367__HTMLTAG_368___Spring Boot 3___HTMLTAG_369__HTMLTAG_370___Quarkus___HTMLTAG_371__HTMLTAG_372___
___HTMLTAG_375__HTMLTAG_376___函式庫___HTMLTAG_377__HTMLTAG_378__HTMLTAG_379___spring-boot-starter-oauth2-resource-ser v呃___HTMLTAG_380__HTMLTAG_381__HTMLTAG_382__HTMLTAG_383___quarkus-oidc___HTMLTAG_384__HTMLTAG_385__HTMLTAG_386___
___HTMLTAG_387__HTMLTAG_388___角色映射____HTMLTAG_389__HTMLTAG_390___自訂JwtAuthenticationConverter____HTMLTAG_392__HTMLTAG_393__HTMLTAG_394___設定roles.role-claim-path___HTMLTAG_396__HTMLTAGML_3978HT
___HTMLTAG_399__HTMLTAG_400___授權___HTMLTAG_401__HTMLTAG_402__HTMLTAG_403___@預先授權, hasRole()___HTMLTAG_406__HTMLTAG_407__HTMLTAG_408__HTMLTAG_409___@允許的角色、@已驗證___HTML___MLTAG_410___、@已驗證___HTML___MLTA4114141414141_____
___HTMLTAG_415__HTMLTAG_416___多重租戶___HTMLTAG_417__HTMLTAG_418___自訂實作___HTMLTAG_419__HTMLTAG_420___內建租戶解析器___HTMLTAG_42221TAGMLHTMLTAG421____422___MLTAG_42221421_4224_ML
___HTMLTAG_425__HTMLTAG_426___策略执行器____HTMLTAG_427__HTMLTAG_428___手册___HTMLTAG_429__HTMLTAG_430__HTMLTAG_431___quarkus-keycloak-authorization___HTMLTAG_432__HTMLTAG_433__HTMLTAG_434___
___HTMLTAG_435__HTMLTAG_436___測試____HTMLTAG_437__HTMLTAG_438___測試容器+模擬JWT___HTMLTAG_439__HTMLTAG_440__HTMLTAG_441___qu__us-test-keycloak-server___MLGMLGML
___HTMLTAG_445__HTMLTAG_446___啟動時間___HTMLTAG_447__HTMLTAG_448___~2-5s___HTMLTAG_449__HTMLTAG_450___~0.5-1s(原生~0.01s)~0.5-12122_____
~0.5-1s(原生~0.01s)
在下一篇文章中,我們將學習如何將 Keycloak 與前端框架(React、Angular)和 Node.js 後端整合。