본문으로 건너뛰기

개발자 가이드

Keycloak Extension SPI 유지보수 및 기능 추가를 위한 구조 안내서


1. 프로젝트 개요

기본 정보

항목
Group IDcom.keycloak.extensions
Artifact IDkeycloak-extensions-spi
Version1.0.0-SNAPSHOT
Java17
Keycloak26.5.2
PackagingJAR (단일 모듈)
테마 이름keycloak.ext (login, email 타입)

의존성

라이브러리스코프용도
keycloak-server-spiprovidedSPI 인터페이스
keycloak-server-spi-privateprovided내부 SPI 인터페이스
keycloak-servicesprovidedKeycloak 서비스 레이어
keycloak-themesprovided테마 리소스
keycloak-model-infinispanprovided분산 캐시 모델
keycloak-crypto-defaultcompile암호화 기본 구현
lombokprovided보일러플레이트 제거

등록된 SPI Provider

SPI 등록은 src/main/resources/META-INF/services/ 하위 파일로 관리된다.

SPI 인터페이스등록된 Factory
AuthenticatorFactoryOtpAuthenticatorFactory
ConditionalOtpAuthenticatorFactory
PasswordResetAuthenticatorFactory
TermsConsentAuthenticatorFactory
DormantAccountAuthenticatorFactory
RequiredActionFactoryTermsConsentFactory
AccountReactivationFactory
SocialIdentityProviderFactoryKakaoIdentityProviderFactory
NaverIdentityProviderFactory
IdentityProviderFactoryInicisIdentityProviderFactory
EventListenerProviderFactoryLastLoginEventListenerFactory
RealmResourceProviderFactoryUsernameFindResourceProviderFactory
MyResourceProviderFactory
RegistrationVerifyResourceProviderFactory
UserProfileResourceFactory
ProfilePageResourceFactory
UserStorageProviderFactoryUserProviderFactory
DormantAccountScheduledTask

2. 개발 환경 설정

빌드

# 전체 빌드 (Tailwind CSS 포함)
mvn clean package -Dmaven.test.skip=true

# CSS 변경만 있을 때
npm run build # 1회 빌드
npm run dev # watch 모드

Maven 빌드 시 frontend-maven-plugin이 Node v20.11.0을 자동 설치하고 npm run build를 실행하여 output.css를 생성한다.

Docker Compose

모든 docker compose 명령은 docker/ 디렉토리에서 실행한다.

서비스포트용도
keycloak8080Keycloak 서버
keycloak8000Remote Debug (JDWP)
postgres_db5432PostgreSQL 16
mailhog8025이메일 수신 Web UI
mailhog1025SMTP 수신
# 시작
cd docker && docker compose up -d

# 로그 확인
docker compose logs -f keycloak

# 코드 변경 후 재시작
cd .. && mvn clean package -Dmaven.test.skip=true
cd docker && docker compose restart keycloak

# 정지
docker compose down

Admin 콘솔 접속

  • URL: http://localhost:8080/admin
  • 계정: admin / eX4mP13p455w0Rd

MailHog (이메일 확인)

  • URL: http://localhost:8025

Hot Reload

compose.yaml에서 KC_SPI_DEPLOYMENTS_SCANNER_ENABLED: true로 변경하면 target/ 디렉토리를 10초 간격으로 스캔하여 JAR 변경을 자동 감지한다. (기본값 false — 운영 안정성을 위해 개발 시에만 활성화)

KC_SPI_DEPLOYMENTS_SCANNER_ENABLED: true
KC_SPI_DEPLOYMENTS_SCANNER_INTERVAL: 10

Remote Debug

compose.yamlJAVA_OPTS 라인 주석을 해제하면 포트 8000으로 디버그 연결이 가능하다.

JAVA_OPTS: -Xms1024m -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:8000

IDE에서 Remote JVM Debug 설정 → host: localhost, port: 8000

새 Realm HTTP 접속

