서버 개발자 가이드
Red Hat Single Sign-On 7.6과 함께 사용하는 경우
초록
보다 포괄적 수용을 위한 오픈 소스 용어 교체
Red Hat은 코드, 문서, 웹 속성에서 문제가 있는 용어를 교체하기 위해 최선을 다하고 있습니다. 먼저 마스터(master), 슬레이브(slave), 블랙리스트(blacklist), 화이트리스트(whitelist) 등 네 가지 용어를 교체하고 있습니다. 이러한 변경 작업은 작업 범위가 크므로 향후 여러 릴리스에 걸쳐 점차 구현할 예정입니다. 자세한 내용은 CTO Chris Wright의 메시지를 참조하십시오.
1장. 머리말
일부 예제 목록에서 한 줄에 표시되는 내용은 사용 가능한 페이지 너비에 적합하지 않습니다. 이 라인은 손상되었습니다. 행 끝에 있는 '\'는 다음 행을 들여쓰기한 상태에서 페이지에 맞게 중단이 도입되었음을 의미합니다. So:
Let's pretend to have an extremely \ long line that \ does not fit This one is short
Let's pretend to have an extremely \
long line that \
does not fit
This one is short
실제로는 다음과 같습니다.
Let's pretend to have an extremely long line that does not fit This one is short
Let's pretend to have an extremely long line that does not fit
This one is short
2장. 관리 REST API
Red Hat Single Sign-On에는 완전히 작동하는 관리 REST API와 관리 콘솔에서 제공하는 모든 기능이 함께 제공됩니다.
API를 호출하려면 적절한 권한이 있는 액세스 토큰을 가져와야 합니다. 필요한 권한은 서버 관리 가이드에 설명되어 있습니다.
Red Hat Single Sign-On을 사용하여 애플리케이션에 대한 인증을 활성화하여 토큰을 가져올 수 있습니다. 보안 애플리케이션 및 서비스 가이드를 참조하십시오. 직접 액세스 부여를 사용하여 액세스 토큰을 가져올 수도 있습니다.
2.1. CURL 사용 예
2.1.1. 사용자 이름과 암호를 사용하여 인증
절차
사용자 이름
admin
및 암호암호
를 사용하여 영역마스터
에서 사용자의 액세스 토큰을 확보합니다.curl \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=password" \ -d "grant_type=password" \ "http://localhost:8080/auth/realms/master/protocol/openid-connect/token"
curl \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=password" \ -d "grant_type=password" \ "http://localhost:8080/auth/realms/master/protocol/openid-connect/token"
Copy to Clipboard Copied! 참고기본적으로 이 토큰은 1분 후에 만료됩니다.
결과는 JSON 문서가 됩니다.
-
access_token
속성의 값을 추출하여 필요한 API를 호출합니다. API에 대한 요청의
Authorization
헤더에 값을 포함하여 API를 호출합니다.다음 예제에서는 마스터 영역의 세부 정보를 가져오는 방법을 보여줍니다.
curl \ -H "Authorization: bearer eyJhbGciOiJSUz..." \ "http://localhost:8080/auth/admin/realms/master"
curl \ -H "Authorization: bearer eyJhbGciOiJSUz..." \ "http://localhost:8080/auth/admin/realms/master"
Copy to Clipboard Copied!
2.1.2. 서비스 계정으로 인증
client_id
및 client_secret
을 사용하여 Admin REST API에 대해 인증하려면 다음 절차를 수행합니다.
절차
클라이언트가 다음과 같이 구성되었는지 확인합니다.
-
client_id
는 영역 마스터에 속하는 기밀 클라이언트입니다. -
client_id
에서비스 계정 활성화
옵션이 활성화되어 있습니다. client_id
에는 사용자 정의 "Audience" 매퍼가 있습니다.-
포함된 ClientECDHE:
security-admin-console
-
포함된 ClientECDHE:
-
-
client_id
에 "Service Account Roles" 탭에 할당된 'admin' 역할이 있는지 확인합니다.
curl \ -d "client_id=<YOUR_CLIENT_ID>" \ -d "client_secret=<YOUR_CLIENT_SECRET>" \ -d "grant_type=client_credentials" \ "http://localhost:8080/auth/realms/master/protocol/openid-connect/token"
curl \
-d "client_id=<YOUR_CLIENT_ID>" \
-d "client_secret=<YOUR_CLIENT_SECRET>" \
-d "grant_type=client_credentials" \
"http://localhost:8080/auth/realms/master/protocol/openid-connect/token"
2.2. 추가 리소스
3장. themes
Red Hat Single Sign-On은 웹 페이지 및 이메일에 대한 주제 지원을 제공합니다. 이를 통해 최종 사용자 대면 페이지의 모양과 인식을 사용자 정의하여 애플리케이션과 통합할 수 있습니다.
그림 3.1. Sunrise 예제를 사용하는 로그인 페이지

