keycloak basic
Tung Bui Thanh (TungDaDev)14 mins read

# keycloak
Nếu bạn đang làm với hệ thống phân tán hay Cloud Native, chắc bạn cũng thấy Identity and Access Management (IAM) nằm ngay giữa mọi luồng request. Nó đâu chỉ là chỗ để đăng nhập, nó quyết định luôn chuyện bảo mật, hiệu năng và khả năng mở rộng (scalability).
Keycloak là một Identity Provider (IdP) mã nguồn mở khá mạnh. Chọn dùng nó ở quy mô lớn là một quyết định kiến trúc, đâu đơn giản là chọn một phần mềm.
Bài này gom lại các kinh nghiệm thực tế của mình, tập trung vào những chỗ cần để ý khi vận hành Keycloak.
# bài toán chi phí
Trước khi đụng vào code, cần hiểu vì sao nhiều công ty thích tự làm (Build/Self-host Keycloak) thay vì mua (Buy/SaaS như Okta, Auth0).
Khi số người dùng lên tới hàng triệu, chi phí license của các nền tảng SaaS có thể tăng vọt (variable cost), vì giá thường tính theo số monthly active user (MAU). Với lượng người dùng cỡ vài trăm nghìn trở lên, hoá đơn hằng năm dễ lên mức sáu chữ số đô la tuỳ gói và tính năng (MFA, enterprise SSO...), khiến các tổ chức lớn bị động về tài chính. Con số cụ thể thì nên lấy báo giá thật, vì bảng giá các hãng thay đổi khá thường xuyên.
Keycloak có lợi thế ở chỗ chi phí cố định (fixed TCO):
- Fixed cost: keycloak là mã nguồn mở nên chỉ phải trả chi phí vận hành (hạ tầng, nhân sự, bảo trì cluster). Mấy khoản này cố định và dự đoán được.
- Toàn quyền kiểm soát: keycloak cho mình nắm cả stack. Có thể dùng
Service Provider Interfaces (SPIs)để can thiệp sâu vào luồng xác thực, tích hợp vớiuser directory legacy (LDAP/AD)hay các quy trình bảo mật riêng của từng đơn vị.
Có điều, Keycloak ở quy mô lớn là cả một "Full-Scale Engineering Project". Phải đầu tư kỹ thuật kha khá vào HA/Clustering, tối ưu database và infinispan caching. Đây là khoản chi phí kỹ thuật rất cần tính trước.
# OIDC, OAuth2 & JWT performance
Keycloak đóng vai Authorization Server, tuân thủ chặt hai giao thức nền tảng: OAuth 2.0 (Ủy quyền) và OpenID Connect (OIDC) (Xác thực). Hiểu chúng chạy thế nào thì mới bảo vệ được hệ thống microservices cho đúng.
# OAuth 2.0 & OIDC
Keycloak có ba loại token chính, mỗi loại một việc:
- ID Token (OIDC): dùng cho xác thực
(Authentication). Nó chứa thông tin người dùng(user identity). Client dùng token này để biết ai đang đăng nhập, không dùng để gọi API. - Access Token (OAuth 2.0): dùng cho uỷ quyền
(Authorization). Đây làBearer Tokengửi tới Resource Server (API) để chứng minh mình có quyền truy cập tài nguyên. Token này chứaRolesvàScopes. - Refresh Token: dùng để lấy
Access Tokenmới mà người dùng không phải đăng nhập lại. Token này phải cất giữ cực kỳ cẩn thận.
# PKCE và Confidential Clients: bảo vệ luồng token
Với ứng dụng Web/Mobile (Public Clients), Authorization Code Flow là cách chuẩn. Keycloak dùng PKCE (Proof Key for Code Exchange) để bảo vệ luồng này.
PKCE: trước khi chuyển hướng sang trang đăng nhập, client sinh một chuỗi ngẫu nhiên code_verifier, chỉ gửi đi bản băm của nó (code_challenge, SHA-256). Lúc đổi Authorization Code lấy token, client phải trình ra code_verifier gốc. Kẻ tấn công có cướp được Authorization Code trên đường redirect cũng không đổi ra token được, vì không có chuỗi gốc. Đây là lớp bảo vệ không thể thiếu cho các client không giữ được bí mật (client_secret), và theo OAuth 2.0 Security BCP (RFC 9700) thì nên bật cho cả confidential client. Trong Keycloak, PKCE bật ở phần Advanced của client (chọn method S256) để bắt buộc client phải dùng.
# tối ưu JWT validation
Để microservices chạy stateless và nhanh, mình phải tự xác thực JWT ngay tại service (Local JWT Validation) bằng cách tải JWKs (public keys) của Keycloak về, thay vì gọi API Introspection đồng bộ vừa chậm vừa tốn.
Luật bắt buộc cho Resource Server (API): phải kiểm tra kỹ các claim sau theo RFC 7519 :
iss (Issuer): có đúng là keycloak của mình phát hành không?aud (Audience): claim quan trọng nhất, cho biết token này phát cho ai. NếuResource Serverbỏ qua bước kiểm tra aud là bạn đang để hở một lỗ bảo mật nghiêm trọng: attacker có thể lấyAccess Tokencấp choClient Ađể gọi API củaMicroservice B(dùng chéo token).exp (Expiration Time): token hết hạn chưa?
Với Spring Boot, khai báo issuer-uri thì Spring Security đã kiểm tra chữ ký, iss và exp/nbf sẵn. Riêng aud thì không tự kiểm, phải khai báo thêm spring.security.oauth2.resourceserver.jwt.audiences (Boot 3.0+). Phía Keycloak, access token mặc định chỉ có aud là account (nếu có), muốn token mang đúng tên service thì thêm mapper kiểu "Audience" vào client scope.
# soft revocation: mẹo hiệu năng với NBF
Cái khó muôn thuở của JWT stateless là: làm sao thu hồi ngay một Access Token đã phát ra (ví dụ admin buộc người dùng logout) khi nó chưa tới giờ hết hạn (exp)?
Keycloak có Not-Before Policy (NBF).
- Admin đặt mốc not-before cho
realmhoặcclient(trong Admin Console: Sessions → Revocation → "Set to now"), rồi bấm "Push" để Keycloak báo mốc mới tới các client có cấu hình Admin URL. - Client nhận được mốc này sẽ so với thời điểm phát hành
(iat)của token.iatsớm hơn mốc not-before thì token bị từ chối.
Cần nói rõ giới hạn của cơ chế này. Mốc NBF không nằm trong token, nên Resource Server chỉ biết tới nó khi có code nhận "push" từ Keycloak; trước đây việc đó do các Keycloak Java adapter làm, mà các adapter này đã bị deprecated. Spring Security Resource Server thì không biết gì về NBF của Keycloak. Đăng xuất một phiên thông thường cũng không đổi NBF, nó chỉ làm refresh token của phiên đó hết hiệu lực.
Vì vậy cách thực tế với JWT stateless là: để access token sống ngắn (Keycloak mặc định 5 phút), thu hồi bằng cách huỷ phiên/refresh token, chấp nhận token cũ còn dùng được tới khi hết hạn. API nào thật sự cần thu hồi tức thì (chuyển tiền, đổi quyền admin) thì mới gọi Token Introspection cho riêng các API đó, chứ không phải cho mọi request.
# HA, scalability và sticky sessions
Dựng Keycloak Cluster cho đạt High Availability (HA) là một bài toán khá đau đầu.
# sticky sessions
Dù Sticky Sessions (Session Affinity) bị coi là anti-pattern trong microservices thuần, với Keycloak Cluster thì từ lâu nó là cấu hình được khuyến nghị. Không bật thì Keycloak vẫn chạy đúng, chỉ chậm và tốn hơn. Lưu ý là khuyến nghị này đã đổi theo phiên bản (xem cuối mục).
Lý do: keycloak dùng Infinispan làm cache phân tán (distributed cache) để giữ trạng thái phiên. Mỗi phiên chỉ được lưu trên một vài node "chủ" (owner), không phải node nào cũng có. Nếu Load Balancer cứ đẩy request của cùng một phiên sang các node khác nhau, node nhận request thường phải hỏi qua mạng tới node đang giữ phiên. Vừa tăng độ trễ, vừa tốn băng thông vô ích.
- Cách xử lý: cấu hình
Load BalancerdùngSticky Sessionsdựa trên cookieAUTH_SESSION_ID(Keycloak gắn sẵn thông tin node vào giá trị cookie này) --> dùng tài nguyên hợp lý hơn và giữ độ trễ thấp.
Từ Keycloak 26.6, tài liệu reverse proxy ghi rõ sticky session không còn mang lại lợi ích đáng kể so với công sức cấu hình proxy: phiên được tạo ở node nhận request đầu tiên, chuyển node thì chi phí thấp, và phần lớn tương tác (backchannel, refresh token) vốn không dùng cookie nên đằng nào cũng được chia đều cho các node. Nên nếu chạy bản mới, có thể bỏ qua phần cấu hình này.
# infinispan cache
Để có HA và chịu lỗi tốt, cần chỉnh cache phân tán của Keycloak:
- Owner count: với
Distributed Cache, nên đặtOwner Count(số bản sao dữ liệu cache) là 2 hoặc 3 node. Keycloak mặc định là 2. Như vậy một node chết thì dữ liệu vẫn còn. Từ Keycloak 26, phiên người dùng mặc định được lưu cả xuống database (persistent user sessions), nên restart cả cluster cũng không mất phiên, đổi lại database phải gánh thêm tải ghi. - Database HA: bắt buộc dùng
Database production-ready (PostgreSQL/MySQL)vớiPrimary-Secondary ReplicationvàAutomatic Failoverđể gánh tải và xử lý sự cố.
# uỷ quyền nâng cao và khả năng mở rộng
# vượt ra ngoài RBAC: PEP, PDP && PBAC
Keycloak Authorization Services có sẵn khung Policy-Based Access Control (PBAC), hợp với các quyết định truy cập phức tạp cần thêm ngữ cảnh (contextual information):
- PEP (Policy Enforcement Point): nằm ở
Resource Server (API)--> chặn request và thực thi quyết định. - PDP (Policy Decision Point): nằm ở Keycloak --> xét các policy dựa trên vai trò, thuộc tính, thời gian, ngữ cảnh để ra quyết định cho hay không cho truy cập.
# UMA 2.0: party-to-party authorization
Keycloak là Authorization Server hỗ trợ chuẩn User-Managed Access (UMA) 2.0. Đây là mô hình uỷ quyền nâng cao, rất cần cho các kịch bản chia sẻ dữ liệu (ví dụ chia sẻ hồ sơ y tế, Open Banking).
- Party-to-Party Authorization: UMA cho chủ sở hữu tài nguyên
(Resource Owner) hoặc (người dùng cuối)tự quản lý và cấp quyền truy cập dữ liệu của mình cho bên thứ ba(Requesting Parties). - Quy trình bất đồng bộ: người dùng có thể đặt và đổi chính sách cấp quyền bất cứ lúc nào. Khi truy cập bị từ chối,
Resource Servertrả vềPermission Ticket(token mờ), rồiClientdùngTicketnày để xinRequesting Party Token (RPT), tức(Access Token cuối cùng), từ Keycloak.
# extensibility: user storage SPI và webAuthn
Khả năng mở rộng qua SPIs là lợi thế TCO lớn nhất của Keycloak.
- User storage SPI: cho Keycloak kết nối và xác thực người dùng từ kho bên ngoài
(LDAP, AD, Custom DB)mà không phải import toàn bộ dữ liệu.- Vấn đề dữ liệu cũ (Stale Data): khi dùng SPI, dữ liệu người dùng được cache trong
Infinispan. Nguồn bên ngoài đổi dữ liệu thì mình phải có cơ chế chủ động xoá cache(cache invalidation), không thì Keycloak sẽ trả ra dữ liệu cũ.
- Vấn đề dữ liệu cũ (Stale Data): khi dùng SPI, dữ liệu người dùng được cache trong
- WebAuthn/FIDO2: keycloak đang đẩy mạnh hỗ trợ chuẩn WebAuthn (thường đi cùng FIDO2). WebAuthn đăng nhập bằng khoá công khai, bỏ hẳn mật khẩu, và chặn phishing rất hiệu quả.
# tích hợp keycloak & spring boot
Với dân Java, thường sẽ dựng Spring Boot microservice theo hai vai: SSO Client (Web App) và Stateless Resource Server (API).
# pattern 1: SSO client (web application)
Ở mô hình này Spring Boot là một Confidential Client, chạy Authorization Code Flow và quản lý phiên HTTP (Session-based).
Dependencies: spring-boot-starter-oauth2-client và spring-boot-starter-security.
Cấu hình application.yml (SSO Client)
server:
port: 8082
spring:
application:
name: keycloak-sso-client
security:
oauth2:
client:
registration:
# Đăng ký Client ID Keycloak
keycloak:
client-id: food-ordering-client
client-secret: your-client-secret # Confidential Client
scope: openid,profile,email # Yêu cầu Scope OIDC
redirect-uri: http://localhost:8082/login/oauth2/code/keycloak
provider:
# Cấu hình Keycloak Provider
keycloak:
# Issuer URI là bắt buộc cho Discovery
issuer-uri: http://localhost:8088/realms/food-ordering-realmSecurity Configuration (SecurityConfig.java - SSO Client). Dùng .oauth2Login() để bật luồng SSO và xử lý logout.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
ClientRegistrationRepository clientRegistrationRepository) throws Exception {
// Gọi end_session_endpoint của Keycloak để đăng xuất cả phiên SSO
OidcClientInitiatedLogoutSuccessHandler oidcLogout =
new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
oidcLogout.setPostLogoutRedirectUri("{baseUrl}/");
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/").permitAll()
.requestMatchers("/menu").authenticated() // Yêu cầu xác thực
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2 // Kích hoạt OAuth2 Login (SSO)
.loginPage("/oauth2/authorization/keycloak")
.defaultSuccessUrl("/menu", true)
)
.logout(logout -> logout // Single Sign-Out qua Keycloak
.logoutSuccessHandler(oidcLogout)
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
);
return http.build();
}
}Chỗ logout hay bị làm thiếu. Nếu chỉ logoutSuccessUrl("/") thì app xoá session của chính nó, nhưng phiên SSO trên Keycloak vẫn còn: người dùng bấm "Đăng nhập" lại là vào thẳng, không phải nhập mật khẩu. OidcClientInitiatedLogoutSuccessHandler chuyển hướng trình duyệt tới end_session_endpoint của Keycloak (kèm id_token_hint) để đăng xuất luôn phiên SSO, rồi quay về {baseUrl}/. URL này phải nằm trong danh sách "Valid post logout redirect URIs" của client trên Keycloak.
# pattern 2: stateless resource server (API protection)
Đây là mô hình chuẩn cho microservices: Stateless, bảo vệ bằng JWT Validation (Bearer Token).
Dependencies: spring-boot-starter-oauth2-resource-server.
Cấu hình application.yml (Resource Server)
Cấu hình rất gọn. Resource Server chỉ cần biết issuer-uri, Spring Security sẽ tự tải JWKS (public keys) từ Keycloak và cache lại. Một bẫy hay gặp khi chạy Docker: issuer-uri phải khớp từng ký tự với claim iss trong token. Trình duyệt lấy token qua localhost:8088 còn service gọi Keycloak qua keycloak:8080 thì iss lệch và mọi request bị 401; cách xử lý là đặt hostname cố định cho Keycloak (KC_HOSTNAME).
server:
port: 8083
spring:
security:
oauth2:
resourceserver:
jwt:
# Chỉ cần Issuer URI. Spring Security tự động tìm JWKS endpoint
issuer-uri: http://localhost:8088/realms/food-ordering-realmSecurity Configuration (ResourceServerConfig.java - API). Phần quan trọng nhất là Custom JWT Converter để map Roles/Authorities của Keycloak sang dạng mà Spring Security hiểu.
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
// Tắt CSRF/Session vì đây là stateless REST API
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Kiểm tra Role đã ánh xạ
.anyRequest().authenticated()
)
// Kích hoạt Resource Server và sử dụng Custom Converter
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(keycloakJwtConverter()))
);
return http.build();
}
// Custom JWT Converter: Ánh xạ claims Keycloak sang GrantedAuthority
@Bean
public JwtAuthenticationConverter keycloakJwtConverter() {
// Giữ hành vi mặc định: claim 'scope' → authority 'SCOPE_xxx'
JwtGrantedAuthoritiesConverter scopesConverter = new JwtGrantedAuthoritiesConverter();
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> authorities = new ArrayList<>(scopesConverter.convert(jwt));
// Keycloak đặt realm role trong claim lồng nhau: realm_access.roles
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess != null && realmAccess.get("roles") instanceof Collection<?> roles) {
// Thêm prefix 'ROLE_' để dùng được hasRole("ADMIN")
roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
}
// Client role nằm ở resource_access.<client-id>.roles, đọc tương tự nếu cần
return authorities;
});
// Lấy username thay vì 'sub' (UUID) làm tên principal
converter.setPrincipalClaimName("preferred_username");
return converter;
}
}Bản cũ của đoạn này chỉ map claim scope thành SCOPE_... và để TODO phần role, nghĩa là hasRole("ADMIN") ở trên sẽ không bao giờ khớp và /api/admin/** luôn trả 403. Spring Security không hiểu cấu trúc realm_access.roles của Keycloak, nên phần đọc role phải tự viết như trên. Tên role so khớp phân biệt hoa thường: role trên Keycloak tên admin thì phải là hasRole("admin").
# lời kết
Keycloak không phải loại mua về, tích hợp vào là xong. Nó là một nền tảng mình phải tự dựng và nuôi. Bù lại, ở quy mô lớn thì về độ kiểm soát lẫn chi phí, nó là lựa chọn rất đáng cân nhắc.
Trước khi tích hợp và vận hành, team nên đọc kỹ phần Server Administration và High Availability trong tài liệu chính thức. Muốn đi sâu hơn về cấu hình Keycloak thì xem thêm bài Keycloak.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.