Docker Desktop 4.67.0 이상에서는 Mac host → container 연결의 실제 TCP source IP가 172.64.66.1 (Docker Desktop 내부 포트 포워딩 프록시 IP)로 고정된다. Keycloak realm의 sslRequired 기본값은 external이므로, 새 realm을 생성하면 localhost:8080으로 접근해도 ssl_required 오류가 발생한다.

compose.yamlkeycloak-cli 컨테이너가 시작 시 자동으로 처리한다.

keycloak-cli:
command:
- sh
- -c
- |
kcadm.sh update realms/master -s sslRequired=none
kcadm.sh update realms/cnap -s sslRequired=none # realm 추가 시 여기에 추가

새 realm을 추가할 경우 위 명령에 해당 realm의 sslRequired=none 줄을 추가한다.

수동으로 설정하는 방법은 두 가지다.

Admin Console에서 변경 (master realm은 이미 none이므로 로그인 가능):

  1. http://localhost:8080/admin 접속
  2. 좌측 상단 realm 드롭다운 → 대상 realm 선택
  3. Realm settings → General → Require SSL → None 선택 → Save

keycloak-cli 컨테이너로 변경 (Admin Console 접근이 불가한 경우):

compose.yamlkeycloak-cli 명령에 해당 realm을 추가한 뒤 실행한다.

docker compose run --rm keycloak-cli

3. SPI 아키텍처 개요

Factory → Provider 패턴

Keycloak SPI는 FactoryProvider 인스턴스를 생성하는 패턴으로 동작한다.

META-INF/services/{SPI 인터페이스} ← Keycloak이 Factory를 탐색

XxxFactory.create() ← 요청마다 Provider 인스턴스 생성

XxxProvider (실제 로직 구현)
  • Factory: getId(), create(), getConfigProperties() 구현 — Keycloak Admin에서 설정 UI 제공
  • Provider: 실제 인증/처리 로직 구현, 요청 scope로 생성됨

패키지 구조

src/main/java/com/keycloak/
├── account/
│ ├── dormancy/ # 계정 휴면 (4.7)
│ │ ├── action/ # AccountReactivationRequiredAction
│ │ ├── authenticator/ # DormantAccountAuthenticator
│ │ ├── listener/ # LastLoginEventListener
│ │ └── scheduler/ # DormantAccountScheduledTask, Config, Service
│ ├── profile/ # 프로파일 관리 REST (5)
│ └── registration/ # 회원가입 (4.4)
│ ├── validator/ # RegistrationUserValidation
│ └── verify/ # RegistrationVerifyResource
├── api/ # MyResourceProvider (6)
├── authentication/
│ ├── idp/ # 외부 로그인 IDP (4.6)
│ │ ├── core/ # AbstractSocialIdentityProvider
│ │ ├── kakao/
│ │ ├── naver/
│ │ └── inicis/
│ ├── otp/ # OTP 인증 (4.2)
│ ├── recovery/ # 아이디·비밀번호 찾기 (4.5)
│ │ ├── password/ # PasswordResetAuthenticator
│ │ └── username/ # UsernameFindResource
│ └── terms/ # 이용약관 동의 (4.3)
├── common/
│ ├── otp/ # OTP 코드 생성·검증 서비스
│ ├── sms/ # SMS 발송 서비스 (연동 포인트)
│ └── util/ # 공통 유틸 (masking, phone, validation, codes)
└── userstorage/ # User Federation (4.8)
├── adapter/ # UserAdapter
└── client/ # 외부 저장소 HTTP 클라이언트

SPI 등록 방식

src/main/resources/META-INF/services/ 디렉토리에 SPI 인터페이스명으로 파일을 생성하고, 구현체의 완전한 클래스명을 한 줄씩 기재한다.

META-INF/services/
├── org.keycloak.authentication.AuthenticatorFactory ← Authenticator 등록
├── org.keycloak.authentication.RequiredActionFactory ← Required Action 등록
├── org.keycloak.broker.social.SocialIdentityProviderFactory ← 소셜 IDP 등록
├── org.keycloak.broker.provider.IdentityProviderFactory ← 일반 IDP 등록
├── org.keycloak.events.EventListenerProviderFactory ← 이벤트 리스너 등록
├── org.keycloak.services.resource.RealmResourceProviderFactory ← REST API 등록
├── org.keycloak.storage.UserStorageProviderFactory ← User Federation 등록
└── ...