3.1. meme 유형
Theme은 Red Hat Single Sign-On의 다양한 측면을 사용자 정의하는 하나 이상의 유형을 제공할 수 있습니다. 사용 가능한 유형은 다음과 같습니다.
- 계정 - 계정 관리
- 관리자 - 관리자 콘솔
- 이메일 - 이메일
- 로그인 - 로그인 양식
- 시작 - 시작 페이지
3.2. 주제 구성
시작을 제외한 모든 Theme type은 Admin Console을 통해 구성됩니다.
절차
- 관리 콘솔에 로그인합니다.
- 왼쪽 상단의 드롭다운 상자에서 해당 영역을 선택합니다.
- 메뉴에서 CloudEvent Settings 을 클릭합니다.
Themes 탭을 클릭합니다.
참고마스터 관리 콘솔의 주제를 설정하려면
마스터
- 관리 콘솔의 변경 사항을 보려면 페이지를 새로 고칩니다.
-
standalone.xml
,standalone-ha.xml
또는domain.xml
을 편집하여 시작 주제를 변경합니다. 다음과 같이 topic 요소에
welcomeTheme
를 추가합니다.<theme> ... <welcomeTheme>custom-theme</welcomeTheme> ... </theme>
<theme> ... <welcomeTheme>custom-theme</welcomeTheme> ... </theme>
Copy to Clipboard Copied! - 시작 항목에 대한 변경 사항을 적용하려면 서버를 다시 시작합니다.
3.3. 기본 주제
Red Hat Single Sign-On은 서버의 루트 주제 디렉터리에 기본 주제
와 함께 번들로 제공됩니다. 업그레이드를 간소화하려면 번들된 주제를 직접 편집해서는 안 됩니다. 대신 번들 주제 중 하나를 확장하는 고유한 주제를 만듭니다.
3.4. 주제 생성
Theme consists of:
- HTML 템플릿 (Freemarker 템플릿)
- 이미지
- 메시지 번들
- 스타일시트
- 스크립트
- meme 속성
각 페이지를 교체하지 않는 한 다른 주제를 확장해야합니다. 대부분의 경우 Red Hat Single Sign-On 주제를 확장하려고 하지만 페이지의 모양과 느낌은 크게 변경되는 경우 기본 Theme를 확장할 수도 있습니다. 기본 주제는 주로 HTML 템플릿과 메시지 번들로 구성되며 Red Hat Single Sign-On Theme에는 주로 이미지와 스타일 대결이 포함되어 있습니다.
주제를 확장할 때 개별 리소스(templates, 스타일 대시 등)를 덮어쓸 수 있습니다. HTML 템플릿을 재정의하려면 새 릴리스로 업그레이드할 때 사용자 지정 템플릿을 업데이트해야 할 수 있습니다.
주제를 만드는 동안 캐싱을 비활성화하는 것이 좋습니다. 따라서 Red Hat Single Sign-On을 다시 시작하지 않고도 themes
디렉토리에서 직접mes 리소스를 편집할 수 있습니다.
절차
-
standalone.xml
을 편집합니다. Theme
에서staticMaxAge
를-1
로 설정하고cacheTemplates
및cacheThemes
를false
로 설정합니다.<theme> <staticMaxAge>-1</staticMaxAge> <cacheThemes>false</cacheThemes> <cacheTemplates>false</cacheTemplates> ... </theme>
<theme> <staticMaxAge>-1</staticMaxAge> <cacheThemes>false</cacheThemes> <cacheTemplates>false</cacheTemplates> ... </theme>
Copy to Clipboard Copied! themes
디렉터리에 디렉터리를 생성합니다.디렉터리의 이름은 topic의 이름이 됩니다. 예를 들어
mytheme
이라는 주제를 생성하면 디렉토리themes/mytheme
가 생성됩니다.주제 디렉터리 내에서 주제가 제공할 각 유형에 대한 디렉터리를 생성합니다.
예를 들어,
mytheme
topic에 로그인 유형을 추가하려면 디렉토리themes/mytheme/login
을 만듭니다.각 유형에 대해
Theme에 대한 일부 구성을 설정할 수 있는 파일 topic.properties
를 생성합니다.예를 들어, 주제 주제/mytheme/login을 구성하여 기본 주제를 확장하고 몇 가지 공통 리소스를 가져오려면 다음 내용을 사용하여 파일
themes/mytheme/login
/theme.propertiesparent=base import=common/keycloak
parent=base import=common/keycloak
Copy to Clipboard Copied! 이제 로그인 유형을 지원하는 주제를 생성했습니다.
- 관리 콘솔에 로그인하여 새 주제를 점검합니다.
- 사용자 영역 선택
- 메뉴에서 CloudEvent Settings 을 클릭합니다.
- Themes 탭을 클릭합니다.
- Login Theme 은 mytheme 을 선택하고 저장 을 클릭합니다.
영역의 로그인 페이지를 엽니다.
애플리케이션을 통해 로그인하거나 계정 관리 콘솔(
/realms/{realm name}/account
)을 열어 이 작업을 수행할 수 있습니다.-
부모 주제를 변경하는 효과를 보려면 topic
.properties
에서parent=keycloak
을 설정하고 로그인 페이지를 새로 고칩니다.
성능에 큰 영향을 미치기 때문에 프로덕션에서 캐싱을 다시 활성화해야 합니다.
3.4.1. meme 속성
Themes 속성은 topic 디렉터리의 <THEME TYPE>/theme.properties
파일에 설정되어 있습니다.
- 부모 - 확장하려는 주제
- 가져오기 - 다른me에서 리소스 가져오기
- style - 공백으로 구분된 스타일 목록을 포함합니다.
- 로케일 - 지원되는 로케일의 쉼표로 구분된 목록
특정 요소 유형에 사용되는 css 클래스를 변경하는 데 사용할 수 있는 속성 목록이 있습니다. 이러한 속성 목록은 keycloak의 해당 유형에서 topic.properties 파일을 참조하십시오 (themes/keycloak/<THEME TYPE>/theme.properties
).
사용자 지정 속성을 추가하고 사용자 지정 템플릿에서 사용할 수도 있습니다.
이렇게 하면 다음 형식을 사용하여 시스템 속성 또는 환경 변수를 대체할 수 있습니다.
-
${some.system.property}
- 시스템 속성의 경우 -
${env.ENV_VAR}
- 환경 변수의 경우.
시스템 속성 또는 환경 변수가 ${foo:defaultValue}
에서 찾을 수 없는 경우에도 기본값을 제공할 수 있습니다.
기본값이 제공되지 않고 해당 시스템 속성 또는 환경 변수가 없으면 아무것도 교체되지 않고 템플릿의 형식으로 끝납니다.
가능한 작업의 예는 다음과 같습니다.
javaVersion=${java.version} unixHome=${env.HOME:Unix home not found} windowsHome=${env.HOMEPATH:Windows home not found}
javaVersion=${java.version}
unixHome=${env.HOME:Unix home not found}
windowsHome=${env.HOMEPATH:Windows home not found}
3.4.2. 제목에 스타일 워크시트 추가
제목에 하나 이상의 스타일시트를 추가할 수 있습니다.
절차
-
주제의 <
THEME TYPE>/resources/css
디렉터리에 파일을 만듭니다. 이 파일을 topic
.
의 style 속성에 추가합니다.properties
예를 들어
mytheme
에 style.css
를 추가하려면 다음 콘텐츠를 사용하여themes/mytheme/login/resources/css/styles.css
를 생성합니다..login-pf body { background: DimGrey none; }
.login-pf body { background: DimGrey none; }
Copy to Clipboard Copied! 주제/mytheme/login/theme.properties
를 편집하고 다음을 추가합니다.styles=css/styles.css
styles=css/styles.css
Copy to Clipboard Copied! 변경 사항을 확인하려면 영역의 로그인 페이지를 엽니다.
적용되는 유일한 스타일은 사용자 정의 스타일로 인한 것입니다.
부모 주제의 스타일을 포함하려면 해당 주제에서 스타일을 로드합니다.
주제/mytheme/login/theme.properties
를 편집하고스타일을
다음과 같이 변경합니다.styles=web_modules/@fontawesome/fontawesome-free/css/icons/all.css web_modules/@patternfly/react-core/dist/styles/base.css web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css css/login.css css/styles.css
styles=web_modules/@fontawesome/fontawesome-free/css/icons/all.css web_modules/@patternfly/react-core/dist/styles/base.css web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css css/login.css css/styles.css
Copy to Clipboard Copied! 참고부모 스타일 대결에서 style을 재정의하려면 스타일시트를 마지막에 나열해야 합니다.
3.4.3. 주제에 스크립트 추가
주제에 하나 이상의 스크립트를 추가할 수 있습니다.
절차
-
주제의 <
THEME TYPE>/resources/js
디렉터리에 파일을 만듭니다. 파일을 topic
.properties
의scripts
속성에 추가합니다.예를 들어
mytheme
에script.js
를 추가하려면 다음 콘텐츠를 사용하여themes/mytheme/login/resources/js/script.js
를 생성합니다.alert('Hello');
alert('Hello');
Copy to Clipboard Copied! 그런 다음
themes/mytheme/login/theme.properties
를 편집하고 다음을 추가합니다.scripts=js/script.js
scripts=js/script.js
Copy to Clipboard Copied!
3.4.4. meme에 이미지 추가
주제에서 이미지를 사용할 수 있도록 하려면 주제의 < THEME TYPE>/resources/img
디렉터리에 이미지를 추가합니다. 스타일시트 내에서 또는 HTML 템플릿에서 직접 사용할 수 있습니다.
예를 들어 mytheme에 이미지를 추가하려면 이미지를 themes/
.
mytheme
/login/resources/img/image.anchor 에 복사합니다
그런 다음 다음을 사용하여 사용자 정의 스타일시트 내에서 이 이미지를 사용할 수 있습니다.
body { background-image: url('../img/image.jpg'); background-size: cover; }
body {
background-image: url('../img/image.jpg');
background-size: cover;
}
또는 HTML 템플릿에서 직접 사용하려면 사용자 지정 HTML 템플릿에 다음을 추가합니다.
<img src="${url.resourcesPath}/img/image.jpg">
<img src="${url.resourcesPath}/img/image.jpg">
3.4.5. 메시지
템플릿의 텍스트는 메시지 번들에서 로드됩니다. 다른me을 확장하는 주제는 부모의 메시지 번들의 모든 메시지를 상속하며, < THEME TYPE>/ECDHE/ECDHE_en.properties를 귀하의 Theme에 추가하여 개별 메시지를
덮어쓸 수 있습니다.
예를 들어 로그인 양식의 Username
을 mytheme
의 Username
으로 교체하려면 파일 themes/mytheme/login/ECDHE_en.properties
를 다음 콘텐츠로 생성합니다.
usernameOrEmail=Your Username
usernameOrEmail=Your Username
메시지를 사용할 때 {0}
및 {1}
와 같은 메시지 값 내에서 인수로 교체됩니다. 예를 들어 로그인에서 {0} {0}
은(는) 영역 이름으로 교체됩니다.
이러한 메시지 번들의 텍스트는 영역별 값으로 덮어쓸 수 있습니다. 영역별 값은 UI 및 API를 통해 관리할 수 있습니다.
3.4.6. 영역에 언어 추가
사전 요구 사항
- 영역에 대한 국제화를 활성화하려면 서버 관리 가이드를 참조하십시오.
절차
-
주제 디렉토리에 <
THEME TYPE>/Forwarded/ECDHE_<LOCALE>.properties
파일을 만듭니다. 이 파일을 <
THEME TYPE>/theme.properties
의locales
속성에 추가합니다. 사용자의로그인
영역,계정
및이메일에
사용할 수 있는 언어를 사용하려면, 주제에서 해당 언어를 지원해야 하므로 해당 주제 유형에 대해 언어를 추가해야 합니다.예를 들어, Norwegian 번역을
mytheme
topic에 추가하려면 다음 콘텐츠와 함께themes/mytheme/login/ECDHE_no.properties
파일을 생성합니다.usernameOrEmail=Brukernavn password=Passord
usernameOrEmail=Brukernavn password=Passord
Copy to Clipboard Copied! 메시지에 대한 번역을 생략하면 영어가 사용됩니다.
주제/mytheme/login/theme.properties
를 편집하고 다음을 추가합니다.locales=en,no
locales=en,no
Copy to Clipboard Copied! -
계정
및이메일
주제 유형에 대해 동일하게 추가합니다. 이렇게 하려면 주제/mytheme/account/ECDHE_no.properties
및themes/mytheme/email/ECDHE/knative_no.properties
를 만듭니다. 이러한 파일을 비워 두면 영어 메시지가 사용됩니다. -
themes/mytheme/login/theme.properties
를themes/mytheme/account/theme.properties
및themes/mytheme/email/theme.properties
에 복사합니다. 언어 선택기에 대한 번역을 추가합니다. 이 작업은 영어 번역에 메시지를 추가하여 수행됩니다. 이렇게 하려면
themes/mytheme/account/ECDHE_en.properties
및themes/mytheme/login/ECDHE_en.properties에 다음을 추가합니다.
locale_no=Norsk
locale_no=Norsk
Copy to Clipboard Copied! 기본적으로 메시지 속성 파일은 ISO-8859-1을 사용하여 인코딩해야 합니다. 특수 헤더를 사용하여 인코딩을 지정할 수도 있습니다. 예를 들어 UTF-8 인코딩을 사용합니다.
encoding: UTF-8
# encoding: UTF-8 usernameOrEmail=....
Copy to Clipboard Copied!
3.4.7. 사용자 정의 ID 공급자 아이콘 추가
Red Hat Single Sign-On은 로그인 화면에 표시되는 사용자 정의 ID 공급자의 아이콘을 추가할 수 있습니다.
절차
-
키 패턴
kcLogoIdP-<alias>를 사용하여
.로그인she
파일에서 아이콘 클래스를 정의합니다.properties
myProvider
별칭이 있는 ID 공급자의 경우 사용자 정의주제의 topic.properties
파일에 행을 추가할 수 있습니다. 예를 들면 다음과 같습니다.kcLogoIdP-myProvider = fa fa-lock
kcLogoIdP-myProvider = fa fa-lock
Copy to Clipboard Copied!
모든 아이콘은 PatternFly4의 공식 웹 사이트에서 사용할 수 있습니다. 소셜 공급자의 아이콘은 이미 기본 로그인 주제 속성 (themes/keycloak/login/theme.properties
)에 정의되어 있습니다.
3.4.8. 사용자 정의 HTML 템플릿 생성
Red Hat Single Sign-On은 Apache Freemarker 템플릿을 사용하여 HTML을 생성합니다. < THEME TYPE>/<TEMPLATE>.ftl
을 생성하여 자체 주제에서 개별 템플릿을 덮어쓸 수 있습니다. 사용된 템플릿 목록은 themes/base/<THEME TYPE>
.
절차
- 템플릿을 기본 주제에서 고유한 주제로 복사합니다.
필요한 수정 사항을 적용합니다.
예를 들어
mytheme
topic에 대한 사용자 지정 로그인 양식을 생성하려면themes/base/login/login
을themes/mytheme/login
에 복사하여 편집기에서 엽니다.첫 번째 줄(<#import …> 뒤에 <
h1>HELLO WORLD!</h1
>을 추가합니다.<#import "template.ftl" as layout> <h1>HELLO WORLD!</h1> ...
<#import "template.ftl" as layout> <h1>HELLO WORLD!</h1> ...
Copy to Clipboard Copied! - 수정된 템플릿을 백업합니다. 새 버전의 Red Hat Single Sign-On으로 업그레이드할 때 해당하는 경우 원래 템플릿에 변경 사항을 적용하려면 사용자 지정 템플릿을 업데이트해야 할 수 있습니다.
3.4.9. email
이메일의 제목과 내용(예: 암호 복구 이메일)을 편집하려면 주제의 이메일
유형에 메시지 번들을 추가합니다. 각 이메일에는 세 개의 메시지가 있습니다. 하나는 제목에 대한 것이며, 하나는 일반 텍스트 본문에, 하나는 HTML 본문에 대한 것입니다.
사용 가능한 모든 이메일을 보려면 themes/base/email/Forwarded/Forwarded_en.properties
를 참조하십시오.
예를 들어 mytheme
의 암호 복구 이메일을 변경하려면 다음 콘텐츠를 사용하여 주제/mytheme/email/ECDHE_en.properties
를 생성합니다.
passwordResetSubject=My password recovery passwordResetBody=Reset password link: {0} passwordResetBodyHtml=<a href="{0}">Reset password</a>
passwordResetSubject=My password recovery
passwordResetBody=Reset password link: {0}
passwordResetBodyHtml=<a href="{0}">Reset password</a>
3.5. 주제 배포
주제 디렉터리를 themes
에 복사하여 Red Hat Single Sign-On에 배포하거나 아카이브로 배포할 수 있습니다. 개발 중에 주제를 themes
디렉터리에 복사할 수 있지만 프로덕션에서는 아카이브
사용을 고려할 수 있습니다. 아카이브
를 사용하면 특히 클러스터링과 같이 Red Hat Single Sign-On의 여러 인스턴스가 있는 경우, 특히 클러스터링의 버전이 지정된 사본을 더 쉽게 사용할 수 있습니다.
절차
- 주제를 아카이브로 배포하려면 topic 리소스를 사용하여 JAR 아카이브를 생성합니다.
META-INF/keycloak-themes.json
파일을 아카이브에서 사용 가능한 주제와 각 주제에서 제공하는 유형을 나열하는 아카이브에 추가합니다.예를 들어
mytheme
topic의 경우 콘텐츠로mytheme.jar
를 생성합니다.- META-INF/keycloak-themes.json
- theme/mytheme/login/theme.properties
- theme/mytheme/login/login.ftl
- theme/mytheme/login/resources/css/styles.css
- theme/mytheme/login/resources/img/image.png
- theme/mytheme/login/messages/messages_en.properties
theme/mytheme/email/messages/messages_en.properties
이 경우
META-INF/keycloak-themes.json
의 내용은 다음과 같습니다.{ "themes": [{ "name" : "mytheme", "types": [ "login", "email" ] }] }
{ "themes": [{ "name" : "mytheme", "types": [ "login", "email" ] }] }
Copy to Clipboard Copied! 단일 아카이브에는 여러 주제가 포함될 수 있으며 각 주제는 하나 이상의 유형을 지원할 수 있습니다.
Red Hat Single Sign-On에 아카이브를 배포하려면 Red Hat Single Sign-On의 독립 실행형/deployments/
디렉터리에 추가하면 자동으로 로드됩니다.
3.6. Theme selector
기본적으로 영역에 대해 구성된 주제가 사용되며, 로그인 주제를 덮어쓸 수 있는 클라이언트를 제외하고 사용됩니다. 이 동작은 Theme Selector SPI를 통해 변경할 수 있습니다.
예를 들어 사용자 에이전트 헤더를 확인하여 데스크탑 및 모바일 장치에 대한 다양한 주제를 선택하는 데 사용할 수 있습니다.
사용자 정의 주제 선택기를 만들려면 ThemeSelectorProviderFactory
및 ThemeSelectorProvider
Provider를 구현해야 합니다.
3.7. meme 리소스
Red Hat Single Sign-On에서 사용자 지정 공급자를 구현하는 경우 템플릿, 리소스 및 메시지 번들을 추가해야 하는 경우가 많습니다.
추가 주제 리소스를 로드하는 가장 쉬운 방법은 topic -resources/templates 리소스에서 resources
in topic -resources/
templates 리소스를 사용하여 JAR를 생성하는 것입니다
.
ThemeResourceSPI를 통해 달성 가능한 템플릿 및 리소스를 보다 유연하게 로드하려면 다음을 수행합니다. ThemeResourceProviderFactory
및 ThemeResourceProvider
를 구현하면 템플릿 및 리소스를 로드하는 방법을 정확하게 결정할 수 있습니다.
3.8. 로케일 선택기
기본적으로 로케일은 LocaleSelectorProvider 인터페이스를 구현하는 Default
를 사용하여 선택합니다. 영어는 국제화가 비활성화된 경우 기본 언어입니다. 국제화를 활성화하면 서버 관리 가이드에 설명된 논리에 따라 로케일이 해결됩니다.
LocaleSelectorProvider
이 동작은 LocaleSelectorProvider 및 LocaleSelectorProvider
Factory
를 구현하여 LocaleSelectorSPI
를 통해 변경할 수 있습니다.
LocaleSelectorProvider
인터페이스에는 단일 메서드 resolveLocale
가 있으며, 이 방법은 CloudEvent Model 및 aECDHE
이 지정된 로케일을 반환해야 합니다. 실제 요청은 UserModel
KeycloakSession#getContext
메서드에서 사용할 수 있습니다.
사용자 지정 구현에서는 기본 동작의 일부를 재사용하기 위해 DefaultLocaleSelectorProvider
를 확장할 수 있습니다. 예를 들어 Accept-
Forwarded 요청 헤더를 무시하기 위해 사용자 정의 구현에서 기본 공급자를 확장하고, getAccept>-<HeaderLocale
를 재정의하고, null 값을 반환할 수 있습니다. 결과적으로 로케일 선택이 영역의 기본 언어로 대체됩니다.
4장. 사용자 정의 사용자 속성
사용자 지정 topic을 사용하여 등록 페이지 및 계정 관리 콘솔에 사용자 지정 사용자 속성을 추가할 수 있습니다.
4.1. 등록 페이지
이 절차를 사용하여 등록 페이지에 사용자 지정 속성을 입력합니다.
절차
-
템플릿
themes/base/login/register.ftl
을 사용자 지정 topic의 로그인 유형에 복사합니다. 편집기에서 사본을 엽니다.
예를 들어 등록 페이지에 휴대폰 번호를 추가하려면 양식에 다음 스니펫을 추가합니다.
<div class="form-group"> <div class="${properties.kcLabelWrapperClass!}"> <label for="user.attributes.mobile" class="${properties.kcLabelClass!}">Mobile number</label> </div> <div class="${properties.kcInputWrapperClass!}"> <input type="text" class="${properties.kcInputClass!}" id="user.attributes.mobile" name="user.attributes.mobile" value="${(register.formData['user.attributes.mobile']!'')}"/> </div> </div>
<div class="form-group"> <div class="${properties.kcLabelWrapperClass!}"> <label for="user.attributes.mobile" class="${properties.kcLabelClass!}">Mobile number</label> </div> <div class="${properties.kcInputWrapperClass!}"> <input type="text" class="${properties.kcInputClass!}" id="user.attributes.mobile" name="user.attributes.mobile" value="${(register.formData['user.attributes.mobile']!'')}"/> </div> </div>
Copy to Clipboard Copied! -
입력 html 요소의 이름이
user.attributes
로 시작하는지 확인합니다. 위의 예에서 속성은 Red Hat Single Sign-On에 이름mobile
에 의해 저장됩니다. - 변경 사항을 보려면 영역에 로그인할 때 사용자 지정 주제를 사용하고 있는지 확인하고 등록 페이지를 엽니다.
4.2. 계정 관리 콘솔
계정 관리 콘솔의 사용자 프로필 페이지에서 사용자 지정 속성을 관리하려면 다음 절차를 사용하십시오.
절차
-
템플릿
themes/base/account/account.ftl
을 사용자 지정 topic의 계정 유형으로 복사합니다. 편집기에서 사본을 엽니다.
예를 들어 계정 페이지에 휴대폰 번호를 추가하려면 양식에 다음 스니펫을 추가합니다.
<div class="form-group"> <div class="col-sm-2 col-md-2"> <label for="user.attributes.mobile" class="control-label">Mobile number</label> </div> <div class="col-sm-10 col-md-10"> <input type="text" class="form-control" id="user.attributes.mobile" name="user.attributes.mobile" value="${(account.attributes.mobile!'')}"/> </div> </div>
<div class="form-group"> <div class="col-sm-2 col-md-2"> <label for="user.attributes.mobile" class="control-label">Mobile number</label> </div> <div class="col-sm-10 col-md-10"> <input type="text" class="form-control" id="user.attributes.mobile" name="user.attributes.mobile" value="${(account.attributes.mobile!'')}"/> </div> </div>
Copy to Clipboard Copied! -
입력 html 요소의 이름이
user.attributes
로 시작하는지 확인합니다. - 변경 사항을 보려면 해당 영역에서 계정 주제로 사용자 지정 주제를 사용하고 있는지 확인하고 계정 관리 콘솔에서 사용자 프로필 페이지를 엽니다.
5장. Identity Brokering API
Red Hat Single Sign-On은 로그인을 위해 상위 IDP에 인증을 위임할 수 있습니다. 일반적인 예는 사용자가 Facebook 또는 Google과 같은 소셜 공급자를 통해 로그인할 수 있도록 하려는 경우입니다. 기존 계정을 브로커 IDP에 연결할 수도 있습니다. 이 섹션에서는 ID 브로커링과 관련하여 애플리케이션에서 사용할 수 있는 일부 API에 대해 설명합니다.
5.1. 외부 IDP 토큰 검색
Red Hat Single Sign-On을 사용하면 외부 IDP를 사용하여 인증 프로세스의 토큰과 응답을 저장할 수 있습니다. 이를 위해 IDP 설정 페이지에서 Store Token
구성 옵션을 사용할 수 있습니다.
애플리케이션 코드는 이러한 토큰과 응답을 검색하여 추가 사용자 정보를 가져오거나 외부 IDP에 대한 요청을 안전하게 호출할 수 있습니다. 예를 들어 애플리케이션은 다른 Google 서비스 및 REST API에서 호출하기 위해 Google 토큰을 사용할 수 있습니다. 특정 ID 공급자에 대한 토큰을 검색하려면 다음과 같이 요청을 보내야 합니다.
GET /auth/realms/{realm}/broker/{provider_alias}/token HTTP/1.1 Host: localhost:8080 Authorization: Bearer <KEYCLOAK ACCESS TOKEN>
GET /auth/realms/{realm}/broker/{provider_alias}/token HTTP/1.1
Host: localhost:8080
Authorization: Bearer <KEYCLOAK ACCESS TOKEN>
애플리케이션은 Red Hat Single Sign-On으로 인증되고 액세스 토큰이 수신되어야 합니다. 이 액세스 토큰에는 브로커
클라이언트 수준 역할 read-token
이 있어야 합니다. 즉, 사용자에게 이 역할에 대한 역할 매핑이 있어야 하며 클라이언트 애플리케이션에 해당 범위 내에서 해당 역할이 있어야 합니다. 이 경우 Red Hat Single Sign-On에서 보호 서비스에 액세스하는 경우 사용자 인증 중에 Red Hat Single Sign-On에서 발행한 액세스 토큰을 보내야 합니다. 브로커 구성 페이지에서 저장된 토큰 읽기 가능 스위치를 켜서 새로 가져온 사용자에게 이 역할을 자동으로 할당할 수
있습니다.
이러한 외부 토큰은 공급자를 통해 다시 로그인하거나 클라이언트 시작 계정 연결 API를 사용하여 다시 설정할 수 있습니다.
5.2. 클라이언트 시작 계정 연결
일부 애플리케이션은 Facebook과 같은 소셜 공급자와 통합하려고 하지만 이러한 소셜 공급자를 통해 로그인할 수 있는 옵션을 제공하지 않으려고 합니다. Red Hat Single Sign-On은 애플리케이션이 기존 사용자 계정을 특정 외부 IDP에 연결하는 데 사용할 수 있는 브라우저 기반 API를 제공합니다. 이를 클라이언트 시작 계정 연결이라고 합니다. 계정 링크는 OIDC 애플리케이션에서만 시작할 수 있습니다.
애플리케이션이 작동하는 방식은 사용자의 브라우저를 Red Hat Single Sign-On 서버의 URL로 전달하여 사용자의 계정을 특정 외부 공급자(예: Facebook)에 연결하도록 요청하는 것입니다. 서버는 외부 공급자를 사용한 로그인을 시작합니다. 브라우저가 외부 공급자에서 로그인되고 다시 서버로 리디렉션됩니다. 서버는 링크를 설정하고 확인과 함께 애플리케이션으로 다시 리디렉션합니다.
이 프로토콜을 시작하기 전에 클라이언트 애플리케이션에서 충족해야 하는 몇 가지 사전 조건이 있습니다.
- 원하는 ID 공급자는 관리 콘솔에서 사용자 영역에 대해 구성하고 활성화해야 합니다.
- 사용자 계정이 이미 OIDC 프로토콜을 통해 기존 사용자로 로그인되어 있어야 합니다.
-
사용자에게
account.manage-account
또는account.manage-account-links
역할 매핑이 있어야 합니다. - 애플리케이션에 액세스 토큰 내에서 해당 역할에 대한 범위를 부여해야 합니다.
- 애플리케이션은 리디렉션 URL을 생성하기 위해 내부 정보가 필요하므로 액세스 토큰에 액세스할 수 있어야 합니다.
로그인을 시작하려면 애플리케이션이 URL을 작성하고 사용자 브라우저를 이 URL로 리디렉션해야 합니다. URL은 다음과 같습니다.
/{auth-server-root}/auth/realms/{realm}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash}
/{auth-server-root}/auth/realms/{realm}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash}
다음은 각 경로 및 쿼리 매개 변수에 대한 설명입니다.
- 공급자
-
이는 관리 콘솔의
ID 공급자 섹션에 정의한 외부 IDP의 공급자
별칭입니다. - client_id
- 애플리케이션의 OIDC 클라이언트 ID입니다. 관리 콘솔에서 애플리케이션을 클라이언트로 등록할 때 이 클라이언트 ID를 지정해야 했습니다.
- redirect_uri
- 계정 링크가 설정된 후 리디렉션하려는 애플리케이션 콜백 URL입니다. 클라이언트 리디렉션 URI 패턴이어야 합니다. 즉, 관리 콘솔에서 클라이언트를 등록할 때 정의한 유효한 URL 패턴 중 하나와 일치해야 합니다.
- nonce
- 애플리케이션에서 생성해야 하는 임의의 문자열입니다.
- hash
-
Base64 URL 인코딩 해시입니다. 이 해시는 Base64 URL에서
nonce
+token.getSessionState()
+token.getIssuedFor()
+공급자
의 SHA_256 해시를 인코딩하여 생성합니다. 토큰 변수는 OIDC 액세스 토큰에서 가져옵니다. 기본적으로 임의의 번호, 사용자 세션 ID, 클라이언트 ID 및 액세스하려는 ID 공급자 별칭을 해시하고 있습니다.
다음은 계정 링크를 설정하는 URL을 생성하는 Java Servlet 코드의 예입니다.
KeycloakSecurityContext session = (KeycloakSecurityContext) httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName()); AccessToken token = session.getToken(); String clientId = token.getIssuedFor(); String nonce = UUID.randomUUID().toString(); MessageDigest md = null; try { md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } String input = nonce + token.getSessionState() + clientId + provider; byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8)); String hash = Base64Url.encode(check); request.getSession().setAttribute("hash", hash); String redirectUri = ...; String accountLinkUrl = KeycloakUriBuilder.fromUri(authServerRootUrl) .path("/auth/realms/{realm}/broker/{provider}/link") .queryParam("nonce", nonce) .queryParam("hash", hash) .queryParam("client_id", clientId) .queryParam("redirect_uri", redirectUri).build(realm, provider).toString();
KeycloakSecurityContext session = (KeycloakSecurityContext) httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName());
AccessToken token = session.getToken();
String clientId = token.getIssuedFor();
String nonce = UUID.randomUUID().toString();
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
String input = nonce + token.getSessionState() + clientId + provider;
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
String hash = Base64Url.encode(check);
request.getSession().setAttribute("hash", hash);
String redirectUri = ...;
String accountLinkUrl = KeycloakUriBuilder.fromUri(authServerRootUrl)
.path("/auth/realms/{realm}/broker/{provider}/link")
.queryParam("nonce", nonce)
.queryParam("hash", hash)
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri).build(realm, provider).toString();
이 해시가 포함된 이유는 무엇입니까? 이를 통해 auth 서버는 클라이언트 애플리케이션이 요청을 시작했으며 다른 악성 앱이 특정 공급자에게 연결되도록 무작위로 요청하지 않았습니다. 인증 서버는 먼저 로그인 시 설정된 SSO 쿠키를 확인하여 사용자가 로그인했는지 확인합니다. 그런 다음 현재 로그인을 기반으로 해시를 다시 생성하고 애플리케이션에서 보낸 해시와 일치하려고 합니다.
계정이 연결되면 auth 서버는 redirect_uri
로 리디렉션됩니다. 링크 요청을 처리하는 데 문제가 있는 경우 auth 서버는 redirect_uri
로 리디렉션할 수도 있습니다. 브라우저가 애플리케이션으로 리디렉션되는 대신 오류 페이지를 종료할 수 있습니다. 오류 조건이 있고 인증 서버가 클라이언트 애플리케이션으로 다시 리디렉션할 수 있을 만큼 안전한 것으로 간주하는 경우 추가 오류
쿼리 매개변수가 redirect_uri
에 추가됩니다.
이 API는 애플리케이션이 요청을 시작한 것을 보장하지만 이 작업에 대한 CSRF 공격을 완전히 방지하지는 않습니다. 애플리케이션은 여전히 CSRF 공격 대상이 되는 CSRF 공격을 방지할 책임이 있습니다.
5.2.1. 외부 토큰 새로 고침
공급자에 로그인하여 생성된 외부 토큰(예: Facebook 또는 GitHub 토큰)을 사용하는 경우 계정을 연결하는 API를 다시 초기화하여 이 토큰을 새로 고칠 수 있습니다.
6장. SPI(Service Provider Interfaces)
Red Hat Single Sign-On은 사용자 정의 코드 없이도 대부분의 사용 사례를 처리하도록 설계되었지만 사용자 정의할 수 있기를 바랍니다. 이를 위해 Red Hat Single Sign-On에는 자체 공급자를 구현할 수 있는 다양한 SPI(서비스 공급자 인터페이스)가 있습니다.
6.1. SPI 구현
SPI를 구현하려면 ProviderFactory 및 Provider 인터페이스를 구현해야 합니다. 서비스 구성 파일도 생성해야 합니다.
예를 들어 Theme Selector SPI를 구현하려면 ThemeSelectorProviderFactory 및 ThemeSelectorProviderProvider 파일을 구현해야 하며 META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory
도 제공합니다.
예제 ThemeSelectorProviderFactory:
package org.acme.provider; import ... public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory { @Override public ThemeSelectorProvider create(KeycloakSession session) { return new MyThemeSelectorProvider(session); } @Override public void init(Config.Scope config) { } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public void close() { } @Override public String getId() { return "myThemeSelector"; } }
package org.acme.provider;
import ...
public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory {
@Override
public ThemeSelectorProvider create(KeycloakSession session) {
return new MyThemeSelectorProvider(session);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "myThemeSelector";
}
}
Red Hat Single Sign-On은 공급자 팩토리의 단일 인스턴스를 생성하여 여러 요청에 대한 상태를 저장할 수 있습니다. 공급자 인스턴스는 각 요청에 대한 팩토리에서 create를 호출하여 생성되므로 경량 개체여야 합니다.
예제 ThemeSelectorProvider:
package org.acme.provider; import ... public class MyThemeSelectorProvider implements ThemeSelectorProvider { public MyThemeSelectorProvider(KeycloakSession session) { } @Override public String getThemeName(Theme.Type type) { return "my-theme"; } @Override public void close() { } }
package org.acme.provider;
import ...
public class MyThemeSelectorProvider implements ThemeSelectorProvider {
public MyThemeSelectorProvider(KeycloakSession session) {
}
@Override
public String getThemeName(Theme.Type type) {
return "my-theme";
}
@Override
public void close() {
}
}
서비스 구성 파일 예 (META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory
):
org.acme.provider.MyThemeSelectorProviderFactory
org.acme.provider.MyThemeSelectorProviderFactory
standalone.xml
,standalone-ha.xml
또는 domain.xml
을 통해 공급자를 구성할 수 있습니다.
예를 들어 standalone.xml
에 다음을 추가하여 다음을 수행합니다.
<spi name="themeSelector"> <provider name="myThemeSelector" enabled="true"> <properties> <property name="theme" value="my-theme"/> </properties> </provider> </spi>
<spi name="themeSelector">
<provider name="myThemeSelector" enabled="true">
<properties>
<property name="theme" value="my-theme"/>
</properties>
</provider>
</spi>
그런 다음 ProviderFactory
init 메서드에서 구성을 검색할 수 있습니다.
public void init(Config.Scope config) { String themeName = config.get("theme"); }
public void init(Config.Scope config) {
String themeName = config.get("theme");
}
필요한 경우 공급자는 다른 공급자를 조회할 수도 있습니다. 예를 들면 다음과 같습니다.
public class MyThemeSelectorProvider implements ThemeSelectorProvider { private KeycloakSession session; public MyThemeSelectorProvider(KeycloakSession session) { this.session = session; } @Override public String getThemeName(Theme.Type type) { return session.getContext().getRealm().getLoginTheme(); } }
public class MyThemeSelectorProvider implements ThemeSelectorProvider {
private KeycloakSession session;
public MyThemeSelectorProvider(KeycloakSession session) {
this.session = session;
}
@Override
public String getThemeName(Theme.Type type) {
return session.getContext().getRealm().getLoginTheme();
}
}
6.1.1. 관리 콘솔에서 SPI 구현의 정보를 표시
경우에 따라 Red Hat Single Sign-On 관리자에게 공급자에 대한 추가 정보를 표시하는 것이 유용합니다. 공급자 빌드 시간 정보(예: 현재 설치된 사용자 정의 공급자의 버전), 공급자의 현재 구성(예: 공급자가 통신하는 원격 시스템의 URL) 또는 일부 운영 정보(프로바이더가 통신하는 원격 시스템의 응답 시간)를 표시할 수 있습니다. Red Hat Single Sign-On 관리 콘솔은 이러한 종류의 정보를 표시할 수 있는 서버 정보 페이지를 제공합니다.
공급자의 정보를 표시하려면 ProviderFactory
에서 org.keycloak.provider.ServerInfoAwareProviderFactory
인터페이스를 구현하는 것으로 충분합니다.
이전 예의 MyThemeSelectorProviderFactory
구현 예:
package org.acme.provider; import ... public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory, ServerInfoAwareProviderFactory { ... @Override public Map<String, String> getOperationalInfo() { Map<String, String> ret = new LinkedHashMap<>(); ret.put("theme-name", "my-theme"); return ret; } }
package org.acme.provider;
import ...
public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory, ServerInfoAwareProviderFactory {
...
@Override
public Map<String, String> getOperationalInfo() {
Map<String, String> ret = new LinkedHashMap<>();
ret.put("theme-name", "my-theme");
return ret;
}
}
6.2. 사용 가능한 공급자 사용
공급자 구현에서는 Red Hat Single Sign-On에서 사용할 수 있는 다른 공급자를 사용할 수 있습니다. 기존 공급자는 일반적으로 SPI 섹션에 설명된 대로 공급자에서 사용할 수 있는 KeycloakSession
을 사용하여 검색할 수 있습니다.
Red Hat Single Sign-On에는 다음 두 가지 공급자 유형이 있습니다.
단일 구현 공급자 유형 - Red Hat Single Sign-On 런타임에 특정 공급자 유형에는 하나의 활성 구현만 있을 수 있습니다.
예를 들어
HostnameProvider
는 Red Hat Single Sign-On에서 사용할 호스트 이름을 지정하고 전체 Red Hat Single Sign-On 서버에 공유하도록 지정합니다. 따라서 이 공급자는 Red Hat Single Sign-On 서버에 대해 활성화된 단일 구현만 있을 수 있습니다. 서버 런타임에 사용할 수 있는 공급자 구현이 여러 개 있는 경우 해당 구현 중 하나를 기본 설정으로 지정해야 합니다.
예를 들면 다음과 같습니다.
<spi name="hostname"> <default-provider>default</default-provider> ... </spi>
<spi name="hostname">
<default-provider>default</default-provider>
...
</spi>
값으로 사용되는 기본값은 특정 공급자 팩토리 구현의 default
-providerProviderFactory.getId()
에서 반환된 ID와 일치해야 합니다. 코드에서 keycloakSession.getProvider(HostnameProvider.class)
와 같은 공급자를 가져올 수 있습니다.
다중 구현 공급자 유형 - 사용 가능한 여러 구현을 허용하고 Red Hat Single Sign-On 런타임에서 함께 작업할 수 있는 공급자 유형입니다.
예를 들어
EventListener
공급자를 사용하면 여러 구현을 사용할 수 있고 등록할 수 있습니다. 즉, 특정 이벤트를 모든 리스너(jboss-logging, sysout 등)로 보낼 수 있습니다. 코드에서는session.getProvider(EventListener.class, "jboss-logging")
와 같이 공급자의 지정된 인스턴스를 가져올 수 있습니다. 위에서 설명한 대로 이 공급자 유형의 여러 인스턴스가 있을 수 있으므로 공급자의provider_id
를 두 번째 인수로 지정해야 합니다.공급자 ID는 특정 공급자 팩토리 구현의
ProviderFactory.getId()
에서 반환된 ID와 일치해야 합니다. 일부 공급자 유형은 두 번째 인수로ComponentModel
을 사용하여 검색할 수 있으며 일부(예:Authenticator
)KeycloakSessionFactory
를 사용하여 검색할 수도 있습니다. 향후 더 이상 사용되지 않을 수 있으므로 자체 공급자를 이러한 방식으로 구현하는 것은 권장되지 않습니다.
6.3. 공급자 구현 등록
공급자 구현을 등록하는 방법은 두 가지가 있습니다. 대부분의 경우 가장 간단한 방법은 여러 종속성을 자동으로 처리하므로 Red Hat Single Sign-On 배포자 접근 방식을 사용하는 것입니다. 핫 배포 및 재배포도 지원합니다.
대체 방법은 모듈로 배포하는 것입니다.
사용자 지정 SPI를 생성하는 경우 모듈로 배포해야 합니다. 그렇지 않으면 Red Hat Single Sign-On 배포자 접근 방식을 사용하는 것이 좋습니다.
6.3.1. Red Hat Single Sign-On 배포자 사용
공급자 ScanSetting을 Red Hat Single Sign-On 독립 실행형/deployments/
디렉터리에 복사하는 경우 공급자가 자동으로 배포됩니다. 핫 디플로이먼트도 작동합니다. 또한 공급자는 jboss-deployment-structure.xml
파일과 같은 기능을 사용할 수 있도록 JBoss EAP 환경에 배포된 다른 구성 요소와 유사하게 작동합니다. 이 파일을 사용하면 다른 구성 요소에 대한 종속성을 설정하고 타사 ScanSetting 및 모듈을 로드할 수 있습니다.
공급자 ScanSetting은 EAR 및 WAR와 같은 다른 배포 가능한 단위에도 포함될 수 있습니다. EAR와 함께 배포하면 실제로 이러한 라이브러리를 EAR의 lib/
디렉토리에 배치할 수 있으므로 타사 ScanSetting을 쉽게 사용할 수 있습니다.
6.3.2. 모듈을 사용하여 공급자 등록
절차
jboss-cli 스크립트를 사용하여 모듈을 생성하거나 수동으로 폴더를 생성합니다.
예를 들어,
jboss-cli
스크립트를 사용하여 이벤트 리스너 sysout 예제 공급자를 추가하려면 다음을 실행합니다.KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.acme.provider --resources=target/provider.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi"
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.acme.provider --resources=target/provider.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi"
Copy to Clipboard Copied! 또는
KEYCLOAK_HOME/modules
내에 모듈을 수동으로 생성하고 ScanSetting 및module.xml
을 추가할 수 있습니다.예를 들어
KEYCLOAK_HOME/modules/org/acme/provider/main
폴더를 생성합니다. 그런 다음provider.jar
를 이 폴더에 복사하고 다음 콘텐츠를 사용하여module.xml
을 만듭니다.<?xml version="1.0" encoding="UTF-8"?> <module xmlns="urn:jboss:module:1.3" name="org.acme.provider"> <resources> <resource-root path="provider.jar"/> </resources> <dependencies> <module name="org.keycloak.keycloak-core"/> <module name="org.keycloak.keycloak-server-spi"/> </dependencies> </module>
<?xml version="1.0" encoding="UTF-8"?> <module xmlns="urn:jboss:module:1.3" name="org.acme.provider"> <resources> <resource-root path="provider.jar"/> </resources> <dependencies> <module name="org.keycloak.keycloak-core"/> <module name="org.keycloak.keycloak-server-spi"/> </dependencies> </module>
Copy to Clipboard Copied!
standalone.xml
,standalone-ha.xml
또는domain.xml
의 keycloak-server 하위 시스템 섹션을 편집하여 Red Hat Single Sign-On에 이 모듈을 등록한 후 공급자에 추가합니다.<subsystem xmlns="urn:jboss:domain:keycloak-server:1.2"> <web-context>auth</web-context> <providers> <provider>module:org.keycloak.examples.event-sysout</provider> </providers> ...
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.2"> <web-context>auth</web-context> <providers> <provider>module:org.keycloak.examples.event-sysout</provider> </providers> ...
Copy to Clipboard Copied!
6.3.3. 공급자 비활성화
standalone.xml
,standalone-ha.xml
또는 domain.xml
에서 공급자의 enabled 특성을 false로 설정하여 공급자를 비활성화할 수 있습니다. 예를 들어 Infinispan 사용자 캐시 공급자를 비활성화하려면 다음을 추가합니다.
<spi name="userCache"> <provider name="infinispan" enabled="false"/> </spi>
<spi name="userCache">
<provider name="infinispan" enabled="false"/>
</spi>
6.4. Leveraging Jakarta EE
서비스 공급자는 공급자를 가리키도록 META-INF/services
파일을 올바르게 설정하는 한 모든 자karta EE 구성 요소 내에 패키징할 수 있습니다. 예를 들어, 공급자가 타사 라이브러리를 사용해야 하는 경우, 공급자를 북마트 내에 패키지하고 이러한 타사 라이브러리를 후자의 lib/
디렉토리에 저장할 수 있습니다. 또한 공급자 ScanSettings는 Egresss, WARS 및 EARs가 JBoss EAP 환경에서 사용할 수 있는 jboss-deployment-structure.xml
파일을 사용할 수 있습니다. 이 파일에 대한 자세한 내용은 JBoss EAP 설명서를 참조하십시오. 이를 통해 다른 미세한 동작 중에서 외부 종속성을 가져올 수 있습니다.
ProviderFactory
구현은 일반 java 오브젝트여야 합니다. 그러나 현재는 공급자 클래스 구현을 Stateful Egresss로 지원합니다. 이렇게 하면 됩니다.
@Stateful @Local(EjbExampleUserStorageProvider.class) public class EjbExampleUserStorageProvider implements UserStorageProvider, UserLookupProvider, UserRegistrationProvider, UserQueryProvider, CredentialInputUpdater, CredentialInputValidator, OnUserCache { @PersistenceContext protected EntityManager em; protected ComponentModel model; protected KeycloakSession session; public void setModel(ComponentModel model) { this.model = model; } public void setSession(KeycloakSession session) { this.session = session; } @Remove @Override public void close() { } ... }
@Stateful
@Local(EjbExampleUserStorageProvider.class)
public class EjbExampleUserStorageProvider implements UserStorageProvider,
UserLookupProvider,
UserRegistrationProvider,
UserQueryProvider,
CredentialInputUpdater,
CredentialInputValidator,
OnUserCache
{
@PersistenceContext
protected EntityManager em;
protected ComponentModel model;
protected KeycloakSession session;
public void setModel(ComponentModel model) {
this.model = model;
}
public void setSession(KeycloakSession session) {
this.session = session;
}
@Remove
@Override
public void close() {
}
...
}
@Local
주석을 정의하고 해당 공급자 클래스를 지정합니다. 이 작업을 수행하지 않으면 NodePort에서 공급자 인스턴스를 올바르게 프록시하지 않으며 공급자가 작동하지 않습니다.
공급자의 close()
메서드에 @Remove
주석을 넣습니다. 그러지 않으면 상태 저장 빈이 정리되지 않으며 결국 오류 메시지가 표시될 수 있습니다.
ProviderFactory
의 Ixmplementations는 일반 java 객체여야 합니다. 팩토리 클래스는 create()
메서드에서 Stateful NodePort의 JNDI 조회를 수행합니다.
public class EjbExampleUserStorageProviderFactory implements UserStorageProviderFactory<EjbExampleUserStorageProvider> { @Override public EjbExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) { try { InitialContext ctx = new InitialContext(); EjbExampleUserStorageProvider provider = (EjbExampleUserStorageProvider)ctx.lookup( "java:global/user-storage-jpa-example/" + EjbExampleUserStorageProvider.class.getSimpleName()); provider.setModel(model); provider.setSession(session); return provider; } catch (Exception e) { throw new RuntimeException(e); } }
public class EjbExampleUserStorageProviderFactory
implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {
@Override
public EjbExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) {
try {
InitialContext ctx = new InitialContext();
EjbExampleUserStorageProvider provider = (EjbExampleUserStorageProvider)ctx.lookup(
"java:global/user-storage-jpa-example/" + EjbExampleUserStorageProvider.class.getSimpleName());
provider.setModel(model);
provider.setSession(session);
return provider;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
6.5. JavaScript 공급자
Red Hat Single Sign-On에서는 관리자가 특정 기능을 사용자 지정할 수 있도록 런타임 중 스크립트를 실행할 수 있습니다.
- Authenticator
- JavaScript Policy
- OpenID Connect 프로토콜 맵퍼
- SAML 프로토콜 맵퍼
6.5.1. Authenticator
인증 스크립트는 다음 기능 중 하나를 제공해야 합니다. authenticate(..)
..은 Authenticator#authenticate(AuthenticationFlowContext)
작업(...)
에서 호출되며 Authenticator#action(AuthenticationFlowContext)
에서 호출됩니다.
사용자 정의 Authenticator
는 최소한 authenticate(..)
함수를 제공해야 합니다. 코드 내에서 javax.script.Bindings
스크립트를 사용할 수 있습니다.
script
-
스크립트 메타데이터에 액세스하기 위한
ScriptModel
realm
-
제품
상세 정보
user
-
현재
UserModel
세션
-
활성
KeycloakSession
authenticationSession
-
현재
AuthenticationSessionModel
httpRequest
-
the current
org.jboss.resteasy.spi.HttpRequest
LOG
-
org.jboss.logging.Logger
를ScriptBasedAuthenticator
로 지정
authenticate(context)
action(
함수에 전달된 컨텍스트 인수에서 추가 컨텍스트 정보를 추출할 수 있습니다.
context
)
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError"); function authenticate(context) { LOG.info(script.name + " --> trace auth for: " + user.username); if ( user.username === "tester" && user.getAttribute("someAttribute") && user.getAttribute("someAttribute").contains("someValue")) { context.failure(AuthenticationFlowError.INVALID_USER); return; } context.success(); }
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
function authenticate(context) {
LOG.info(script.name + " --> trace auth for: " + user.username);
if ( user.username === "tester"
&& user.getAttribute("someAttribute")
&& user.getAttribute("someAttribute").contains("someValue")) {
context.failure(AuthenticationFlowError.INVALID_USER);
return;
}
context.success();
}
6.5.2. 배포할 스크립트를 사용하여 JAR 생성
JAR 파일은 .jar
확장자가 있는 일반 ZIP 파일입니다.
Red Hat Single Sign-On에서 스크립트를 사용할 수 있도록 하려면 서버에 배포해야 합니다. 이를 위해 다음 구조를 사용하여 JAR
파일을 생성해야 합니다.
META-INF/keycloak-scripts.json my-script-authenticator.js my-script-policy.js my-script-mapper.js
META-INF/keycloak-scripts.json
my-script-authenticator.js
my-script-policy.js
my-script-mapper.js
META-INF/keycloak-scripts.json
은 배포하려는 스크립트에 대한 메타데이터 정보를 제공하는 파일 설명자입니다. 다음 구조가 포함된 JSON 파일입니다.
{ "authenticators": [ { "name": "My Authenticator", "fileName": "my-script-authenticator.js", "description": "My Authenticator from a JS file" } ], "policies": [ { "name": "My Policy", "fileName": "my-script-policy.js", "description": "My Policy from a JS file" } ], "mappers": [ { "name": "My Mapper", "fileName": "my-script-mapper.js", "description": "My Mapper from a JS file" } ], "saml-mappers": [ { "name": "My Mapper", "fileName": "my-script-mapper.js", "description": "My Mapper from a JS file" } ] }
{
"authenticators": [
{
"name": "My Authenticator",
"fileName": "my-script-authenticator.js",
"description": "My Authenticator from a JS file"
}
],
"policies": [
{
"name": "My Policy",
"fileName": "my-script-policy.js",
"description": "My Policy from a JS file"
}
],
"mappers": [
{
"name": "My Mapper",
"fileName": "my-script-mapper.js",
"description": "My Mapper from a JS file"
}
],
"saml-mappers": [
{
"name": "My Mapper",
"fileName": "my-script-mapper.js",
"description": "My Mapper from a JS file"
}
]
}
이 파일은 배포하려는 다양한 유형의 스크립트 공급자를 참조해야 합니다.
Authenticators
OpenID Connect 스크립트 Authenticator의 경우. 동일한 JAR 파일에 하나 또는 여러 개의 인증자가 있을 수 있습니다.
Policies
Red Hat Single Sign-On 권한 부여 서비스를 사용할 때 JavaScript 정책의 경우 동일한 JAR 파일에 하나 이상의 정책을 가질 수 있습니다.
매퍼
OpenID Connect 스크립트 프로토콜 맵퍼의 경우. 동일한 JAR 파일에 하나 또는 여러 개의 매퍼가 있을 수 있습니다.
SAML-mappers
SAML 스크립트 프로토콜 맵퍼의 경우. 동일한 JAR 파일에 하나 또는 여러 개의 매퍼가 있을 수 있습니다.
JAR
파일의 각 스크립트 파일에 대해 스크립트 파일을 특정 공급자 유형에 매핑하는 META-INF/keycloak-scripts.json
에 해당 항목이 필요합니다. 이를 위해 각 항목에 대해 다음 속성을 제공해야 합니다.
name
Red Hat Single Sign-On 관리 콘솔을 통해 스크립트를 표시하는 데 사용되는 친숙한 이름입니다. 지정하지 않으면 스크립트 파일의 이름이 대신 사용됩니다.
description
스크립트 파일의 의도를 더 잘 설명하는 선택적 텍스트
fileName
스크립트 파일의 이름입니다. 이 속성은 필수 이며 JAR 내에서 파일에 매핑해야 합니다.
6.5.3. 스크립트 JAR 배포
설명자가 있고 배포하려는 스크립트가 있는 JAR 파일이 있으면 JAR 파일을 Red Hat Single Sign-On 독립 실행형/deployments/
디렉터리에 복사하면 됩니다.
6.5.3.1. Java 15 이상에 스크립트 엔진 배포
스크립트를 실행하려면 JavaScript 공급자에게 Java 애플리케이션에서 JavaScript 엔진을 사용할 수 있어야 합니다. Java 14 이하 버전에는 Nashorn JavaScript 엔진이 포함되어 있습니다. Java 자체의 일부로 자동으로 사용할 수 있으며 JavaScript 공급자는 기본적으로 이 스크립트 엔진을 사용할 수 있습니다. 그러나 Java 15 이상 버전의 경우 스크립트 엔진은 Java 자체의 일부가 아닙니다. Red Hat Single Sign-On에는 기본적으로 스크립트 엔진이 없으므로 서버에 추가해야 합니다. Java 15 이상 버전에서는 스크립트 공급자를 배포할 때 추가 단계가 필요합니다 - 배포에 선택한 스크립트 엔진을 추가합니다.
스크립트 엔진을 사용할 수 있습니다. 그러나 Nashorn JavaScript 엔진 만 테스트합니다. 다음 단계에서는 이 엔진을 사용하는 것으로 가정합니다.
새 모듈 nashorn-core를 Red Hat Single Sign-On에 추가하여 스크립트 엔진을 설치할 수 있습니다. 서버가 시작된 후 KEYCLOAK_HOME/bin
디렉터리에서 이와 유사한 명령을 실행할 수 있습니다.
export NASHORN_VERSION=15.3 wget https://repo1.maven.org/maven2/org/openjdk/nashorn/nashorn-core/$NASHORN_VERSION/nashorn-core-$NASHORN_VERSION.jar ./jboss-cli.sh -c --command="module add --module-root-dir=../modules/system/layers/keycloak/ --name=org.openjdk.nashorn.nashorn-core --resources=./nashorn-core-$NASHORN_VERSION.jar --dependencies=asm.asm,jdk.dynalink" rm nashorn-core-$NASHORN_VERSION.jar
export NASHORN_VERSION=15.3
wget https://repo1.maven.org/maven2/org/openjdk/nashorn/nashorn-core/$NASHORN_VERSION/nashorn-core-$NASHORN_VERSION.jar
./jboss-cli.sh -c --command="module add --module-root-dir=../modules/system/layers/keycloak/ --name=org.openjdk.nashorn.nashorn-core --resources=./nashorn-core-$NASHORN_VERSION.jar --dependencies=asm.asm,jdk.dynalink"
rm nashorn-core-$NASHORN_VERSION.jar
공급자를 다른 모듈에 설치하려면 기본 스크립팅 공급자의 구성 속성 script-engine-module
을 사용할 수 있습니다. 예를 들어 KEYCLOAK_HOME/standalone/configuration/standalone-*.xml
파일에서 다음과 같은 항목을 사용할 수 있습니다.
<spi name="scripting"> <provider name="default" enabled="true"> <properties> <property name="script-engine-module" value="org.graalvm.js.js-scriptengine"/> </properties> </provider> </spi>
<spi name="scripting">
<provider name="default" enabled="true">
<properties>
<property name="script-engine-module" value="org.graalvm.js.js-scriptengine"/>
</properties>
</provider>
</spi>
6.6. 사용 가능한 SPI
런타임 시 사용 가능한 모든 SPI 목록을 보려면 관리 콘솔 섹션에 설명된 대로 Admin Console 의 Server Info
페이지를 확인할 수 있습니다.
7장. 사용자 스토리지 SPI
User Storage SPI를 사용하여 Red Hat Single Sign-On에 확장을 작성하여 외부 사용자 데이터베이스 및 자격 증명 저장소에 연결할 수 있습니다. 기본 제공 LDAP 및 ActiveDirectory 지원은 이 SPI를 구현하는 것입니다. Red Hat Single Sign-On은 기본적으로 로컬 데이터베이스를 사용하여 사용자를 생성, 업데이트 및 조회하고 자격 증명을 검증합니다. 하지만 조직에서 Red Hat Single Sign-On의 데이터 모델로 마이그레이션할 수 없는 기존 외부 독점 사용자 데이터베이스가 있는 경우가 많습니다. 이러한 상황에서 애플리케이션 개발자는 User Storage SPI 구현을 작성하여 Red Hat Single Sign-On이 사용자에게 로그인하고 관리하는 데 사용하는 내부 사용자 개체 모델 및 외부 사용자 저장소를 연결할 수 있습니다.
Red Hat Single Sign-On 런타임에서 사용자를 검색해야 하는 경우(예: 사용자가 로그인할 때) 사용자를 찾으려면 여러 단계를 수행합니다. 먼저 사용자가 사용자 캐시에 있는지 확인합니다. 사용자가 발견되면 메모리 내 표현을 사용합니다. 그런 다음 Red Hat Single Sign-On 로컬 데이터베이스 내에서 사용자를 찾습니다. 사용자를 찾을 수 없는 경우 사용자 스토리지 SPI 공급자 구현을 반복하여 런타임 중 원하는 사용자를 반환할 때까지 사용자 쿼리를 수행합니다. 공급자는 외부 사용자 저장소에서 사용자를 쿼리하고 사용자의 외부 데이터 표현을 Red Hat Single Sign-On의 사용자 메타 모델에 매핑합니다.
또한 사용자 스토리지 SPI 공급자 구현은 복잡한 기준 쿼리를 수행하거나, 사용자에 대한 CRUD 작업을 수행하거나, 자격 증명을 검증 및 관리하거나, 여러 사용자의 대규모 업데이트를 한 번에 수행할 수 있습니다. 이는 외부 저장소의 기능에 따라 달라집니다.
사용자 스토리지 SPI 공급자 구현은 패키지화되어 자karta EE 구성 요소와 유사하게 배포됩니다. 기본적으로 활성화되어 있지 않지만 관리 콘솔의 User Federation
탭에서 영역별로 활성화 및 구성해야 합니다.
사용자 공급자 구현에서 일부 사용자 속성을 사용자 ID 연결/설정을 위한 메타데이터 속성으로 사용하는 경우 사용자가 속성을 편집할 수 없고 해당 속성이 읽기 전용인지 확인하십시오. 예시는 LDAP_ID
속성으로, 기본 제공 Red Hat Single Sign-On LDAP 공급자가 LDAP 서버 측에 사용자 ID를 저장하기 위해 사용하고 있는 LDAP_ID 속성입니다. 위협 모델 완화 장에서 자세한 내용을 참조하십시오.
7.1. 공급자 인터페이스
User Storage SPI 구현을 빌드할 때 공급자 클래스 및 공급자 팩토리를 정의해야 합니다. 공급자 클래스 인스턴스는 공급자 팩토리에 의해 트랜잭션별로 생성됩니다. 공급자 클래스는 사용자 조회 및 기타 사용자 작업의 과도한 리프팅을 수행합니다. org.keycloak.storage.UserStorageProvider
인터페이스를 구현해야 합니다.
package org.keycloak.storage; public interface UserStorageProvider extends Provider { /** * Callback when a realm is removed. Implement this if, for example, you want to do some * cleanup in your user storage when a realm is removed * * @param realm */ default void preRemove(RealmModel realm) { } /** * Callback when a group is removed. Allows you to do things like remove a user * group mapping in your external store if appropriate * * @param realm * @param group */ default void preRemove(RealmModel realm, GroupModel group) { } /** * Callback when a role is removed. Allows you to do things like remove a user * role mapping in your external store if appropriate * @param realm * @param role */ default void preRemove(RealmModel realm, RoleModel role) { } }
package org.keycloak.storage;
public interface UserStorageProvider extends Provider {
/**
* Callback when a realm is removed. Implement this if, for example, you want to do some
* cleanup in your user storage when a realm is removed
*
* @param realm
*/
default
void preRemove(RealmModel realm) {
}
/**
* Callback when a group is removed. Allows you to do things like remove a user
* group mapping in your external store if appropriate
*
* @param realm
* @param group
*/
default
void preRemove(RealmModel realm, GroupModel group) {
}
/**
* Callback when a role is removed. Allows you to do things like remove a user
* role mapping in your external store if appropriate
* @param realm
* @param role
*/
default
void preRemove(RealmModel realm, RoleModel role) {
}
}
UserStorageProvider
인터페이스가 상당히 스파스라고 생각할 수 있습니까? 이 장의 뒷부분에서 공급자 클래스에서 사용자 통합의류를 지원하기 위해 구현할 수 있는 다른 혼합 인터페이스가 있음을 확인할 수 있습니다.
UserStorageProvider
인스턴스는 트랜잭션당 한 번 생성됩니다. 트랜잭션이 완료되면 UserStorageProvider.close()
메서드가 호출되고 인스턴스가 가비지 수집됩니다. 인스턴스는 공급자 팩토리에서 생성합니다. 공급자 팩토리에서 org.keycloak.storage.UserStorageProviderFactory
인터페이스를 구현합니다.
package org.keycloak.storage; /** * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ public interface UserStorageProviderFactory<T extends UserStorageProvider> extends ComponentFactory<T, UserStorageProvider> { /** * This is the name of the provider and will be shown in the admin console as an option. * * @return */ @Override String getId(); /** * called per Keycloak transaction. * * @param session * @param model * @return */ T create(KeycloakSession session, ComponentModel model); ... }
package org.keycloak.storage;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserStorageProviderFactory<T extends UserStorageProvider> extends ComponentFactory<T, UserStorageProvider> {
/**
* This is the name of the provider and will be shown in the admin console as an option.
*
* @return
*/
@Override
String getId();
/**
* called per Keycloak transaction.
*
* @param session
* @param model
* @return
*/
T create(KeycloakSession session, ComponentModel model);
...
}
공급자 팩토리 클래스는 UserStorageProviderFactory
를 구현할 때 구체적인 공급자 클래스를 템플릿 매개변수로 지정해야 합니다. 런타임에서 이 클래스를 인트로스펙션하여 해당 기능(구현하는 다른 인터페이스)을 검사하므로 필수입니다. 예를 들어 공급자 클래스 이름이 FileProvider
인 경우 factory 클래스는 다음과 같아야 합니다.
public class FileProviderFactory implements UserStorageProviderFactory<FileProvider> { public String getId() { return "file-provider"; } public FileProvider create(KeycloakSession session, ComponentModel model) { ... }
public class FileProviderFactory implements UserStorageProviderFactory<FileProvider> {
public String getId() { return "file-provider"; }
public FileProvider create(KeycloakSession session, ComponentModel model) {
...
}
getId()
메서드는 User Storage 공급자의 이름을 반환합니다. 이 ID는 특정 영역에 대한 공급자를 활성화하려는 경우 관리 콘솔의 사용자 페더 페이지에 표시됩니다.
create()
메서드는 공급자 클래스의 인스턴스를 할당합니다. org.keycloak.models.KeycloakSession
매개변수를 사용합니다. 이 오브젝트는 다른 정보 및 메타데이터를 조회하고 런타임 내의 다른 다양한 구성 요소에 대한 액세스를 제공하는 데 사용할 수 있습니다. ComponentModel
매개변수는 특정 영역 내에서 공급자가 활성화 및 구성된 방법을 나타냅니다. 여기에는 활성화된 공급자의 인스턴스 ID와 관리 콘솔을 통해 활성화할 때 지정할 수 있는 구성이 포함되어 있습니다.
UserStorageProviderFactory
에는 다른 기능이 있으며 이 기능은 이 장의 뒷부분에서 설명합니다.
7.2. 공급자 기능 인터페이스
UserStorageProvider
인터페이스를 자세히 검토한 경우 사용자를 검색하거나 관리하기 위한 방법을 정의하지 않을 수 있습니다. 이러한 방법은 외부 사용자 저장소가 제공하고 실행할 수 있는 기능의 범위에 따라 다른 기능 인터페이스에서 실제로 정의됩니다. 예를 들어 일부 외부 저장소는 읽기 전용이며 간단한 쿼리 및 인증 정보 검증만 수행할 수 있습니다. 기능 인터페이스를 구현하면 사용자가 수행할 수 있는 기능의 인터페이스 만 구현할 수 있습니다. 이러한 인터페이스를 구현할 수 있습니다.
SPI | 설명 |
---|---|
| 이 인터페이스는 이 외부 저장소에서 사용자로 로그인할 수 있도록 하려면 필요합니다. 대부분의(모든?) 공급자는 이 인터페이스를 구현합니다. |
| 하나 이상의 사용자를 찾는 데 사용되는 복잡한 쿼리를 정의합니다. 관리 콘솔에서 사용자를 보고 관리하려면 이 인터페이스를 구현해야 합니다. |
| 공급자가 사용자 추가 및 제거를 지원하는 경우 이 인터페이스를 구현합니다. |
| 공급자가 일련의 사용자 집합에 대한 대규모 업데이트를 지원하는 경우 이 인터페이스를 구현합니다. |
| 공급자가 하나 이상의 다른 인증 정보 유형을 검증할 수 있는 경우(예: 공급자가 암호를 검증할 수 있는 경우) 이 인터페이스를 구현합니다. |
| 공급자가 하나 이상의 다른 인증 정보 유형 업데이트를 지원하는 경우 이 인터페이스를 구현합니다. |
7.3. 모델 인터페이스
기능 인터페이스에 정의된 대부분의 메서드가 반환 또는 사용자의 표현으로 전달됩니다. 이러한 표현은 org.keycloak.models.UserModel
인터페이스에 의해 정의됩니다. 앱 개발자는 이 인터페이스를 구현해야 합니다. Red Hat Single Sign-On에서 사용하는 외부 사용자 저장소와 사용자 metamodel 간의 매핑을 제공합니다.
package org.keycloak.models; public interface UserModel extends RoleMapperModel { String getId(); String getUsername(); void setUsername(String username); String getFirstName(); void setFirstName(String firstName); String getLastName(); void setLastName(String lastName); String getEmail(); void setEmail(String email); ... }
package org.keycloak.models;
public interface UserModel extends RoleMapperModel {
String getId();
String getUsername();
void setUsername(String username);
String getFirstName();
void setFirstName(String firstName);
String getLastName();
void setLastName(String lastName);
String getEmail();
void setEmail(String email);
...
}
UserModel
구현에서는 사용자 이름, 이름, 이메일, 역할 및 그룹 매핑과 같은 기타 임의의 특성을 포함하여 사용자에 대한 메타데이터 읽기 및 업데이트에 대한 액세스 권한을 제공합니다.
org.keycloak.models
패키지에는 Red Hat Single Sign-On 메타 모델의 다른 부분, 즉Role
의 다른 부분을 나타내는 다른 모델 클래스가 있습니다.
Model
, RoleModel,GroupModel
및 ClientModel
7.3.1. 스토리지 ID
UserModel
의 중요한 방법 중 하나는 getId()
메서드입니다. UserModel
개발자를 구현하는 경우 사용자 ID 형식을 알고 있어야 합니다. 형식은 다음과 같아야 합니다.
"f:" + component id + ":" + external id
"f:" + component id + ":" + external id
Red Hat Single Sign-On 런타임은 사용자 ID로 사용자를 조회해야 하는 경우가 많습니다. 사용자 ID에는 충분한 정보가 포함되어 있으므로 런타임에서 사용자를 찾기 위해 시스템의 모든 UserStorageProvider
를 쿼리할 필요가 없습니다.
구성 요소 ID는 ComponentModel.getId()
에서 반환된 ID입니다. ComponentModel
은 공급자 클래스를 생성할 때 매개 변수로 전달되므로 여기에서 가져올 수 있습니다. 외부 ID는 공급자 클래스에서 외부 저장소에서 사용자를 찾는 데 필요한 정보입니다. 이는 종종 사용자 이름 또는 uid입니다. 예를 들면 다음과 같습니다.
f:332a234e31234:wburke
f:332a234e31234:wburke
런타임에서 id로 조회를 수행하는 경우 ID는 구성 요소 ID를 가져오기 위해 구문 분석됩니다. 구성 요소 ID는 원래 사용자를 로드하는 데 사용된 UserStorageProvider
를 찾는 데 사용됩니다. 그러면 해당 공급자가 ID를 전달합니다. 공급자는 다시 외부 ID를 가져오기 위해 ID를 구문 분석하고, 을 사용하여 외부 사용자 스토리지에서 사용자를 찾습니다.
7.4. 패키징 및 배포
Red Hat Single Sign-On이 공급자를 인식하려면 JAR: META-INF/services/org.keycloak.storage.UserStorageProviderFactory
에 파일을 추가해야 합니다. 이 파일에는 UserStorageProviderFactory
구현의 정규화된 클래스 이름 목록이 포함되어야 합니다.
org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
이 ScanSetting을 배포하려면 독립 실행형/deployments/
디렉토리에 복사하십시오. === Simple read-only, lookup 예제
User Storage SPI 구현의 기본 사항을 설명하기 위해 간단한 예제를 살펴보겠습니다. 이 장에서는 간단한 속성 파일에서 사용자를 조회하는 간단한 UserStorageProvider
의 구현을 확인할 수 있습니다. 속성 파일에는 사용자 이름 및 암호 정의가 포함되어 있으며 classpath의 특정 위치로 하드 코딩됩니다. 공급자는 ID와 사용자 이름으로 사용자를 검색하고 암호를 검증할 수도 있습니다. 이 공급자에서 시작된 사용자는 읽기 전용입니다.
7.4.1. 공급자 클래스
첫 번째는 UserStorageProvider
클래스입니다.
public class PropertyFileUserStorageProvider implements UserStorageProvider, UserLookupProvider, CredentialInputValidator, CredentialInputUpdater { ... }
public class PropertyFileUserStorageProvider implements
UserStorageProvider,
UserLookupProvider,
CredentialInputValidator,
CredentialInputUpdater
{
...
}
당사의 공급자 클래스인 PropertyFileUserStorageProvider
는 많은 인터페이스를 구현합니다. 이는 SPI의 기본 요구 사항인 UserStorageProvider
를 구현합니다. 이 공급자가 저장한 사용자로 로그인할 수 있도록 하려면 UserLookupProvider
인터페이스를 구현합니다. 로그인 화면을 사용하여 에 입력한 암호를 검증할 수 있으므로 CredentialInputValidator
인터페이스를 구현합니다. 속성 파일은 읽기 전용입니다. CredentialInputUpdater
를 구현하는데, 이는 사용자가 암호를 업데이트하려고 할 때 오류 조건을 게시하려고 하기 때문입니다.
protected KeycloakSession session; protected Properties properties; protected ComponentModel model; // map of loaded users in this transaction protected Map<String, UserModel> loadedUsers = new HashMap<>(); public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) { this.session = session; this.model = model; this.properties = properties; }
protected KeycloakSession session;
protected Properties properties;
protected ComponentModel model;
// map of loaded users in this transaction
protected Map<String, UserModel> loadedUsers = new HashMap<>();
public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) {
this.session = session;
this.model = model;
this.properties = properties;
}
이 공급자 클래스의 생성자는 KeycloakSession
,ComponentModel
및 속성 파일에 대한 참조를 저장합니다. 나중에 이 모든 것을 사용할 것입니다. 또한 로드된 사용자 맵이 있습니다. 사용자가 이 맵에 저장할 때마다 동일한 트랜잭션 내에서 다시 생성하지 않도록 합니다. 이 방법은 많은 공급업체를 따라야 하는 것이 좋습니다(즉, JPA와 통합된 모든 공급업체). 공급자 클래스 인스턴스는 트랜잭션당 한 번 생성되고 트랜잭션이 완료된 후 닫힙니다.
7.4.1.1. UserLookupProvider 구현
@Override public UserModel getUserByUsername(String username, RealmModel realm) { UserModel adapter = loadedUsers.get(username); if (adapter == null) { String password = properties.getProperty(username); if (password != null) { adapter = createAdapter(realm, username); loadedUsers.put(username, adapter); } } return adapter; } protected UserModel createAdapter(RealmModel realm, String username) { return new AbstractUserAdapter(session, realm, model) { @Override public String getUsername() { return username; } }; } @Override public UserModel getUserById(String id, RealmModel realm) { StorageId storageId = new StorageId(id); String username = storageId.getExternalId(); return getUserByUsername(username, realm); } @Override public UserModel getUserByEmail(String email, RealmModel realm) { return null; }
@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
UserModel adapter = loadedUsers.get(username);
if (adapter == null) {
String password = properties.getProperty(username);
if (password != null) {
adapter = createAdapter(realm, username);
loadedUsers.put(username, adapter);
}
}
return adapter;
}
protected UserModel createAdapter(RealmModel realm, String username) {
return new AbstractUserAdapter(session, realm, model) {
@Override
public String getUsername() {
return username;
}
};
}
@Override
public UserModel getUserById(String id, RealmModel realm) {
StorageId storageId = new StorageId(id);
String username = storageId.getExternalId();
return getUserByUsername(username, realm);
}
@Override
public UserModel getUserByEmail(String email, RealmModel realm) {
return null;
}
getUserByUsername()
메서드는 사용자가 로그인할 때 Red Hat Single Sign-On 로그인 페이지에서 호출합니다. 구현에서 먼저 로드된Users
맵을 확인하여 사용자가 이 트랜잭션 내에 이미 로드되었는지 확인합니다. 로드되지 않은 경우 사용자 이름의 속성 파일을 찾습니다. UserModel
의 구현을 생성하고 나중에 참조할 수 있도록 Load Users
에 저장한 후 이 인스턴스를 반환합니다.
createAdapter()
메서드는 helper 클래스 org.keycloak.storage.adapter.AbstractUserAdapter
를 사용합니다. UserModel
에 대한 기본 구현을 제공합니다. 사용자 이름을 외부 ID로 사용하여 필요한 스토리지 ID 형식을 기반으로 사용자 ID를 자동으로 생성합니다.
"f:" + component id + ":" + username
"f:" + component id + ":" + username
AbstractUserAdapter
의 모든 get 메서드는 null 또는 빈 컬렉션을 반환합니다. 그러나 역할 및 그룹 매핑을 반환하는 방법은 모든 사용자의 영역에 대해 구성된 기본 역할 및 그룹을 반환합니다. AbstractUserAdapter
의 모든 세트 메서드에서 org.keycloak.storage.ReadOnlyException
이 발생합니다. 따라서 관리 콘솔에서 사용자를 수정하려고 하면 오류가 발생합니다.
getUserById()
메서드는 org.keycloak.storage.StorageId
도우미 클래스를 사용하여 id
매개변수를 구문 분석합니다. StorageId.getExternalId()
메서드를 호출하여 id
매개변수에 포함된 사용자 이름을 가져옵니다. 그러면 메서드가 getUserByUsername()
에 위임됩니다.
이메일은 저장되지 않으므로 getUserByEmail()
메서드는 null을 반환합니다.
7.4.1.2. CredentialInputValidator 구현
다음으로 CredentialInputValidator
에 대한 메서드 구현을 살펴보겠습니다.
@Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { String password = properties.getProperty(user.getUsername()); return credentialType.equals(PasswordCredentialModel.TYPE) && password != null; } @Override public boolean supportsCredentialType(String credentialType) { return credentialType.equals(PasswordCredentialModel.TYPE); } @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { if (!supportsCredentialType(input.getType())) return false; String password = properties.getProperty(user.getUsername()); if (password == null) return false; return password.equals(input.getChallengeResponse()); }
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
String password = properties.getProperty(user.getUsername());
return credentialType.equals(PasswordCredentialModel.TYPE) && password != null;
}
@Override
public boolean supportsCredentialType(String credentialType) {
return credentialType.equals(PasswordCredentialModel.TYPE);
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType())) return false;
String password = properties.getProperty(user.getUsername());
if (password == null) return false;
return password.equals(input.getChallengeResponse());
}
isConfiguredFor()
메서드를 런타임에서 호출하여 사용자에게 특정 인증 정보 유형이 구성되었는지 확인합니다. 이 방법은 암호가 사용자에 대해 설정되어 있는지 확인합니다.
supportsCredentialType()
메서드는 특정 인증 정보 유형에 대해 검증이 지원되는지 여부를 반환합니다. 인증 정보 유형이 password
인지 확인합니다.
isValid()
메서드는 암호의 유효성을 검사합니다. CredentialInput
매개변수는 실제로 모든 인증 정보 유형에 대한 추상 인터페이스일 뿐입니다. 인증 정보 유형을 지원하고 UserCredentialModel
의 인스턴스이기도 합니다. 사용자가 로그인 페이지에서 로그인하면 암호 입력의 일반 텍스트가 UserCredentialModel
의 인스턴스에 배치됩니다. isValid()
메서드는 속성 파일에 저장된 일반 텍스트 암호에 대해 이 값을 확인합니다. 반환 값이 true
이면 암호가 유효함을 의미합니다.
7.4.1.3. CredentialInputUpdater 구현
앞에서 언급했듯이 CredentialInputUpdater
인터페이스를 구현하는 유일한 이유는 사용자 암호를 수정하지 않는 것입니다. 그렇지 않으면 런타임에서 Red Hat Single Sign-On 로컬 스토리지에서 암호를 덮어쓸 수 있기 때문입니다. 이 장의 뒷부분에서 이에 대해 자세히 살펴보겠습니다.
@Override public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { if (input.getType().equals(PasswordCredentialModel.TYPE)) throw new ReadOnlyException("user is read only for this update"); return false; } @Override public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { } @Override public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) { return Collections.EMPTY_SET; }
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (input.getType().equals(PasswordCredentialModel.TYPE)) throw new ReadOnlyException("user is read only for this update");
return false;
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return Collections.EMPTY_SET;
}
updateCredential()
메서드는 인증 정보 유형이 password인지 확인하기만 합니다. 이 경우 ReadOnlyException
이 throw됩니다.
7.4.2. 공급자 팩토리 구현
이제 공급자 클래스가 완료되었으므로 이제 공급자 팩토리 클래스에 주의를 기울입니다.
public class PropertyFileUserStorageProviderFactory implements UserStorageProviderFactory<PropertyFileUserStorageProvider> { public static final String PROVIDER_NAME = "readonly-property-file"; @Override public String getId() { return PROVIDER_NAME; }
public class PropertyFileUserStorageProviderFactory
implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
public static final String PROVIDER_NAME = "readonly-property-file";
@Override
public String getId() {
return PROVIDER_NAME;
}
먼저 알아두어야 할 것은 UserStorageProviderFactory
클래스를 구현할 때 결정적 공급자 클래스 구현을 템플릿 매개변수로 전달해야 한다는 것입니다. 여기에서 이전에 정의한 공급자 클래스를 지정합니다. PropertyFileUserStorageProvider
.
template 매개변수를 지정하지 않으면 공급자가 작동하지 않습니다. 런타임은 클래스 인트로스펙션을 수행하여 공급자가 구현하는 기능 인터페이스를 결정합니다.
getId()
메서드는 런타임에서 팩토리를 식별하고 영역의 사용자 스토리지 공급자를 활성화하려는 경우 admin 콘솔에 표시된 문자열이기도 합니다.
7.4.2.1. 초기화
private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class); protected Properties properties = new Properties(); @Override public void init(Config.Scope config) { InputStream is = getClass().getClassLoader().getResourceAsStream("/users.properties"); if (is == null) { logger.warn("Could not find users.properties in classpath"); } else { try { properties.load(is); } catch (IOException ex) { logger.error("Failed to load users.properties file", ex); } } } @Override public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) { return new PropertyFileUserStorageProvider(session, model, properties); }
private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class);
protected Properties properties = new Properties();
@Override
public void init(Config.Scope config) {
InputStream is = getClass().getClassLoader().getResourceAsStream("/users.properties");
if (is == null) {
logger.warn("Could not find users.properties in classpath");
} else {
try {
properties.load(is);
} catch (IOException ex) {
logger.error("Failed to load users.properties file", ex);
}
}
}
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new PropertyFileUserStorageProvider(session, model, properties);
}
UserStorageProviderFactory
인터페이스에는 구현할 수 있는 선택적 init()
메서드가 있습니다. Red Hat Single Sign-On이 부팅되면 각 공급자 팩토리의 인스턴스 한 개만 생성됩니다. 또한 부팅 시 init()
메서드가 이러한 팩토리 인스턴스마다 호출됩니다. postInit()
도 구현할 수 있습니다. 각 팩토리의 init()
메서드가 호출되면 해당 postInit()
메서드가 호출됩니다.
init()
메서드 구현에서는 classpath에서 사용자 선언이 포함된 속성 파일을 찾습니다. 그런 다음 저장된 사용자 이름 및 암호 조합으로 properties
필드를 로드합니다.
Config.Scope
매개변수는 서버 구성을 통해 구성된 팩토리 구성입니다.
예를 들어 standalone.xml
에 다음을 추가하여 다음을 수행합니다.
<spi name="storage"> <provider name="readonly-property-file" enabled="true"> <properties> <property name="path" value="/other-users.properties"/> </properties> </provider> </spi>
<spi name="storage">
<provider name="readonly-property-file" enabled="true">
<properties>
<property name="path" value="/other-users.properties"/>
</properties>
</provider>
</spi>
사용자 속성 파일의 클래스 경로를 하드 코딩하지 않고 지정할 수 있습니다. 그런 다음 PropertyFileUserStorageProviderFactory.init()
에서 구성을 검색할 수 있습니다.
public void init(Config.Scope config) { String path = config.get("path"); InputStream is = getClass().getClassLoader().getResourceAsStream(path); ... }
public void init(Config.Scope config) {
String path = config.get("path");
InputStream is = getClass().getClassLoader().getResourceAsStream(path);
...
}
7.4.2.2. 메서드 생성
공급자 팩토리를 생성하는 마지막 단계는 create()
메서드입니다.
@Override public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) { return new PropertyFileUserStorageProvider(session, model, properties); }
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new PropertyFileUserStorageProvider(session, model, properties);
}
PropertyFileUserStorageProvider
클래스를 할당하기만 하면 됩니다. 이 생성 방법은 트랜잭션당 한 번 호출됩니다.
7.4.3. 패키징 및 배포
공급자 구현을 위한 클래스 파일은 ScanSetting에 배치해야 합니다. 또한 META-INF/services/org.keycloak.storage.UserStorageProviderFactory
파일에서 공급자 팩토리 클래스를 선언해야 합니다.
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
이 ScanSetting을 배포하려면 standalone/deployments/
디렉터리에 복사합니다.
7.4.4. 관리 콘솔에서 공급자 활성화
관리 콘솔의 사용자 페더레이션 페이지에서 영역당 사용자 스토리지 공급자를 활성화합니다.
절차
목록에서 방금 만든 공급자를 선택합니다.
readonly-property-file
.해당 공급자의 구성 페이지가 표시됩니다.
- 구성할 항목이 없으므로 저장 을 클릭합니다.
기본 사용자 페더레이션 페이지로 돌아가기
이제 공급자가 표시됩니다.
이제 users.properties
파일에 선언된 사용자로 로그인할 수 있습니다. 이 사용자는 로그인 후 계정 페이지를 볼 수 있습니다.
7.5. 구성 기술
PropertyFileUserStorageProvider
예제가 약간 사용됩니다. 이는 공급자의 ScanSetting에 포함된 속성 파일로 하드 코딩되며, 이는 매우 유용하지 않습니다. 공급자의 인스턴스별로 구성 가능한 이 파일의 위치를 만들 수 있습니다. 즉, 이 공급자를 여러 영역에서 여러 번 재사용하고 완전히 다른 사용자 속성 파일을 가리킬 수 있습니다. 또한 Admin Console UI에서 이 구성을 수행해야 합니다.
UserStorageProviderFactory
에는 공급자 구성을 처리할 수 있는 추가 방법이 있습니다. 공급자별로 구성할 변수를 설명하고 Admin Console에서 일반 입력 페이지를 자동으로 렌더링하여 이 구성을 수집합니다. 구현된 경우 콜백 메서드도 구성을 저장하기 전에, 공급자가 처음 생성될 때 및 업데이트 시의 유효성을 검사합니다. UserStorageProviderFactory
는 org.keycloak.component.ComponentFactory
인터페이스에서 이러한 메서드를 상속합니다.
List<ProviderConfigProperty> getConfigProperties(); default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { } default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) { } default void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel model) { }
List<ProviderConfigProperty> getConfigProperties();
default
void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
throws ComponentValidationException
{
}
default
void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
default
void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
ComponentFactory.getConfigProperties()
메서드는 org.keycloak.provider.ProviderConfigProperty
인스턴스 목록을 반환합니다. 이러한 인스턴스는 공급자의 각 구성 변수를 렌더링하고 저장하는 데 필요한 메타데이터를 선언합니다.
7.5.1. 구성 예
PropertyFileUserStorageProviderFactory
예제를 확장하여 디스크의 특정 파일에 공급자 인스턴스를 가리킬 수 있습니다.
PropertyFileUserStorageProviderFactory
public class PropertyFileUserStorageProviderFactory implements UserStorageProviderFactory<PropertyFileUserStorageProvider> { protected static final List<ProviderConfigProperty> configMetadata; static { configMetadata = ProviderConfigurationBuilder.create() .property().name("path") .type(ProviderConfigProperty.STRING_TYPE) .label("Path") .defaultValue("${jboss.server.config.dir}/example-users.properties") .helpText("File path to properties file") .add().build(); } @Override public List<ProviderConfigProperty> getConfigProperties() { return configMetadata; }
public class PropertyFileUserStorageProviderFactory
implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
protected static final List<ProviderConfigProperty> configMetadata;
static {
configMetadata = ProviderConfigurationBuilder.create()
.property().name("path")
.type(ProviderConfigProperty.STRING_TYPE)
.label("Path")
.defaultValue("${jboss.server.config.dir}/example-users.properties")
.helpText("File path to properties file")
.add().build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configMetadata;
}
ProviderConfigurationBuilder
클래스는 구성 속성 목록을 만들 수 있는 유용한 도우미 클래스입니다. 여기서는 String 유형인 path
라는 변수를 지정합니다. 이 공급자의 Admin Console 구성 페이지에서 이 구성 변수는 Path
로 레이블이 지정되어 있으며 기본값은 ${jboss.server.config.dir}/example-users.properties
입니다. 이 구성 옵션의 툴팁 위에 마우스를 가져가면 도움말 텍스트, 파일 경로 속성 파일이
표시됩니다.
다음으로는 이 파일이 디스크에 있는지 확인하는 것입니다. 유효한 사용자 속성 파일을 가리키지 않는 한 이 공급자의 인스턴스를 활성화하려고 하지 않습니다. 이를 위해 validateConfiguration()
메서드를 구현합니다.
@Override public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException { String fp = config.getConfig().getFirst("path"); if (fp == null) throw new ComponentValidationException("user property file does not exist"); fp = EnvUtil.replace(fp); File file = new File(fp); if (!file.exists()) { throw new ComponentValidationException("user property file does not exist"); } }
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
throws ComponentValidationException {
String fp = config.getConfig().getFirst("path");
if (fp == null) throw new ComponentValidationException("user property file does not exist");
fp = EnvUtil.replace(fp);
File file = new File(fp);
if (!file.exists()) {
throw new ComponentValidationException("user property file does not exist");
}
}
validateConfiguration()
메서드에서 ComponentModel
에서 구성 변수를 가져오고 해당 파일이 디스크에 있는지 확인합니다. org.keycloak.common.util.EnvUtil.replace()
메서드를 사용합니다. 이 방법을 사용하면 ${}
가 포함된 모든 문자열이 시스템 속성 값으로 대체됩니다. ${jboss.server.config.dir}
문자열은 서버의 configuration/
디렉터리에 해당하며 이 예제에 매우 유용합니다.
다음으로 해야 할 것은 이전 init()
메서드를 제거하는 것입니다. 사용자 속성 파일이 공급자 인스턴스당 고유하므로 이 작업을 수행합니다. 이 논리를 create()
메서드로 이동합니다.
@Override public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) { String path = model.getConfig().getFirst("path"); Properties props = new Properties(); try { InputStream is = new FileInputStream(path); props.load(is); is.close(); } catch (IOException e) { throw new RuntimeException(e); } return new PropertyFileUserStorageProvider(session, model, props); }
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
String path = model.getConfig().getFirst("path");
Properties props = new Properties();
try {
InputStream is = new FileInputStream(path);
props.load(is);
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
return new PropertyFileUserStorageProvider(session, model, props);
}
이 논리는 모든 트랜잭션이 디스크에서 전체 사용자 속성 파일을 읽을 때 비효율적이지만, 구성 변수에 연결하는 방법을 간단히 보여줍니다.
7.5.2. 관리 콘솔에서 공급자 구성
구성이 활성화되었으므로 Admin Console에서 공급자를 구성할 때 경로
변수를 설정할 수 있습니다.
7.6. 사용자 및 쿼리 기능 인터페이스 추가/제거
예제에 포함되지 않은 한 가지는 사용자를 추가 및 제거하거나 암호를 변경할 수 있도록 허용하는 것입니다. 예제에 정의된 사용자도 Admin Console에서 쿼리하거나 볼 수 없습니다. 이러한 향상된 기능을 추가하려면 예제 공급자가 UserQueryProvider
및 UserRegistrationProvider
인터페이스를 구현해야 합니다.
7.6.1. Implementing UserRegistrationProvider
이 절차를 사용하여 특정 저장소에서 사용자 추가 및 제거를 구현하려면 먼저 속성 파일을 디스크에 저장할 수 있어야 합니다.
PropertyFileUserStorageProvider
public void save() { String path = model.getConfig().getFirst("path"); path = EnvUtil.replace(path); try { FileOutputStream fos = new FileOutputStream(path); properties.store(fos, ""); fos.close(); } catch (IOException e) { throw new RuntimeException(e); } }
public void save() {
String path = model.getConfig().getFirst("path");
path = EnvUtil.replace(path);
try {
FileOutputStream fos = new FileOutputStream(path);
properties.store(fos, "");
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
그런 다음, addUser()
및 removeUser()
메서드의 구현이 간소화됩니다.
PropertyFileUserStorageProvider
public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD"; @Override public UserModel addUser(RealmModel realm, String username) { synchronized (properties) { properties.setProperty(username, UNSET_PASSWORD); save(); } return createAdapter(realm, username); } @Override public boolean removeUser(RealmModel realm, UserModel user) { synchronized (properties) { if (properties.remove(user.getUsername()) == null) return false; save(); return true; } }
public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD";
@Override
public UserModel addUser(RealmModel realm, String username) {
synchronized (properties) {
properties.setProperty(username, UNSET_PASSWORD);
save();
}
return createAdapter(realm, username);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
synchronized (properties) {
if (properties.remove(user.getUsername()) == null) return false;
save();
return true;
}
}
사용자를 추가할 때 속성 맵의 암호 값을 UNSET_PASSWORD
로 설정합니다. 속성 값에 속성에 대한 null 값이 없기 때문에 이 작업을 수행합니다. 또한 이를 반영하기 위해 CredentialInputValidator
방법을 수정해야 합니다.
공급자가 UserRegistrationProvider
인터페이스를 구현하는 경우 addUser()
메서드가 호출됩니다. 공급자에 사용자 추가를 끄는 구성 스위치가 있는 경우 이 메서드에서 null
을 반환하면 공급자를 건너뛰고 다음 항목을 호출합니다.
PropertyFileUserStorageProvider
@Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; UserCredentialModel cred = (UserCredentialModel)input; String password = properties.getProperty(user.getUsername()); if (password == null || UNSET_PASSWORD.equals(password)) return false; return password.equals(cred.getValue()); }
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
String password = properties.getProperty(user.getUsername());
if (password == null || UNSET_PASSWORD.equals(password)) return false;
return password.equals(cred.getValue());
}
이제 속성 파일을 저장할 수 있으므로 암호 업데이트를 허용하는 것도 좋습니다.
PropertyFileUserStorageProvider
@Override public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { if (!(input instanceof UserCredentialModel)) return false; if (!input.getType().equals(CredentialModel.PASSWORD)) return false; UserCredentialModel cred = (UserCredentialModel)input; synchronized (properties) { properties.setProperty(user.getUsername(), cred.getValue()); save(); } return true; }
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false;
if (!input.getType().equals(CredentialModel.PASSWORD)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
synchronized (properties) {
properties.setProperty(user.getUsername(), cred.getValue());
save();
}
return true;
}
이제 암호 비활성화도 구현할 수 있습니다.
PropertyFileUserStorageProvider
@Override public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { if (!credentialType.equals(CredentialModel.PASSWORD)) return; synchronized (properties) { properties.setProperty(user.getUsername(), UNSET_PASSWORD); save(); } } private static final Set<String> disableableTypes = new HashSet<>(); static { disableableTypes.add(CredentialModel.PASSWORD); } @Override public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) { return disableableTypes; }
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
if (!credentialType.equals(CredentialModel.PASSWORD)) return;
synchronized (properties) {
properties.setProperty(user.getUsername(), UNSET_PASSWORD);
save();
}
}
private static final Set<String> disableableTypes = new HashSet<>();
static {
disableableTypes.add(CredentialModel.PASSWORD);
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return disableableTypes;
}
이러한 방법을 구현하면 Admin Console에서 사용자의 암호를 변경하고 비활성화할 수 있습니다.
7.6.2. UserQueryProvider 구현
UserQueryProvider
를 구현하지 않으면 관리 콘솔은 예제 공급자가 로드한 사용자를 보고 관리할 수 없습니다. 이 인터페이스 구현을 살펴보겠습니다.
PropertyFileUserStorageProvider
@Override public int getUsersCount(RealmModel realm) { return properties.size(); } @Override public List<UserModel> getUsers(RealmModel realm) { return getUsers(realm, 0, Integer.MAX_VALUE); } @Override public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) { List<UserModel> users = new LinkedList<>(); int i = 0; for (Object obj : properties.keySet()) { if (i++ < firstResult) continue; String username = (String)obj; UserModel user = getUserByUsername(username, realm); users.add(user); if (users.size() >= maxResults) break; } return users; }
@Override
public int getUsersCount(RealmModel realm) {
return properties.size();
}
@Override
public List<UserModel> getUsers(RealmModel realm) {
return getUsers(realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
List<UserModel> users = new LinkedList<>();
int i = 0;
for (Object obj : properties.keySet()) {
if (i++ < firstResult) continue;
String username = (String)obj;
UserModel user = getUserByUsername(username, realm);
users.add(user);
if (users.size() >= maxResults) break;
}
return users;
}
getUsers()
메서드는 getUserByUsername()이 사용자를 로드하기 위해 속성 파일의 키 집합을 반복하고 getUserByUsername()
에 위임합니다. firstResult
및 maxResults
매개변수를 기반으로 이 호출을 인덱싱하고 있습니다. 외부 저장소에서 페이지 번호를 지원하지 않는 경우 비슷한 논리를 수행해야 합니다.
PropertyFileUserStorageProvider
@Override public List<UserModel> searchForUser(String search, RealmModel realm) { return searchForUser(search, realm, 0, Integer.MAX_VALUE); } @Override public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { List<UserModel> users = new LinkedList<>(); int i = 0; for (Object obj : properties.keySet()) { String username = (String)obj; if (!username.contains(search)) continue; if (i++ < firstResult) continue; UserModel user = getUserByUsername(username, realm); users.add(user); if (users.size() >= maxResults) break; } return users; }
@Override
public List<UserModel> searchForUser(String search, RealmModel realm) {
return searchForUser(search, realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
List<UserModel> users = new LinkedList<>();
int i = 0;
for (Object obj : properties.keySet()) {
String username = (String)obj;
if (!username.contains(search)) continue;
if (i++ < firstResult) continue;
UserModel user = getUserByUsername(username, realm);
users.add(user);
if (users.size() >= maxResults) break;
}
return users;
}
searchForUser()
의 첫 번째 선언에는 String
매개 변수가 사용됩니다. 사용자를 찾기 위해 사용자 이름 및 이메일 속성을 검색하는 데 사용하는 문자열이 되어야 합니다. 이 문자열은 하위 문자열일 수 있으므로 검색을 수행할 때 String.contains()
메서드를 사용합니다.
PropertyFileUserStorageProvider
@Override public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) { return searchForUser(params, realm, 0, Integer.MAX_VALUE); } @Override public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) { // only support searching by username String usernameSearchString = params.get("username"); if (usernameSearchString == null) return Collections.EMPTY_LIST; return searchForUser(usernameSearchString, realm, firstResult, maxResults); }
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
return searchForUser(params, realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
// only support searching by username
String usernameSearchString = params.get("username");
if (usernameSearchString == null) return Collections.EMPTY_LIST;
return searchForUser(usernameSearchString, realm, firstResult, maxResults);
}
Map
매개변수를 사용하는 searchForUser()
메서드는 첫 번째, 성, 사용자 이름, 이메일에 따라 사용자를 검색할 수 있습니다. 사용자 이름만 저장하므로 사용자 이름을 기반으로만 검색합니다. 이를 위해 searchForUser()
에 위임합니다.
PropertyFileUserStorageProvider
@Override public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { return Collections.EMPTY_LIST; } @Override public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) { return Collections.EMPTY_LIST; } @Override public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { return Collections.EMPTY_LIST; }
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
return Collections.EMPTY_LIST;
}
그룹 또는 특성을 저장하지 않으므로 다른 방법으로는 빈 목록이 반환됩니다.
7.7. 외부 스토리지 강화
PropertyFileUserStorageProvider
예제는 실제로 제한됩니다. 속성 파일에 저장된 사용자로 로그인할 수는 있지만 다른 많은 작업을 수행할 수는 없습니다. 이 공급자가 로드한 사용자가 특정 애플리케이션에 완전히 액세스하기 위해 특별한 역할 또는 그룹 매핑이 필요한 경우 이러한 사용자에게 역할 매핑을 추가할 수 있는 방법이 없습니다. 또한 이메일, 첫 번째 및 성과 같은 중요한 속성을 수정하거나 추가할 수 없습니다.
이러한 유형의 경우 Red Hat Single Sign-On을 사용하면 Red Hat Single Sign-On의 데이터베이스에 추가 정보를 저장하여 외부 저장소를 보강할 수 있습니다. 이를 페더레이션 사용자 스토리지라고 하며 org.keycloak.storage.federated.UserFederatedStorageProvider
클래스에 캡슐화됩니다.
UserFederatedStorageProvider
package org.keycloak.storage.federated; public interface UserFederatedStorageProvider extends Provider { Set<GroupModel> getGroups(RealmModel realm, String userId); void joinGroup(RealmModel realm, String userId, GroupModel group); void leaveGroup(RealmModel realm, String userId, GroupModel group); List<String> getMembership(RealmModel realm, GroupModel group, int firstResult, int max); ...
package org.keycloak.storage.federated;
public interface UserFederatedStorageProvider extends Provider {
Set<GroupModel> getGroups(RealmModel realm, String userId);
void joinGroup(RealmModel realm, String userId, GroupModel group);
void leaveGroup(RealmModel realm, String userId, GroupModel group);
List<String> getMembership(RealmModel realm, GroupModel group, int firstResult, int max);
...
UserFederatedStorageProvider
인스턴스는 KeycloakSession.userFederatedStorage()
메서드에서 사용할 수 있습니다. 속성, 그룹 및 역할 매핑, 다양한 인증 정보 유형 및 필요한 작업을 저장하는 데 필요한 모든 종류의 방법이 있습니다. 외부 저장소의 데이터 모델이 전체 Red Hat Single Sign-On 기능을 지원할 수 없는 경우 이 서비스는 격차를 채울 수 있습니다.
Red Hat Single Sign-On에는 org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage
가 포함되어 있습니다. 이 클래스는 사용자 이름 가져오기/설정을 제외한 모든 UserModel
방법을 사용자 페더레이션 스토리지에 위임합니다. 외부 스토리지 표현에 위임하기 위해 재정의해야 하는 메서드를 재정의합니다. 재정의할 수 있는 더 작은 보호 메서드가 있으므로 이 클래스의 javadoc을 읽는 것이 좋습니다. 그룹 멤버십 및 역할 매핑을 구체적으로 분석합니다.
7.7.1. 보강 예
우리의 PropertyFileUserStorageProvider
예제에서는 AbstractUserAdapterFederatedStorage
를 사용하기 위해 공급자에게 간단한 변경이 필요합니다.
PropertyFileUserStorageProvider
protected UserModel createAdapter(RealmModel realm, String username) { return new AbstractUserAdapterFederatedStorage(session, realm, model) { @Override public String getUsername() { return username; } @Override public void setUsername(String username) { String pw = (String)properties.remove(username); if (pw != null) { properties.put(username, pw); save(); } } }; }
protected UserModel createAdapter(RealmModel realm, String username) {
return new AbstractUserAdapterFederatedStorage(session, realm, model) {
@Override
public String getUsername() {
return username;
}
@Override
public void setUsername(String username) {
String pw = (String)properties.remove(username);
if (pw != null) {
properties.put(username, pw);
save();
}
}
};
}
대신 AbstractUserAdapterFederatedStorage
의 익명 클래스 구현을 정의합니다. setUsername()
메서드는 속성 파일을 변경하여 저장합니다.
7.8. 구현 전략 가져오기
사용자 스토리지 공급자를 구현할 때 수행할 수 있는 또 다른 전략이 있습니다. 사용자 페더레이션 스토리지를 사용하는 대신 Red Hat Single Sign-On 내장 사용자 데이터베이스에 로컬로 사용자를 생성하고 외부 저장소의 속성을 이 로컬 복사본으로 복사할 수 있습니다. 이 접근 방식에는 많은 이점이 있습니다.
- Red Hat Single Sign-On은 기본적으로 외부 저장소의 지속성 사용자 캐시가 됩니다. 사용자를 가져오면 더 이상 외부 저장소에 충돌하지 않으므로 로드를 수행하지 않습니다.
- 공식 사용자 저장소로 Red Hat Single Sign-On으로 이동하여 이전 외부 저장소를 사용 중단하는 경우 Red Hat Single Sign-On을 사용하도록 애플리케이션을 천천히 마이그레이션할 수 있습니다. 모든 애플리케이션이 마이그레이션되면 가져온 사용자를 분리하고 기존의 기존 외부 저장소를 중단시킵니다.
가져오기 전략을 사용하는 데는 몇 가지 명확한 문제가 있습니다.
- 처음 사용자를 찾으려면 Red Hat Single Sign-On 데이터베이스에 대한 여러 업데이트가 필요합니다. 이는 부하가 크게 저하될 수 있으며 Red Hat Single Sign-On 데이터베이스에 많은 부담을 줄 수 있습니다. 사용자 페더레이션 스토리지 접근 방식은 필요에 따라 추가 데이터만 저장하고 외부 저장소의 기능에 따라 절대 사용하지 않을 수 있습니다.
- 가져오기 접근 방식을 사용하면 로컬 Red Hat Single Sign-On 스토리지와 외부 스토리지를 동기화해야 합니다. User Storage SPI에는 동기화를 지원하기 위해 구현할 수 있는 기능 인터페이스가 있지만 이로 인해 번거롭고 혼란스러울 수 있습니다.
가져오기 전략을 구현하려면 먼저 사용자를 로컬에서 가져온지 확인하기만 하면 됩니다. 이 경우 로컬 사용자를 반환하고 사용자를 로컬로 생성하지 않고 외부 저장소에서 데이터를 가져옵니다. 대부분의 변경 사항이 자동으로 동기화되도록 로컬 사용자를 프록시할 수도 있습니다.
이로 인해 약간 혼란스러울 수 있지만 이 방법을 사용하기 위해 PropertyFileUserStorageProvider
를 확장할 수 있습니다. 먼저 createAdapter()
메서드를 수정합니다.
PropertyFileUserStorageProvider
protected UserModel createAdapter(RealmModel realm, String username) { UserModel local = session.userLocalStorage().getUserByUsername(username, realm); if (local == null) { local = session.userLocalStorage().addUser(realm, username); local.setFederationLink(model.getId()); } return new UserModelDelegate(local) { @Override public void setUsername(String username) { String pw = (String)properties.remove(username); if (pw != null) { properties.put(username, pw); save(); } super.setUsername(username); } }; }
protected UserModel createAdapter(RealmModel realm, String username) {
UserModel local = session.userLocalStorage().getUserByUsername(username, realm);
if (local == null) {
local = session.userLocalStorage().addUser(realm, username);
local.setFederationLink(model.getId());
}
return new UserModelDelegate(local) {
@Override
public void setUsername(String username) {
String pw = (String)properties.remove(username);
if (pw != null) {
properties.put(username, pw);
save();
}
super.setUsername(username);
}
};
}
이 방법에서는 KeycloakSession.userLocalStorage()
메서드를 호출하여 로컬 Red Hat Single Sign-On 사용자 스토리지에 대한 참조를 가져옵니다. 사용자가 로컬에 저장되었는지 여부를 확인할 수 있습니다. 그렇지 않은 경우 로컬에 추가합니다. 로컬 사용자의 ID 를
설정하지 마십시오. Red Hat Single Sign-On에서 ID를 자동으로 생성하도록 합니다
. 또한 UserModel.setFederationLink()
를 호출하고 공급자의 ComponentModel
ID를 전달합니다. 이렇게 하면 공급자와 가져온 사용자 간 링크가 설정됩니다.
사용자 스토리지 공급자가 제거되면 가져온 모든 사용자도 제거됩니다. 이는 UserModel.setFederationLink()
를 호출하기 위한 목적 중 하나입니다.
또 다른 점은 로컬 사용자가 연결되어 있는 경우 스토리지 공급자가 CredentialInputValidator
및 CredentialInputUpdater
인터페이스에서 구현하는 메서드에 여전히 위임된다는 것입니다. 검증 또는 업데이트에서 false
를 반환하면 로컬 스토리지를 사용하여 검증하거나 업데이트할 수 있는지 Red Hat Single Sign-On이 표시됩니다.
또한 org.keycloak.models.utils.UserModelForwarded 클래스를 사용하여 로컬 사용자를 프록시하고
있습니다. 이 클래스는 UserModel
의 구현입니다. 모든 메서드는 인스턴스화한 UserModel
에 위임합니다. 이 delegate 클래스의 setUsername()
메서드를 재정의하여 속성 파일과 자동으로 동기화합니다. 공급자의 경우 이를 사용하여 로컬 UserModel
의 다른 메서드를 가로채 어 외부 저장소와의 동기화를 수행할 수 있습니다. 예를 들어, get 메서드가 로컬 저장소가 동기화되어 있는지 확인할 수 있습니다. 설정 방법은 로컬 저장소와 동기화된 외부 저장소를 유지합니다. 한 가지 주목할 점은 getId()
메서드에서 사용자를 로컬로 생성할 때 자동 생성된 ID를 항상 반환해야 한다는 것입니다. 다른 중요하지 않은 예에 표시된 대로 페더레이션 ID를 반환해서는 안 됩니다.
공급자가 UserRegistrationProvider
인터페이스를 구현하는 경우 removeUser()
메서드가 로컬 스토리지에서 사용자를 제거할 필요가 없습니다. 런타임은 이 작업을 자동으로 수행합니다. 또한 removeUser()
는 로컬 스토리지에서 제거되기 전에 호출됩니다.
7.8.1. ImportedUserValidation 인터페이스
이 장의 앞부분을 기억하는 경우 사용자 쿼리가 어떻게 작동하는지에 대해 설명합니다. 사용자가 있는 경우 로컬 스토리지를 먼저 쿼리한 다음 쿼리가 종료됩니다. 이는 로컬 UserModel
을 프록시하여 사용자 이름을 동기화 상태로 유지할 수 있도록 위의 구현에서 문제가 됩니다. User Storage SPI에는 연결된 로컬 사용자가 로컬 데이터베이스에서 로드될 때마다 콜백이 있습니다.
package org.keycloak.storage.user; public interface ImportedUserValidation { /** * If this method returns null, then the user in local storage will be removed * * @param realm * @param user * @return null if user no longer valid */ UserModel validate(RealmModel realm, UserModel user); }
package org.keycloak.storage.user;
public interface ImportedUserValidation {
/**
* If this method returns null, then the user in local storage will be removed
*
* @param realm
* @param user
* @return null if user no longer valid
*/
UserModel validate(RealmModel realm, UserModel user);
}
연결된 로컬 사용자가 로드될 때마다 사용자 스토리지 공급자 클래스에서 이 인터페이스를 구현하는 경우 validate()
메서드가 호출됩니다. 여기에서 매개 변수로 전달된 로컬 사용자를 프록시하고 반환할 수 있습니다. 새로운 UserModel
이 사용될 예정입니다. 선택적으로 검사를 수행하여 사용자가 외부 저장소에 여전히 존재하는지 확인할 수도 있습니다. validate()
가 null
을 반환하면 로컬 사용자가 데이터베이스에서 제거됩니다.
7.8.2. ImportSynchronization 인터페이스
가져오기 전략을 사용하면 로컬 사용자 복사에서 외부 스토리지와 동기화할 수 없음을 확인할 수 있습니다. 예를 들어 사용자가 외부 저장소에서 제거되었을 수 있습니다. User Storage SPI에는 org.keycloak.storage.user.ImportSynchronization
을 처리하기 위해 구현할 수 있는 추가 인터페이스가 있습니다.
package org.keycloak.storage.user; public interface ImportSynchronization { SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model); SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model); }
package org.keycloak.storage.user;
public interface ImportSynchronization {
SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
}
이 인터페이스는 공급자 팩토리에 의해 구현됩니다. 공급자 팩토리에서 이 인터페이스를 구현하면 공급자의 관리 콘솔 관리 페이지에 추가 옵션이 표시됩니다. 버튼을 클릭하여 수동으로 동기화를 강제 적용할 수 있습니다. ImportSynchronization.sync()
메서드를 호출합니다. 또한 동기화를 자동으로 예약할 수 있는 추가 구성 옵션이 표시됩니다. 자동 동기화는 syncSince()
메서드를 호출합니다.
7.9. 사용자 캐시
사용자 오브젝트가 ID, 사용자 이름 또는 이메일에 의해 로드되면 캐시됩니다. 사용자 오브젝트가 캐시되는 경우 전체 UserModel
인터페이스를 반복하고 이 정보를 로컬 메모리 전용 캐시로 가져옵니다. 클러스터에서 이 캐시는 여전히 로컬이지만 무효화 캐시가 됩니다. 사용자 오브젝트가 수정되면 제거됩니다. 이 제거 이벤트는 다른 노드의 사용자 캐시도 무효화되도록 전체 클러스터에 전파됩니다.
7.9.1. 사용자 캐시 관리
KeycloakSession.userCache()
를 호출하여 사용자 캐시에 액세스할 수 있습니다.
/** * All these methods effect an entire cluster of Keycloak instances. * * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ public interface UserCache extends UserProvider { /** * Evict user from cache. * * @param user */ void evict(RealmModel realm, UserModel user); /** * Evict users of a specific realm * * @param realm */ void evict(RealmModel realm); /** * Clear cache entirely. * */ void clear(); }
/**
* All these methods effect an entire cluster of Keycloak instances.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserCache extends UserProvider {
/**
* Evict user from cache.
*
* @param user
*/
void evict(RealmModel realm, UserModel user);
/**
* Evict users of a specific realm
*
* @param realm
*/
void evict(RealmModel realm);
/**
* Clear cache entirely.
*
*/
void clear();
}
특정 사용자, 특정 영역에 포함된 사용자 또는 전체 캐시를 제거하는 방법이 있습니다.
7.9.2. OnUserCache 콜백 인터페이스
공급자 구현과 관련된 추가 정보를 캐시할 수 있습니다. 사용자 스토리지 SPI에는 org.keycloak.models.cache.OnUserCache
사용자가 캐시될 때마다 콜백이 있습니다.
public interface OnUserCache { void onCache(RealmModel realm, CachedUserModel user, UserModel delegate); }
public interface OnUserCache {
void onCache(RealmModel realm, CachedUserModel user, UserModel delegate);
}
공급자 클래스는 이 콜백을 원하는 경우 이 인터페이스를 구현해야 합니다. UserModel
delegate 매개변수는 공급자가 반환한 UserModel
인스턴스입니다. CachedUserModel
은 확장된 UserModel
인터페이스입니다. 로컬 스토리지에서 로컬로 캐시된 인스턴스입니다.
public interface CachedUserModel extends UserModel { /** * Invalidates the cache for this user and returns a delegate that represents the actual data provider * * @return */ UserModel getDelegateForUpdate(); boolean isMarkedForEviction(); /** * Invalidate the cache for this model * */ void invalidate(); /** * When was the model was loaded from database. * * @return */ long getCacheTimestamp(); /** * Returns a map that contains custom things that are cached along with this model. You can write to this map. * * @return */ ConcurrentHashMap getCachedWith(); }
public interface CachedUserModel extends UserModel {
/**
* Invalidates the cache for this user and returns a delegate that represents the actual data provider
*
* @return
*/
UserModel getDelegateForUpdate();
boolean isMarkedForEviction();
/**
* Invalidate the cache for this model
*
*/
void invalidate();
/**
* When was the model was loaded from database.
*
* @return
*/
long getCacheTimestamp();
/**
* Returns a map that contains custom things that are cached along with this model. You can write to this map.
*
* @return
*/
ConcurrentHashMap getCachedWith();
}
이 CachedUserModel
인터페이스를 사용하면 캐시에서 사용자를 제거하고 공급자 UserModel
인스턴스를 가져올 수 있습니다. getCachedWith()
메서드는 사용자와 관련된 추가 정보를 캐시할 수 있는 맵을 반환합니다. 예를 들어, 자격 증명은 UserModel
인터페이스의 일부가 아닙니다. 메모리에 자격 증명을 캐시하려면 OnUserCache
를 구현하고 getCachedWith()
메서드를 사용하여 사용자 자격 증명을 캐시합니다.
7.9.3. 캐시 정책
사용자 스토리지 공급자의 관리 콘솔 관리 페이지에서 고유한 캐시 정책을 지정할 수 있습니다.
7.10. Leveraging Jakarta EE
META-INF/services
파일을 올바르게 설정하여 공급자를 가리키도록 사용자 스토리지 공급자를 모든 자karta EE 구성 요소 내에 패키징할 수 있습니다. 예를 들어 공급자가 타사 라이브러리를 사용해야 하는 경우 EAR 내에서 공급자를 패키지하고 이러한 타사 라이브러리를 EAR의 lib/
디렉터리에 저장할 수 있습니다. 또한 공급자 JAR는 Egresss, WARS 및 EARs가 JBoss EAP 환경에서 사용할 수 있는 jboss-deployment-structure.xml
파일을 사용할 수 있습니다. 이 파일에 대한 자세한 내용은 JBoss EAP 설명서를 참조하십시오. 이를 통해 다른 세분화된 작업 간에 외부 종속성을 가져올 수 있습니다.
공급자 구현은 일반 java 오브젝트여야 합니다. 또한 현재 Stateful Egresss로 UserStorageProvider
클래스 구현을 지원합니다. JPA를 사용하여 관계형 저장소에 연결하려는 경우 특히 유용합니다. 이렇게 하면 됩니다.
@Stateful @Local(EjbExampleUserStorageProvider.class) public class EjbExampleUserStorageProvider implements UserStorageProvider, UserLookupProvider, UserRegistrationProvider, UserQueryProvider, CredentialInputUpdater, CredentialInputValidator, OnUserCache { @PersistenceContext protected EntityManager em; protected ComponentModel model; protected KeycloakSession session; public void setModel(ComponentModel model) { this.model = model; } public void setSession(KeycloakSession session) { this.session = session; } @Remove @Override public void close() { } ... }
@Stateful
@Local(EjbExampleUserStorageProvider.class)
public class EjbExampleUserStorageProvider implements UserStorageProvider,
UserLookupProvider,
UserRegistrationProvider,
UserQueryProvider,
CredentialInputUpdater,
CredentialInputValidator,
OnUserCache
{
@PersistenceContext
protected EntityManager em;
protected ComponentModel model;
protected KeycloakSession session;
public void setModel(ComponentModel model) {
this.model = model;
}
public void setSession(KeycloakSession session) {
this.session = session;
}
@Remove
@Override
public void close() {
}
...
}
@Local
주석을 정의하고 해당 공급자 클래스를 지정해야 합니다. 이 작업을 수행하지 않으면 NodePort가 사용자를 올바르게 프록시하지 않으며 공급자가 작동하지 않습니다.
공급자의 close()
메서드에 @Remove
주석을 배치해야 합니다. 그러지 않으면 상태 저장 빈이 정리되지 않으며 결국 오류 메시지가 표시될 수 있습니다.
UserStorageProvider
의 구현은 일반 Java 오브젝트여야 합니다. 팩토리 클래스는 create() 메서드에서 Stateful NodePort의 JNDI 조회를 수행합니다.
public class EjbExampleUserStorageProviderFactory implements UserStorageProviderFactory<EjbExampleUserStorageProvider> { @Override public EjbExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) { try { InitialContext ctx = new InitialContext(); EjbExampleUserStorageProvider provider = (EjbExampleUserStorageProvider)ctx.lookup( "java:global/user-storage-jpa-example/" + EjbExampleUserStorageProvider.class.getSimpleName()); provider.setModel(model); provider.setSession(session); return provider; } catch (Exception e) { throw new RuntimeException(e); } }
public class EjbExampleUserStorageProviderFactory
implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {
@Override
public EjbExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) {
try {
InitialContext ctx = new InitialContext();
EjbExampleUserStorageProvider provider = (EjbExampleUserStorageProvider)ctx.lookup(
"java:global/user-storage-jpa-example/" + EjbExampleUserStorageProvider.class.getSimpleName());
provider.setModel(model);
provider.setSession(session);
return provider;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
또한 이 예제에서는 공급자와 동일한 JAR에 JPA 배포를 정의했다고 가정합니다. 즉, persistence.xml
파일 및 JPA @Entity
클래스를 의미합니다.
JPA를 사용하는 경우 추가 데이터 소스는 XA 데이터 소스여야 합니다. Red Hat Single Sign-On 데이터 소스는 XA 데이터 소스가 아닙니다. 동일한 트랜잭션에서 둘 이상의 XA 데이터 소스와 상호 작용하면 서버에서 오류 메시지를 반환합니다. 단일 트랜잭션에서는 XA가 아닌 하나의 리소스만 허용됩니다. XA 데이터 소스 배포에 대한 자세한 내용은 JBoss EAP 설명서를 참조하십시오.
CDI는 지원되지 않습니다.
7.11. REST 관리 API
관리자 REST API를 통해 사용자 스토리지 공급자 배포를 생성, 제거 및 업데이트할 수 있습니다. User Storage SPI는 일반 구성 요소 인터페이스를 기반으로 구축되므로 일반 API를 사용하여 공급자를 관리합니다.
REST 구성 요소 API는 영역 관리 리소스에 있습니다.
/admin/realms/{realm-name}/components
/admin/realms/{realm-name}/components
Java 클라이언트와 이 REST API 상호 작용만 표시합니다. 이 API에서 curl
에서 이 작업을 수행하는 방법을 추출할 수 있기를 바랍니다.
public interface ComponentsResource { @GET @Produces(MediaType.APPLICATION_JSON) public List<ComponentRepresentation> query(); @GET @Produces(MediaType.APPLICATION_JSON) public List<ComponentRepresentation> query(@QueryParam("parent") String parent); @GET @Produces(MediaType.APPLICATION_JSON) public List<ComponentRepresentation> query(@QueryParam("parent") String parent, @QueryParam("type") String type); @GET @Produces(MediaType.APPLICATION_JSON) public List<ComponentRepresentation> query(@QueryParam("parent") String parent, @QueryParam("type") String type, @QueryParam("name") String name); @POST @Consumes(MediaType.APPLICATION_JSON) Response add(ComponentRepresentation rep); @Path("{id}") ComponentResource component(@PathParam("id") String id); } public interface ComponentResource { @GET public ComponentRepresentation toRepresentation(); @PUT @Consumes(MediaType.APPLICATION_JSON) public void update(ComponentRepresentation rep); @DELETE public void remove(); }
public interface ComponentsResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ComponentRepresentation> query();
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ComponentRepresentation> query(@QueryParam("parent") String parent);
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ComponentRepresentation> query(@QueryParam("parent") String parent, @QueryParam("type") String type);
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ComponentRepresentation> query(@QueryParam("parent") String parent,
@QueryParam("type") String type,
@QueryParam("name") String name);
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response add(ComponentRepresentation rep);
@Path("{id}")
ComponentResource component(@PathParam("id") String id);
}
public interface ComponentResource {
@GET
public ComponentRepresentation toRepresentation();
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void update(ComponentRepresentation rep);
@DELETE
public void remove();
}
사용자 스토리지 공급자를 생성하려면 org.keycloak.storage.UserStorageProvider
문자열의 공급자 유형 및 구성을 지정해야 합니다.
import org.keycloak.admin.client.Keycloak; import org.keycloak.representations.idm.RealmRepresentation; ... Keycloak keycloak = Keycloak.getInstance( "http://localhost:8080/auth", "master", "admin", "password", "admin-cli"); RealmResource realmResource = keycloak.realm("master"); RealmRepresentation realm = realmResource.toRepresentation(); ComponentRepresentation component = new ComponentRepresentation(); component.setName("home"); component.setProviderId("readonly-property-file"); component.setProviderType("org.keycloak.storage.UserStorageProvider"); component.setParentId(realm.getId()); component.setConfig(new MultivaluedHashMap()); component.getConfig().putSingle("path", "~/users.properties"); realmResource.components().add(component); // retrieve a component List<ComponentRepresentation> components = realmResource.components().query(realm.getId(), "org.keycloak.storage.UserStorageProvider", "home"); component = components.get(0); // Update a component component.getConfig().putSingle("path", "~/my-users.properties"); realmResource.components().component(component.getId()).update(component); // Remove a component realmREsource.components().component(component.getId()).remove();
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RealmRepresentation;
...
Keycloak keycloak = Keycloak.getInstance(
"http://localhost:8080/auth",
"master",
"admin",
"password",
"admin-cli");
RealmResource realmResource = keycloak.realm("master");
RealmRepresentation realm = realmResource.toRepresentation();
ComponentRepresentation component = new ComponentRepresentation();
component.setName("home");
component.setProviderId("readonly-property-file");
component.setProviderType("org.keycloak.storage.UserStorageProvider");
component.setParentId(realm.getId());
component.setConfig(new MultivaluedHashMap());
component.getConfig().putSingle("path", "~/users.properties");
realmResource.components().add(component);
// retrieve a component
List<ComponentRepresentation> components = realmResource.components().query(realm.getId(),
"org.keycloak.storage.UserStorageProvider",
"home");
component = components.get(0);
// Update a component
component.getConfig().putSingle("path", "~/my-users.properties");
realmResource.components().component(component.getId()).update(component);
// Remove a component
realmREsource.components().component(component.getId()).remove();
7.12. 이전 사용자 페더레이션 SPI에서 마이그레이션
이 장은 이전 (및 현재 사용자 페더레이션 SPI)을 사용하여 공급자를 구현 한 경우에만 적용됩니다.
Keycloak 버전 2.4.0 및 이전 버전에는 사용자 페더레이션 SPI가 있었습니다. Red Hat Single Sign-On 버전 7.0은 지원되지 않지만 이전 SPI도 사용할 수 있었습니다. 이전 사용자 페더레이션 SPI는 Keycloak 버전 2.5.0 및 Red Hat Single Sign-On 버전 7.1에서 제거되었습니다. 그러나 이 이전 SPI를 사용하여 공급자를 작성한 경우 이 장에서는 이 공급자를 포트하는 데 사용할 수 있는 몇 가지 전략에 대해 설명합니다.
7.12.1. 가져오기 대 비 가져오기
이전 사용자 페더레이션 SPI는 Red Hat Single Sign-On의 데이터베이스에서 사용자의 로컬 사본을 생성하고 외부 저장소에서 로컬 사본으로 정보를 가져와야 했습니다. 그러나 이는 더 이상 요구 사항이 아닙니다. 이전 공급자를 그대로 이식할 수 있지만 중요하지 않은 전략이 더 나은 접근 방식이 될 수 있는지 고려해야 합니다.
가져오기 전략의 장점은 다음과 같습니다.
- Red Hat Single Sign-On은 기본적으로 외부 저장소의 지속성 사용자 캐시가 됩니다. 사용자를 가져오면 더 이상 외부 저장소에 충돌하지 않으므로 로드를 해제합니다.
- 공식 사용자 저장소로 Red Hat Single Sign-On으로 이동하여 이전 외부 저장소를 사용 중단하는 경우 Red Hat Single Sign-On을 사용하도록 애플리케이션을 천천히 마이그레이션할 수 있습니다. 모든 애플리케이션이 마이그레이션되면 가져온 사용자를 분리하고 이전의 레거시 외부 저장소를 고집합니다.
가져오기 전략을 사용하는 데는 몇 가지 명확한 문제가 있습니다.
- 처음 사용자를 찾으려면 Red Hat Single Sign-On 데이터베이스에 대한 여러 업데이트가 필요합니다. 이는 부하가 크게 저하될 수 있으며 Red Hat Single Sign-On 데이터베이스에 많은 부담을 줄 수 있습니다. 사용자 페더레이션 스토리지 접근 방식은 필요에 따라 추가 데이터만 저장하고 외부 저장소의 기능에 따라 사용하지 않을 수 있습니다.
- 가져오기 접근 방식을 사용하면 로컬 Red Hat Single Sign-On 스토리지와 외부 스토리지를 동기화해야 합니다. User Storage SPI에는 동기화를 지원하기 위해 구현할 수 있는 기능 인터페이스가 있지만 이로 인해 번거롭고 혼란스러울 수 있습니다.
7.12.2. UserFederationProvider versus UserStorageProvider
가장 먼저 눈에 띄는 것은 UserFederationProvider
가 완전한 인터페이스라는 것입니다. 이 인터페이스의 모든 메서드를 구현했습니다. 그러나 UserStorageProvider
는 필요에 따라 이 인터페이스를 구현하는 여러 기능 인터페이스로 분류했습니다.
UserFederationProvider.getUserByUsername()
및 getUserByEmail()
은 새 SPI에 정확히 동등한 값을 갖습니다. 이 둘의 차이점은 어떻게 가져오는가입니다. 가져오기 전략을 계속 진행하려는 경우 더 이상 KeycloakSession.userStorage().addUser()
를 호출하여 로컬로 사용자를 생성하지 않습니다. Instead you call KeycloakSession.userLocalStorage().addUser()
. userStorage()
메서드가 더 이상 존재하지 않습니다.
UserFederationProvider.validateAndProxy()
메서드가 선택적 기능 인터페이스 ImportedUserValidation
로 이동되었습니다. 이전 공급자를 그대로 이식하는 경우 이 인터페이스를 구현하려고 합니다. 또한 이전 SPI에서는 로컬 사용자가 캐시에 있는 경우에도 이 방법을 사용자가 액세스할 때마다 호출됩니다. 이후 SPI에서 이 방법은 로컬 사용자가 로컬 저장소에서 로드될 때만 호출됩니다. 로컬 사용자가 캐시되면 ImportedUserValidation.validate()
메서드를 전혀 호출하지 않습니다.
UserFederationProvider.isValid()
메서드는 이후 SPI에 더 이상 존재하지 않습니다.
UserFederationProvider
메서드는 synchronizeRegistrations
(), registerUser()
및 removeUser()
가 UserRegistrationProvider
기능 인터페이스로 이동되었습니다. 이 새 인터페이스는 구현에 선택 사항이므로 공급자에서 사용자 생성 및 제거를 지원하지 않으면 구현할 필요가 없습니다. 이전 공급자가 새 사용자 등록 지원을 토글하도록 전환한 경우 새 SPI에서 지원되는 경우 공급자가 사용자 추가를 지원하지 않는 경우 UserRegistrationProvider.addUser()
에서 null
을 반환합니다.
인증 정보를 중심으로 하는 이전 UserFederationProvider
메서드가 CredentialInputValidator
및 CredentialInputUpdater
인터페이스에 캡슐화되며 인증 정보 유효성 확인 또는 업데이트를 지원하는 경우에 따라 구현할 수도 있습니다. UserModel
메서드에 존재하는 데 사용되는 인증 정보 관리. 또한 CredentialInputValidator
및 CredentialInputUpdater
인터페이스로 이동되었습니다. 한 가지 사항은 CredentialInputUpdater
인터페이스를 구현하지 않으면 Red Hat Single Sign-On 스토리지에서 로컬로 공급자에게 제공되는 모든 인증 정보를 재정의할 수 있습니다. 인증 정보를 읽기 전용으로 만들려면 CredentialInputUpdater.updateCredential()
메서드를 구현하고 ReadOnlyException
을 반환합니다.
UserFederationProvider
쿼리 메서드(예: searchByAttributes()
및 getGroupMembers
())는 이제 선택적 인터페이스 UserQueryProvider
로 캡슐화됩니다. 이 인터페이스를 구현하지 않으면 관리 콘솔에서 사용자를 볼 수 없습니다. 그러나 로그인할 수 있습니다.
7.12.3. UserFederationProviderFactory versus UserStorageProviderFactory
이전 SPI의 동기화 방법은 이제 선택적 ImportSynchronization
인터페이스 내에 캡슐화됩니다. 동기화 논리를 구현한 경우 새 UserStorageProviderFactory
가 ImportSynchronization
인터페이스를 구현하도록 합니다.
7.12.4. 새 모델로 업그레이드
User Storage SPI 인스턴스는 다른 관계형 테이블에 저장됩니다. Red Hat Single Sign-On은 마이그레이션 스크립트를 자동으로 실행합니다. 이전 사용자 페더레이션 공급자가 영역을 위해 배포되는 경우 데이터의 ID를 포함하여 이후 스토리지 모델로 변환됩니다. 이 마이그레이션은 사용자 스토리지 공급자가 이전 사용자 페더 공급자와 동일한 공급자 ID(예: "ldap", "kerberos")인 경우에만 발생합니다.
이를 알고 있는 경우 다양한 접근 방식을 취할 수 있습니다.
- 이전 Red Hat Single Sign-On 배포에서 이전 공급자를 제거할 수 있습니다. 이렇게 하면 가져온 모든 사용자의 로컬 링크 사본이 제거됩니다. 그런 다음 Red Hat Single Sign-On을 업그레이드할 때 해당 영역에 대한 새 공급자를 배포하고 구성하십시오.
-
두 번째 옵션은 새 공급자를 작성하여 새 공급자 ID:
UserStorageProviderFactory.getId()
가 있는지 확인하는 것입니다. 이 공급자가 서버에 배포되었는지 확인합니다. 서버를 부팅하고 기본 제공 마이그레이션 스크립트가 이전 데이터 모델에서 이후 데이터 모델로 변환되도록 합니다. 이 경우 이전에 연결된 모든 사용자가 작동하고 동일합니다.
가져오기 전략을 제거하고 사용자 스토리지 공급자를 다시 작성하려면 Red Hat Single Sign-On을 업그레이드하기 전에 이전 공급자를 제거하는 것이 좋습니다. 이렇게 하면 가져온 사용자의 연결된 로컬 가져오기 사본이 제거됩니다.
7.13. 스트림 기반 인터페이스
Red Hat Single Sign-On의 많은 사용자 스토리지 인터페이스에는 대량 오브젝트 세트를 반환할 수 있는 쿼리 방법이 포함되어 있어 메모리 소비 및 처리 시간 측면에서 상당한 영향을 미칠 수 있습니다. 특히 쿼리 메서드의 논리에 개체의 내부 상태의 작은 하위 집합만 사용되는 경우 더욱 그러합니다.
이러한 쿼리 메서드에서 대규모 데이터 세트를 처리할 수 있는 보다 효율적인 대안을 제공하기 위해 Streams
하위 인터페이스가 사용자 스토리지 인터페이스에 추가되었습니다. 이러한 Streams
하위 인터페이스는 수퍼 인터페이스의 원래 컬렉션 기반 메서드를 스트림 기반 변형으로 교체하여 컬렉션 기반 메서드를 기본값으로 설정합니다. 컬렉션 기반 쿼리 메서드의 기본 구현은 Stream
카운터를 호출하고 적절한 컬렉션 유형으로 결과를 수집합니다.
Streams
하위 인터페이스를 사용하면 구현이 데이터 집합 처리를 위한 스트림 기반 접근 방식에 중점을 두고 해당 접근 방식의 잠재적인 메모리 및 성능 최적화를 활용할 수 있습니다. 구현할 Streams
하위 인터페이스를 제공하는 인터페이스에는 몇 가지 기능 인터페이스, org.keycloak.storage.federated
패키지의 모든 인터페이스 및 사용자 정의 스토리지 구현 범위에 따라 구현할 수 있는 몇 가지 기타 항목이 포함됩니다.
개발자에게 Streams
하위 인터페이스를 제공하는 인터페이스 목록을 참조하십시오.
패키지 | 클래스 |
|
|
|
|
| 모든 인터페이스 |
|
|
인터페이스가 기능 인터페이스임을 나타냅니다.Indicates the interface is a capability interface
스트림 접근 방식을 활용하려는 사용자 정의 사용자 스토리지 구현은 원래 인터페이스 대신 Streams
하위 인터페이스를 구현하는 것입니다. 예를 들어 다음 코드는 UserQueryProvider
인터페이스의 Streams
변형을 사용합니다.
public class CustomQueryProvider extends UserQueryProvider.Streams { ... @Override Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults) { // custom logic here } @Override Stream<UserModel> searchForUserStream(String search, RealmModel realm) { // custom logic here } ... }
public class CustomQueryProvider extends UserQueryProvider.Streams {
...
@Override
Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults) {
// custom logic here
}
@Override
Stream<UserModel> searchForUserStream(String search, RealmModel realm) {
// custom logic here
}
...
}
8장. Vault SPI
8.1. Vault 공급자
org.keycloak.vault
패키지에서 vault SPI를 사용하여 Red Hat Single Sign-On의 사용자 지정 확장을 작성하여 임의의 자격 증명 모음 구현에 연결할 수 있습니다.
기본 제공 files-plaintext
provider는 이 SPI 구현의 예입니다. 일반적으로 다음 규칙이 적용됩니다.
-
시크릿이 여러 영역에서 유출되지 않도록 하려면 영역을 통해 검색할 수 있는 시크릿을 격리하거나 제한할 수 있습니다. 이 경우 공급자는 시크릿을 검색할 때 영역 이름을 고려해야 합니다. 예를 들어 영역 이름을 사용하여 항목 접두사를 붙여야 합니다. 예를 들어,
${vault.key}
표현식은 영역 A 또는 영역 B 에서 사용되었는지 여부에 따라 일반적으로 다른 항목 이름으로 평가됩니다. 영역을 구분하기 위해 영역을KeycloakSession
매개변수에서 사용할 수 있는VaultProvider
Factory.create() -
자격 증명 모음 공급자는 지정된 보안 이름에
VaultRawSecret
을 반환하는 단일 메서드obtainSecret
을 구현해야 합니다. 해당 클래스에는byte[]
또는 10.0.0.1Buffer
로 시크릿의 표현이 있으며 필요에 따라 둘 간에 변환할 수 있습니다. 이 버퍼는 아래에 설명된 대로 사용 후 삭제됩니다.
사용자 지정 공급자를 패키지하고 배포하는 방법에 대한 자세한 내용은 서비스 공급자 인터페이스 장을 참조하십시오.
8.2. 자격 증명 모음에서 값 사용
자격 증명 모음에는 중요한 데이터가 포함되어 있으며 Red Hat Single Sign-On은 시크릿을 적절하게 처리합니다. 시크릿에 액세스할 때 자격 증명 모음에서 시크릿을 가져오고 필요한 시간 동안만 JVM 메모리에 유지됩니다. 그런 다음 JVM 메모리에서 해당 콘텐츠를 삭제하려고 합니다. 이 작업은 아래 설명된 대로 try
-with-resources 문 내에만 자격 증명 모음 시크릿을 사용하여 수행됩니다.
char[] c; try (VaultCharSecret cSecret = session.vault().getCharSecret(SECRET_NAME)) { // ... use cSecret c = cSecret.getAsArray().orElse(null); // if c != null, it now contains password } // if c != null, it now contains garbage
char[] c;
try (VaultCharSecret cSecret = session.vault().getCharSecret(SECRET_NAME)) {
// ... use cSecret
c = cSecret.getAsArray().orElse(null);
// if c != null, it now contains password
}
// if c != null, it now contains garbage
예제에서는 KeycloakSession.vault()
를 시크릿에 액세스하기 위한 진입점으로 사용합니다. VaultProvider.obtainSecret
방법을 직접 사용하는 것도 사실상 가능합니다. 그러나 vault()
메서드에는 원시 시크릿 (일반적으로 바이트 배열)을 문자 배열 (일반적으로 byte 배열) 또는 문자열 (Docker ().get
을 통해)을 해석할 String
Secret()수 있는 기능이 있으며 원래의 해석되지 않은 값 (즉,
메서드를 통해)을 확보할 수 있습니다.
vault()
.getRawSecret()
String
개체는 변경할 수 없으므로 임의 가비지로 재정의하여 해당 콘텐츠를 삭제할 수 없습니다. 문자열 내부를 방지하기 위해 기본 VaultStringSecret
구현에서 측정이 수행되었지만
개체에 저장된 보안은 최소한 다음 GC 라운드에 저장됩니다. 따라서 일반 바이트 및 문자 배열과 버퍼를 사용하는 것이 좋습니다.
String