신규 SPI 추가 시: ① Factory/Provider 클래스 작성 → ② 해당 services 파일에 클래스명 추가 → ③ 빌드 후 재시작

공통 유틸리티

패키지클래스역할
common/otpOtpServiceOTP 코드 생성·저장·검증, TTL 관리
common/smsSmsServiceSMS 발송 외부 연동 (구현체 교체 포인트)
common/utilMaskingUtil개인정보 마스킹
common/utilPhoneUtil휴대폰 번호 정규화
common/utilValidationUtil입력값 검증
common/utilOtpConstants코드 길이(6자리), TTL(300초) 상수 정의

4. 기능 영역별 구조

4.1 인증 플로우 (Authentication Flow)

Keycloak Admin Console → Authentication → Flows 에서 플로우를 구성한다. 각 Authenticator는 Required / Alternative / Conditional / Disabled 중 하나로 설정된다.

로그인 플로우 (Browser OTP)

순서Authenticator역할
1DormantAccountAuthenticator휴면 상태 감지 → Required Action 추가
2Cookie / Kerberos기존 세션 처리 (기본 Keycloak)
3Username Form아이디 입력
4Password Form비밀번호 입력
5OtpAuthenticator 또는 ConditionalOtpAuthenticatorOTP 인증 코드 발송·검증

회원가입 플로우 (Registration Term)

순서Authenticator역할
1TermsConsentAuthenticator이용약관 동의
2Registration (기본)사용자 정보 입력
3RegistrationUserValidation이메일/전화 인증 완료 검증

비밀번호 재설정 플로우 (Reset Credentials OTP)

순서Authenticator역할
1PasswordResetAuthenticator코드 발송·검증·비밀번호 변경

플로우 변경 시 주의사항

  • Authenticator 순서 변경 시 인증 세션 노트(auth session notes) 의존성 확인
  • 새 Authenticator 추가 후 반드시 services 파일 등록 → 빌드 → Keycloak 재시작
  • 기존 플로우를 직접 수정하지 않고 복사 후 수정 권장 (Admin Console → Duplicate)

4.2 OTP 인증

클래스 구조

OtpAuthenticatorFactory (PROVIDER_ID: "otp-authenticator")
ConditionalOtpAuthenticatorFactory (PROVIDER_ID: "conditional-otp-authenticator")
└── OtpAuthenticatorFactory 상속 + 조건부 skip/force 설정 추가
└── OtpAuthenticator (실제 인증 로직)
└── OtpService
├── CacheCodeStorage (코드 저장 - Infinispan)
├── EmailDeliveryService
└── SmsDeliveryService ← 외부 SMS API 연동 포인트

OTP 전달 전략 (Admin Console에서 설정)

전략동작
STRATEGY_CONFIG_ONLYFactory 설정의 delivery method(SMS/EMAIL) 고정 사용
STRATEGY_USER_ATTRIBUTE사용자 속성 otpMethod 값 참조 (SMS/EMAIL/SKIP)
STRATEGY_USER_CHOICE로그인 시 사용자가 직접 선택

ConditionalOtpAuthenticator 조건 설정

설정동작
Skip OTP for Role특정 역할 보유 시 OTP 건너뜀
Force OTP for Role특정 역할 보유 시 OTP 강제
Skip/Force OTP for HTTP Header헤더 패턴 매칭으로 제어
Fallback OTP Handling조건 미충족 시 SKIP 또는 FORCE

코드 길이 / TTL 변경

common/util/OtpConstants.java 에서 상수 수정:

  • 코드 길이: 6자리
  • TTL: 300초 (기본값)

SMS 발송 연동 포인트

common/sms/SmsDeliveryService — SMS API URL, 발신 키, 발신자 번호는 Admin Console → Authentication → OTP Authenticator 설정에서 관리한다. SMS 실패 시 이메일로 자동 폴백된다.


4.3 이용약관 동의

동작 흐름

TermsConsentAuthenticator.authenticate()terms-consent.ftl 렌더링 → action() → 필수 항목 검증 → 선택 항목 저장 → 인증 세션 노트에 완료 마킹

동의 항목 처리

항목폼 필드명구분저장 위치
서비스 이용약관termsAccepted필수인증 세션 노트
만 14세 이상ageConsent필수인증 세션 노트
개인정보 수집(필수)privacyRequired필수인증 세션 노트
개인정보 수집(선택)privacyOptional선택사용자 속성
마케팅 수신 동의marketingConsent선택사용자 속성
마케팅 이메일marketingEmail선택사용자 속성
마케팅 SMSmarketingSMS선택사용자 속성
마케팅 푸시marketingPush선택사용자 속성

항목 추가/제거 포인트

TermsConsentAuthenticator.action() 내 두 영역을 수정:

  1. 필수 항목 검증: if (formData.getFirst("항목명") == null) 블록 추가
  2. 선택 항목 저장: user.setSingleAttribute("속성명", value) 추가

FTL 반영: terms-consent.ftl (theme-resources/templates/)에 체크박스 추가

Required Action 연동

TermsConsentFactory (RequiredActionFactory)로도 등록되어 있어, 기존 사용자에게 약관 재동의를 Required Action으로 부여할 수 있다.


4.4 회원가입

구성 요소

클래스역할
TermsConsentAuthenticator1단계: 이용약관 동의
RegistrationUserValidation2단계: 입력값 검증 (FormAction)
RegistrationVerifyResource사전 이메일/SMS 인증 REST API

RegistrationUserValidation 검증 로직

  • 이메일·휴대폰 번호 중복 확인 (현재 사용자 제외)
  • 인증 세션 노트 verified_email / verified_phone 존재 여부 확인 → 미인증 시 폼 에러 반환
  • 휴대폰 번호 정규화 (normalizePhone()) — 비숫자 제거

인증 코드 REST API (/realms/{realm}/registration-verify/)

엔드포인트설명
POST /send-code이메일 또는 SMS 인증 코드 발송
POST /verify-code인증 코드 확인 → 세션 노트 저장
  • Rate limit: 30분 내 최대 5회 (RateLimitService)
  • 코드 TTL: 300초

OTP 수단 선택 저장

가입 폼의 otpMethod 필드(SMS/EMAIL/SKIP) 값이 RegistrationUserValidation에서 사용자 속성 otpMethod로 저장된다. 이 값은 이후 로그인 시 ConditionalOtpAuthenticatorSTRATEGY_USER_ATTRIBUTE가 참조한다.


4.5 아이디 · 비밀번호 찾기

비밀번호 재설정 (PasswordResetAuthenticator)

단일 Authenticator가 멀티 스텝 플로우를 action 파라미터로 분기 처리한다.

action 파라미터처리 메서드동작
sendCodehandleSendCode()사용자 조회 → 속도 제한 확인 → OtpService로 코드 발송
verifyCodehandleVerifyCode()코드 형식 검증 → OTP 일치 확인
resendCodehandleResendCode()속도 제한 확인 → 재발송
  • 사용자 조회: 아이디·이메일·휴대폰 번호 모두 지원
  • 휴대폰 번호 매칭: +82 국가코드 변환 처리 포함
  • 속도 제한: 30분 내 최대 5회 (RateLimitService)
  • 코드 TTL: VerificationCodeGenerator.CODE_LIFESPAN_SECONDS
  • FTL: theme-resources/templates/login-reset-password-otp.ftl

아이디 찾기 (UsernameFindResource)

REST API 방식으로 구현 (/realms/{realm}/username-find/). 이메일 또는 휴대폰 번호 인증 후 마스킹된 아이디를 반환한다. FTL: theme-resources/templates/username-find.ftl


4.6 외부 로그인 (Identity Provider)

클래스 계층

AbstractSocialIdentityProvider<C> (core/)
├── KakaoIdentityProvider (SocialIdentityProviderFactory 등록)
└── NaverIdentityProvider (SocialIdentityProviderFactory 등록)

AbstractIdentityProviderFactory
└── InicisIdentityProvider (IdentityProviderFactory 등록 — 소셜 아님)

AbstractSocialIdentityProvider 핵심 동작

  • doGetFederatedIdentity(accessToken): Bearer 토큰으로 프로필 API 호출 → 이메일 기반으로 Keycloak 사용자 조회 (이메일이 없으면 오류)
  • updateBrokeredUser(): 이름·이메일·phoneNumber·otpMethod 속성 동기화
  • Inner class SocialEndpoint: OAuth2 콜백 처리 (state 검증 → code 교환 → 사용자 조회)

소셜 IDP 비교

항목카카오네이버
PROVIDER_IDkakaonaver
OpenID ConnectON (profile_nickname, profile_image)OFF
Redirect URI/realms/{realm}/broker/kakao/endpoint/realms/{realm}/broker/naver/endpoint

Inicis IDP (간편인증 목업)

  • PROVIDER_ID: kg-inicis
  • SocialIdentityProviderFactory 미구현 → IdentityProviderFactory로만 등록
  • OAuth2 방식이 아닌 가맹점 자격증명(MID, API Key) 기반
  • 신규 통신사/인증기관 연동 시 이 구조를 참고하여 확장

신규 IDP 추가 방법

  1. AbstractSocialIdentityProvider 상속 → extractUserProfile() 등 구현
  2. Factory 클래스 작성 (PROVIDER_ID, getName(), getConfigProperties())
  3. 소셜이면 META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory에 등록 일반이면 org.keycloak.broker.provider.IdentityProviderFactory에 등록
  4. 필요시 UserAttributeMapper 추가 → org.keycloak.broker.provider.IdentityProviderMapper에 등록
  5. Admin Console → Identity Providers에서 Client ID/Secret·Redirect URI 설정

4.7 계정 휴면 관리

컴포넌트 역할

클래스역할
LastLoginEventListener로그인 성공 이벤트 수신 → lastLoginDate 갱신, 휴면 속성 초기화
DormantAccountScheduledTask스케줄 실행 → 미접속 계정 탐색 → 상태 전이 및 이메일 발송
DormantAccountConfigUser Federation ComponentModel에서 설정값 로딩
DormantAccountAuthenticator로그인 시 dormantStatus 확인 → Required Action 등록
AccountReactivationRequiredAction6자리 토큰 발급·이메일 발송·검증·계정 복원

계정 상태 전이

ACTIVE
└─(미접속 365일)──→ DORMANT ← 사전 안내: 30일 전
└─(추가 미접속 90일)──→ PENDING_DELETE ← 삭제 안내: 30일 전
└─(기간 초과)──→ 영구 삭제

설정값 변경 위치

Admin Console → User Federation → 해당 Federation 클릭 → 설정 탭

설정 키기본값설명
dormantPeriodDays365휴면 전환 기준 일수
warningPeriodDays30휴면 사전 안내 일수
deleteEnabledfalse계정 삭제 기능 활성화
deletePeriodDays90삭제 기준 일수 (휴면 이후)
deletionWarningPeriodDays30삭제 사전 안내 일수

이메일 템플릿 연결

이벤트템플릿
휴면 전환 사전 안내html/account-dormant-notification.ftl
재활성화 코드 발송html/account-reactivation-email.ftl
재활성화 완료 확인html/account-reactivation-confirmation.ftl
삭제 예정 안내html/account-deletion-warning.ftl

재활성화 토큰

AccountReactivationRequiredAction: 6자리 숫자 (%06d, 범위 100000~999999) 유효시간: 1시간 (reactivationTokenExpiry 속성)


4.8 User Storage Federation

구성

클래스역할
UserProviderFactoryPROVIDER_ID: REST — Factory 등록, 설정 검증
UserProviderUserStorageProvider 구현 — 외부 REST API로 사용자 조회/인증
UserAdapterKeycloak UserModel 래핑 — 외부 사용자 데이터를 Keycloak 인터페이스로 제공
HTTP Clientuserstorage/client/ — 외부 저장소 API 호출 (Basic Auth)

Admin Console 설정 항목

항목설명
Base URL외부 사용자 저장소 REST API 기본 URL
UsernameBasic Auth 사용자명
PasswordBasic Auth 비밀번호
Sync Enabled동기화 활성화 여부
Import EnabledKeycloak 로컬 DB에 사용자 임포트 여부

동기화

sync() / syncSince() 는 현재 SynchronizationResult.ignored() 반환 (미구현 상태). 외부 저장소 동기화가 필요한 경우 이 메서드를 구현한다.


5. 테마 및 FTL 템플릿

템플릿 경로와 로딩 방식

이 프로젝트에는 FTL 템플릿이 놓이는 경로가 두 곳이며, 로딩 주체가 완전히 다르다.

경로로딩 주체역할
theme/keycloak.ext/login/Keycloak 테마 엔진 (자동)Keycloak 기본 화면 오버라이드
theme-resources/templates/이 SPI의 Java 코드 (명시적)이 SPI가 새로 만든 커스텀 화면

theme/keycloak.ext/login/

Realm에 keycloak.ext 테마가 설정되면 Keycloak이 JarThemeProvider를 통해 theme/keycloak.ext/login/ 경로를 자동으로 탐색한다. Keycloak 내장 로그인 플로우(login, register 등)가 렌더링할 파일을 이 경로에서 찾으며, 없으면 부모 테마(keycloak base)로 폴백한다. 이 경로의 파일은 기존 Keycloak 화면을 교체(override)하는 용도이므로 파일명이 Keycloak 표준 파일명과 일치해야 한다.

theme-resources/templates/

ThemeResourceProvider SPI가 담당하는 경로로, Keycloak 테마 엔진의 자동 탐색 대상이 아니다. 이 SPI의 Authenticator/RequiredAction이 context.form().createForm("파일명.ftl")으로 명시적으로 호출할 때만 렌더링된다. Keycloak에 없는 새 화면(약관 동의, 휴면 재활성화 등)은 모두 이 경로에 추가한다.

신규 화면은 반드시 theme-resources/templates/에 추가한다. theme/keycloak.ext/login/에 넣어도 Keycloak 내장 플로우가 자동으로 연결하지 않으므로 Java 코드 없이는 렌더링되지 않는다.

테마 구조

경로용도
theme/keycloak.ext/login/로그인 플로우 FTL (Keycloak 기본 템플릿 오버라이드)
theme/keycloak.ext/email/이메일 텍스트 번들
theme-resources/templates/커스텀 SPI에서 사용하는 FTL (createForm()으로 지정)
theme-resources/templates/html/HTML 이메일 템플릿

주요 FTL 파일

로그인 테마 (theme/keycloak.ext/login/)

파일용도
template.ftl공통 레이아웃 (cardClass 파라미터로 너비 제어)
login.ftl로그인 폼
register.ftl회원가입 폼
login-otp.ftlOTP 코드 입력
login-reset-password.ftl비밀번호 재설정 진입
terms.ftl이용약관 동의

커스텀 SPI 템플릿 (theme-resources/templates/)

파일용도
terms-consent.ftl이용약관 동의 (TermsConsentAuthenticator)
login-reset-password-otp.ftl비밀번호 재설정 OTP (PasswordResetAuthenticator)
account-reactivation.ftl휴면 재활성화 코드 입력
username-find.ftl아이디 찾기

Tailwind CSS 빌드 흐름

input.css → (tailwindcss) → output.css → JAR 패키징
  • input.css: Tailwind 지시자 + 커스텀 컴포넌트 (@layer components)
  • output.css: minified CSS (~13KB), FTL에서 <link> 로드
  • Maven 빌드 시 자동 실행, CSS 단독 변경은 npm run build

CSS 변경 시 캐시 무효화

template.ftl은 CSS URL에 ?v={themeVersion} 쿼리스트링을 붙여 브라우저 캐시를 제어한다. CSS를 변경할 때마다 theme.propertiesthemeVersion 값을 올려야 브라우저가 새 파일을 요청한다.

# theme/keycloak.ext/login/theme.properties
themeVersion=26.5.2-4

버전을 올리지 않으면 배포 후에도 브라우저가 이전 CSS를 그대로 사용한다.

커스텀 CSS 클래스 (input.css @layer components 정의)

클래스용도
.kc-container로그인 컨테이너 전체 래퍼
.kc-card로그인 카드 박스
.kc-input입력 필드
.kc-btn-primary주요 버튼
.kc-label라벨
.kc-error에러 메시지

신규 FTL 추가 방법

  1. theme-resources/templates/에 FTL 파일 작성
  2. Authenticator에서 context.form().createForm("파일명.ftl") 호출
  3. 필요한 변수는 context.form().setAttribute("키", 값) 으로 전달
  4. 메시지 키는 theme/keycloak.ext/login/messages/messages_ko.properties에 추가

메시지 번들

theme/keycloak.ext/login/messages/messages_ko.properties — FTL에서 ${msg("키")} 또는 ${msg("키", 파라미터)} 로 참조


6. REST API 엔드포인트

모든 커스텀 REST API는 RealmResourceProvider SPI로 등록되어 /realms/{realm}/ 하위 경로에 노출된다.

등록된 엔드포인트

Base PathFactory용도
/my-rest-resource/MyResourceProviderFactory범용 테스트/유틸 API
/registration-verify/RegistrationVerifyResourceProviderFactory회원가입 인증 코드
/username-find/UsernameFindResourceProviderFactory아이디 찾기
(프로파일 관련)UserProfileResourceFactory사용자 프로파일 API
(프로파일 페이지)ProfilePageResourceFactory프로파일 페이지

주요 API 상세

MyResourceProvider

메서드경로인증설명
GET/hello없음Realm 이름 반환
GET/hello-authBearer 토큰인증된 사용자명 반환
GET/user?username=&password=없음자격증명 검증

RegistrationVerifyResource

메서드경로설명
POST/send-code이메일/SMS 인증 코드 발송
POST/verify-code인증 코드 검증

제한: 30분 내 최대 5회, TTL 300초

Bearer 토큰 인증 처리

AccessToken token = authManager.verifyBearerToken(session);
if (token == null) return Response.status(401).build();

신규 REST 엔드포인트 추가 방법

  1. RealmResourceProvider 구현 클래스 작성 (getResource() → JAX-RS 리소스 반환)
  2. RealmResourceProviderFactory 구현 (getId(), create())
  3. META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory에 Factory 클래스명 추가
  4. 빌드 후 재시작 → /realms/{realm}/{getId()} 경로로 접근 가능

7. 배포 및 운영

JAR 배포

mvn clean package -Dmaven.test.skip=true
cp target/keycloak-extensions-spi-1.0.0-SNAPSHOT.jar /opt/keycloak/providers/
/opt/keycloak/bin/kc.sh build # 운영환경에서 최초 1회
/opt/keycloak/bin/kc.sh start # 또는 start-dev (개발)

Docker 환경에서는 target/ 디렉토리가 /opt/keycloak/providers/에 볼륨 마운트되어 빌드 후 재시작만으로 적용된다.

Admin Console 초기 설정 순서

  1. Realm 생성: cnap Realm 생성
  2. 테마 적용: Realm Settings → Themes → keycloak.ext
  3. 이벤트 리스너 등록: Realm Settings → Events → Event listeners → last-login-event-listener
  4. Authentication Flow 구성: Authentication → Flows → 로그인·회원가입·비밀번호재설정 플로우 설정
  5. Required Actions 설정: Authentication → Required Actions → Terms Consent, Account Reactivation 등록
  6. IDP 설정: Identity Providers → Kakao / Naver / Inicis Client ID·Secret 입력
  7. User Federation 설정: User Federation → REST → Base URL·인증정보·휴면 설정 입력
  8. SMTP 설정: Realm Settings → Email → SMTP 서버 정보 입력

상세 설정 절차는 docs/keycloak-setup-guide.md 참고

Realm Export / Import

# Export (운영→개발 설정 이관)
/opt/keycloak/bin/kc.sh export --dir /tmp/export --realm cnap

# Import
/opt/keycloak/bin/kc.sh import --dir /tmp/export

개발용 기준 설정: docs/realm-export.json

로그 확인

# Docker 환경
docker compose logs -f keycloak

# 로그 레벨 변경 (compose.yaml)
KC_LOG_LEVEL: debug # info / debug / trace

운영 환경 주요 설정

항목변경 위치
휴면 전환 기간Admin Console → User Federation → 설정 탭
OTP 코드 길이·TTLAdmin Console → Authentication → OTP Authenticator 설정
SMS API 연동 정보Admin Console → Authentication → OTP Authenticator 설정
SMTP 서버Admin Console → Realm Settings → Email
IDP Client ID/SecretAdmin Console → Identity Providers → 각 IDP
운영 모드 전환compose.yaml command: start --optimized (현재 start-dev)

8. Troubleshooting

FTL 약관 내용이 HTML 태그 텍스트로 표시되는 문제

증상: 약관 보기 버튼 클릭 시 내용 영역이 열리지만 약관 HTML이 렌더링되지 않고 <div class="terms-content-body"><p><strong>... 같은 텍스트가 그대로 노출된다.

원인: FreeMarker 연산자 우선순위 문제.

?(built-in) 가 !(missing value handler) 보다 우선순위가 높기 때문에, 아래 표현식은 의도와 다르게 파싱된다.

<!-- 의도: 전체 표현식에 ?no_esc 적용 -->
${(termsServiceContent)!""?no_esc}

<!-- 실제 파싱: ?no_esc 가 default 값 "" 에만 적용됨 -->
${(termsServiceContent)!(""?no_esc)}

변수가 존재하는 경우 ?no_esc 없이 출력되므로 Keycloak FreeMarker auto-escaping이 <&lt;, "&quot; 로 변환한다.

해결: 전체 표현식을 괄호로 감싼 뒤 ?no_esc 적용.

<!-- 올바른 표현 -->
${((termsServiceContent)!"")?no_esc}

적용 위치: terms-consent.ftl 내 모든 약관 content 출력 표현식 (termsServiceContent, termsPrivacyRequiredContent, termsPrivacyOptionalContent, termsMarketingContent).

CSS 변경 후 브라우저에 반영되지 않는 문제

증상: mvn package 및 재배포 후에도 스타일 변경 사항이 브라우저에 반영되지 않는다.

원인: 브라우저가 이전 output.css를 캐시하고 있어 새 파일을 요청하지 않는다. output.css.gitignore에 등록되어 있어 빌드 시 생성되며, URL이 동일하면 캐시를 그대로 사용한다.

해결: template.ftl에서 CSS URL에 ?v={themeVersion} 쿼리스트링을 추가하여 캐시를 무효화한다.

<link href="${url.resourcesPath}/${style}?v=${properties.themeVersion!''}" rel="stylesheet" />

theme.properties에 버전을 정의하고, CSS 변경 시마다 값을 올린다.

themeVersion=26.5.2-3

배포 버전이 바뀌면 URL이 달라지므로 브라우저가 새 파일을 강제로 요청한다.

관련 파일: template.ftl, theme.properties

unmanagedAttributePolicy와 UserModel 접근

unmanagedAttributePolicy: "ADMIN_VIEW"는 User Profile 레이어(검증/접근제어)에만 적용된다. raw UserModel은 이 정책의 영향을 받지 않는다.

접근 방식정책 적용
UserModel.getAttributes() / getFirstAttribute()적용 안 됨 — 모든 속성 반환
UserProfileProvider (스키마 기반 접근)적용됨 — 정책에 따라 필터링
Account REST API, Admin Console적용됨 — 정책에 따라 노출 제어

따라서 커스텀 SPI 코드에서 user.getFirstAttribute("phoneNumber") 등으로 직접 접근하면 ADMIN_VIEW로 지정된 속성도 정상적으로 읽힌다.

반면 VerifyProfileBean(user, formData, session) 등 User Profile API를 거치는 경우에는 정책이 적용되어 일반 사용자에게 해당 속성이 노출되지 않는다.