服务器开发人员指南


Red Hat build of Keycloak 26.0

Red Hat Customer Content Services

摘要

本指南包含开发人员自定义 Keycloak 26.0 的红帽构建的信息。

第 1 章 前言

在一些示例列表中,每行显示的内容不适用于可用页面宽度。这些行已划分。行末尾的 '\' 表示已将中断用于页面中,并缩进以下行:因此:

Let's pretend to have an extremely \
  long line that \
  does not fit
This one is short
Copy to Clipboard Toggle word wrap

确实:

Let's pretend to have an extremely long line that does not fit
This one is short
Copy to Clipboard Toggle word wrap

第 2 章 Admin REST API

红帽构建的 Keycloak 附带一个功能齐全的 Admin REST API,具有管理控制台提供的所有功能。

若要调用 API,您需要获取具有适当权限的访问令牌。服务器管理指南 中描述了所需的权限。

您可以使用红帽构建的 Keycloak 为应用程序启用身份验证来获取令牌。请参阅保护应用程序和服务指南。您还可以使用直接访问授权来获取访问令牌。

2.1. 使用 CURL 的示例

2.1.1. 使用用户名和密码进行身份验证

注意

以下示例假定您在 master 域中 使用密码 创建了 admin 用户,如 Getting Started Guide 指南中所述。

流程

  1. 获取 realm master 中用户的访问令牌,用户名 admin 和密码 password

    curl \
      -d "client_id=admin-cli" \
      -d "username=admin" \
      -d "password=password" \
      -d "grant_type=password" \
      "http://localhost:8080/realms/master/protocol/openid-connect/token"
    Copy to Clipboard Toggle word wrap
    注意

    默认情况下,此令牌在 1 分钟后过期

    结果将是 JSON 文档。

  2. 通过提取 access_token 属性的值来调用所需的 API。
  3. 通过在对 API 的请求 Authorization 标头中包含值来调用 API。

    以下示例演示了如何获取 master 域的详情:

    curl \
      -H "Authorization: bearer eyJhbGciOiJSUz..." \
      "http://localhost:8080/admin/realms/master"
    Copy to Clipboard Toggle word wrap

2.1.2. 使用服务帐户进行身份验证

要使用 client_idclient_secret 对 Admin REST API 进行身份验证,请执行此流程。

流程

  1. 确保客户端配置如下:

    • client_id 属于域 master的机密客户端
    • client_id 启用了 Service Accounts Enabled 选项
    • client_id 有一个自定义"Audience"映射器

      • 包括的客户端 Audience: security-admin-console
  2. 检查 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/realms/master/protocol/openid-connect/token"
Copy to Clipboard Toggle word wrap

2.2. 其他资源

第 3 章 themes

Red Hat build of Keycloak 为网页和电子邮件提供主题支持。这允许自定义面向最终用户的页面的外观和感觉,以便与您的应用程序集成。

图 3.1. 带有 sunrise 示例的登录页面

3.1. 主题类型

主题可以提供一个或多个类型来自定义红帽构建的 Keycloak 的不同方面。可用的类型有:

  • 帐户 - 帐户控制台
  • Admin - 管理控制台
  • 电子邮件 - 电子邮件
  • login - 登录表单
  • Welcome - 欢迎页面

3.2. 配置主题

除 welcome 外,所有主题类型都通过管理控制台进行配置。

流程

  1. 登录管理控制台。
  2. 从左上角的下拉菜单中选择您的域。
  3. 从菜单中选择 Realm Settings
  4. Themes 选项卡。

    注意

    要为 master Admin 控制台设置主题,您需要为 master 域设置 Admin Console 主题。

  5. 要查看对 Admin Console 的更改,请刷新页面。
  6. 使用 spi-theme-welcome-theme 选项更改欢迎主题。
  7. 例如:

    bin/kc.[sh|bat] start --spi-theme-welcome-theme=custom-theme
    Copy to Clipboard Toggle word wrap

3.3. 默认主题

红帽 Keycloak 的构建与服务器分发中的 JAR 文件 keycloak-themes-26.0.15.redhat-00001.jar 中的默认主题捆绑在一起。服务器的根 主题 目录默认不包含任何主题,但它包含一个 README 文件,其中包含有关默认主题的一些额外详情。要简化升级,请不要直接编辑捆绑主题。相反,请创建自己的主题来扩展其中一个捆绑主题。

3.4. 创建主题

主题包括:

除非计划替换每个页面,否则您应该扩展另一个主题。最有可能您要扩展某些现有主题。或者,如果您想提供自己的 admin 或帐户控制台实施,请考虑扩展 基础 主题。该 基础 主题由一个消息捆绑包组成,因此此类实施需要从头开始,包括实施主 index.ftl Freemarker 模板,但它可以使用消息捆绑包中的现有转换。

在扩展主题时,您可以覆盖单个资源(templates、样式表等)。如果您决定覆盖 HTML 模板,您可能需要在升级到新版本时更新自定义模板。

在创建主题时,最好禁用缓存,因为这可以直接从主题目录中编辑主题资源,而无需重启 Red Hat build of Keycloak。

流程

  1. 使用以下选项运行 Keycloak:

    bin/kc.[sh|bat] start --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false
    Copy to Clipboard Toggle word wrap
  2. themes 目录中创建目录。

    目录的名称成为主题的名称。例如,要创建一个名为 mytheme 的主题,请创建目录 themes/mytheme

  3. 在主题目录中,为您的主题将提供的每个类型创建一个目录。

    例如,要将登录类型添加到 mytheme theme 中,请创建目录 themes/mytheme/login

  4. 对于每个类型,创建一个 file theme.properties,它允许为主题设置一些配置。

    例如,要将主题主题/mytheme/login 配置为扩展 基础 主题并导入一些通用资源,请使用以下内容创建文件 themes/mytheme/login /theme.properties

    parent=base
    import=common/keycloak
    Copy to Clipboard Toggle word wrap

    您现在已创建了支持登录类型的主题。

  5. 登录管理控制台以签出您的新主题
  6. 选择您的域
  7. 从菜单中选择 Realm Settings
  8. Themes 选项卡。
  9. 对于 Login Theme,请选择 mytheme,然后单击 Save
  10. 打开域的登录页面。

    您可以通过应用程序登录或打开帐户控制台(/realms/{realm name}/account)来完成此操作。

  11. 要查看更改父主题的影响,请在me .properties 中设置 parent=keycloak 并刷新登录页面。
注意

务必在生产环境中重新启用缓存,因为它将对性能有严重影响。

注意

如果要手动删除主题缓存的内容,可以通过删除服务器分发的 data/tmp/kc-gzip-cache 目录来实现。如果您重新部署了自定义供应商或自定义主题,则在之前的服务器执行中不禁用主题缓存,则它对实例很有用。

3.4.1. 主题属性

me 属性在主题目录中的 &lt ;THEME TYPE>/theme.properties 文件中设置。

  • parent - 涉及要扩展的主题
  • import - 从另一个主题导入资源
  • common - 覆盖通用资源路径。如果没有指定,默认值为 common/keycloak。这个值将用作 ${url.resourcesCommonPath} 的后缀值,它通常用于 freemarker 模板(前缀 ${url.resoucesCommonPath} 值是主题 root uri)。
  • 样式 - 要包括的以空格分开的样式列表
  • locales - 以逗号分隔的支持的区域设置列表

有一些属性列表可用于更改用于特定元素类型的 cs 类。如需这些属性的列表,请查看 keycloak theme 对应的类型中的me.properties 文件(themes/keycloak/<THEME TYPE>/theme.properties)。

您还可以添加自己的自定义属性并从自定义模板中使用它们。

当这样做时,您可以使用以下格式替换系统属性或环境变量:

  • ${some.system.property} - for system properties
  • ${env.ENV_VAR }- 用于环境变量。

也可以在系统属性或环境变量中使用 ${foo:defaultValue} 找到时提供默认值。

注意

如果没有提供默认值,且没有对应的系统属性或环境变量,则不会替换任何内容,并且您最终使用模板中的格式。

下面是一个可能的例子:

javaVersion=${java.version}

unixHome=${env.HOME:Unix home not found}
windowsHome=${env.HOMEPATH:Windows home not found}
Copy to Clipboard Toggle word wrap

3.4.2. 向主题添加一个风格表

您可以在主题中添加一个或多个风格表。

流程

  1. 在主题的 &lt ;THEME TYPE>/resources/css 目录中创建一个文件。
  2. 将此文件添加到 theme.properties 中的 styles 属性中。

    例如,要将 styles.css 添加到 mytheme 中,请使用以下内容创建 themes/mytheme/login/resources/css/styles.css

    .login-pf body {
        background: DimGrey none;
    }
    Copy to Clipboard Toggle word wrap
  3. 编辑 themes/mytheme/login/theme.properties 并添加:

    styles=css/styles.css
    Copy to Clipboard Toggle word wrap
  4. 要查看更改,请打开您的域的登录页面。

    您会注意到应用的唯一风格是来自您的自定义风格表。

  5. 要包含父主题的样式,请从该主题加载样式。编辑 themes/mytheme/login/theme.properties,并将 样式 更改为:

    styles=css/login.css css/styles.css
    Copy to Clipboard Toggle word wrap
    注意

    要覆盖父风格表中的样式,请确保最后列出了您的风格表。

3.4.3. 在主题中添加脚本

您可以在主题中添加一个或多个脚本。

流程

  1. 在主题的 &lt ;THEME TYPE>/resources/js 目录中创建一个文件。
  2. 将此文件添加到 theme.properties 中的 scripts 属性中。

    例如,要将 脚本.js 添加到 mytheme 中,请使用以下内容创建 themes/mytheme/login/resources/js/script.js

    alert('Hello');
    Copy to Clipboard Toggle word wrap

    然后编辑 themes/mytheme/login/theme.properties 并添加:

    scripts=js/script.js
    Copy to Clipboard Toggle word wrap

3.4.4. 将镜像添加到主题中

要使镜像可用于主题,请将它们添加到主题的 &lt ;THEME TYPE>/resources/img 目录中。它们可以从样式表或直接在 HTML 模板中使用。

例如,将镜像添加到 mytheme 将镜像复制到 themes/mytheme/login/resources/img/image.jpg 中。

然后,您可以从自定义风格表中使用此镜像:

body {
    background-image: url('../img/image.jpg');
    background-size: cover;
}
Copy to Clipboard Toggle word wrap

或者直接在 HTML 模板中使用,请将以下内容添加到自定义 HTML 模板中:

<img src="${url.resourcesPath}/img/image.jpg" alt="My image description">
Copy to Clipboard Toggle word wrap

3.4.6. 将镜像添加到电子邮件主题

要使镜像可用于主题,请将它们添加到主题的 &lt ;THEME TYPE>/email/resources/img 目录中。它们可以从 直接在 HTML 模板中使用。

例如,将镜像添加到 mytheme 将镜像复制到 themes/mytheme/email/resources/img/logo.jpg

要直接在 HTML 模板中使用,请将以下内容添加到自定义 HTML 模板中:

<img src="${url.resourcesUrl}/img/image.jpg" alt="My image description">
Copy to Clipboard Toggle word wrap

3.4.7. messages

模板中的文本是从消息捆绑包加载的。扩展另一个主题的主题将继承父消息捆绑包中的所有消息,您可以通过将 < THEME TYPE>/messages/messages_en.properties 添加到您的主题来覆盖单个消息。

例如,要将登录表单上的 Username 替换为 mythemeYour Username,请创建文件 themes/mytheme/login/messages/messages_en.properties 替换为以下内容:

usernameOrEmail=Your Username
Copy to Clipboard Toggle word wrap

在使用消息时,在消息值(如 {0} 和 能)中被替换为参数。例如,登录 {0} 到 {0} 被替换为域的名称。

这些消息捆绑包的文本可以被特定于域的值覆盖。特定于域的值可以通过 UI 和 API 进行管理。

3.4.8. 在域中添加语言

先决条件

流程

  1. 在主题的目录中创建文件 <THEME TYPE>/messages/messages_<LOCALE>.properties
  2. 将此文件添加到 < THEME TYPE>/theme.properties 中的 locales 属性中。要使用户可用的语言是域 登录帐户和 电子邮件,主题必须支持语言,因此您需要为主题类型添加您的语言。

    例如,要将 Norwegian 翻译添加到 mytheme theme 中,请使用以下内容创建文件 themes/mytheme/login/messages/messages_no.properties

    usernameOrEmail=Brukernavn
    password=Passord
    Copy to Clipboard Toggle word wrap

    如果您省略了消息的翻译,将使用英语。

  3. 编辑 themes/mytheme/login/theme.properties 并添加:

    locales=en,no
    Copy to Clipboard Toggle word wrap
  4. 帐户 添加相同的内容并通过 电子邮件 主题类型。为此,可创建 themes/mytheme/account/messages/messages_no.propertiesthemes/mytheme/email/messages/messages_no.properties。将这些文件留空将导致使用英语消息。
  5. themes/mytheme/login/theme.properties 复制到 themes/mytheme/account/theme.propertiesthemes/mytheme/email/theme.properties
  6. 为语言选择器添加翻译。这可以通过在英语翻译中添加消息来完成。要做到这一点,将以下内容添加到 themes/mytheme/account/messages/messages_en.propertiesthemes/mytheme/login/messages/messages_en.properties

    locale_no=Norsk
    Copy to Clipboard Toggle word wrap

默认情况下,消息属性文件应使用 UTF-8 进行编码。如果 Keycloak 无法读取内容为 UTF-8,则 Keycloak 会返回 ISO-8859-1 处理。Unicode 字符可以进行转义,如 Java 文档中用于 PropertyResourceBundle 所述。以前的 Keycloak 版本支持在第一行中指定带有注释的编码,如 # encoding: UTF-8,它不再被支持。

3.4.9. 添加自定义身份提供程序图标

红帽构建的 Keycloak 支持为自定义身份提供程序添加图标,这些图标显示在登录屏幕上。

流程

  1. 在登录 theme.properties 文件中定义图标类(例如: themes/mytheme/login/theme.properties),其键模式为 kcLogoIdP-<alias>
  2. 对于带有别名 myProvider 的身份提供程序,您可以在自定义 主题的me.properties 文件中添加一行。例如:

    kcLogoIdP-myProvider = fa fa-lock
    Copy to Clipboard Toggle word wrap

所有图标都位于 PatternFly4 的官方网站上。社交提供程序的图标已在 基本 登录属性(themes/keycloak/login/theme.properties)中定义,您可以在其中自己激发。

3.4.10. 创建自定义 HTML 模板

Red Hat build of Keycloak 使用 Apache Freemarker 模板来生成 HTML 和呈现页面。

虽然可以创建自定义模板来完全改变页面的呈现方式,但建议尽可能利用内置模板。原因包括:

  • 在升级过程中,您可能被强制更新自定义模板,以便从更新的版本获取最新的更新
  • 通过为主题 配置 CSS 风格,您可以调整 UI 以匹配 UI 设计标准和准则。
  • 用户配置文件 允许您支持自定义用户属性并配置它们的呈现方式。

在大多数情况下,您不需要更改模板来根据您的需求调整红帽 Keycloak 的构建,但您可以通过创建 < THEME TYPE>/<TEMPLATE>.ftl 来覆盖您自己的主题中的各个 模板。管理员和帐户控制台使用单个模板 index.ftl 渲染应用程序。

对于其他主题类型中的模板列表,请查看 $KEYCLOAK _HOME/lib/lib/main/org.keycloak.keycloak-themes-<VERSION>.jar 中的me/base/<THEME_TYPE &gt; 目录。

流程

  1. 将模板从基础主题复制到您自己的主题。
  2. 应用您需要的修改。

    例如,要为 mytheme 主题创建一个自定义登录表单,请将 themes/base/login/login.ftl 复制到 themes/mytheme/login,并在编辑器中打开它。

    第一行(<#import …​>)后,添加 & lt;h1>HELLO WORLD!</h1& gt;,如下所示:

    <#import "template.ftl" as layout>
    <h1>HELLO WORLD!</h1>
    ...
    Copy to Clipboard Toggle word wrap
  3. 备份修改后的模板。当升级到一个新版本的 Red Hat build of Keycloak 时,您可能需要更新自定义模板,以便在适用的情况下对原始模板应用更改。

3.4.11. 电子邮件

要编辑电子邮件的主题和内容,如密码恢复电子邮件,请在主题 的电子邮件 类型中添加一个消息捆绑包。每个电子邮件都有三个消息。一个用于主题,一个用于纯文本正文,另一个用于 html 正文。

要查看所有可用的电子邮件,请查看 themes/base/email/messages/messages_en.properties

例如,要更改 mytheme 的密码恢复电子邮件,使用以下内容创建 themes/mytheme/email/messages/messages_en.properties

passwordResetSubject=My password recovery
passwordResetBody=Reset password link: {0}
passwordResetBodyHtml=<a href="{0}">Reset password</a>
Copy to Clipboard Toggle word wrap

3.5. 部署主题

通过将主题目录复制到主题或者将其部署为存档,可以部署到红帽构建的 Keycloak 中。在开发过程中,您可以将主题复制到 主题 目录,但在生产中,您可能需要考虑使用 存档一个存档 可以更轻松地拥有主题版本的副本,特别是当您有多个红帽构建的 Keycloak 实例(例如使用集群)时。

流程

  1. 要将主题部署为存档,请使用主题资源创建一个 JAR 存档。
  2. 将文件 META-INF/keycloak-themes.json 添加到存档中可用的主题的存档中,以及每个主题提供的类型。

    例如,对于 mytheme theme create 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" ]
          }]
      }
      Copy to Clipboard Toggle word wrap

      一个存档可以包含多个主题,每个主题可以支持一个或多个类型。

要将存档部署到红帽构建的 Keycloak 中,将其添加到红帽构建的 Keycloak 的 provider/ 目录中,如果已在运行,重启服务器。

第 4 章 基于 React 的 themes

管理控制台和帐户控制台基于 React。要完全自定义它们,您可以使用基于 React 的 npm 软件包。有两个软件包:

  • @Keycloak/keycloak-admin-ui :这是管理控制台的基础主题。
  • @Keycloak/keycloak-account-ui :这是帐户控制台的基础主题。

npm 上提供了两个软件包。

4.1. 安装软件包

要安装软件包,请运行以下命令:

pnpm install @keycloak/keycloak-account-ui
Copy to Clipboard Toggle word wrap

4.2. 使用软件包

要使用这些页面,您需要在组件层次结构中添加 KeycloakProvider,以设置要使用的客户端、域和 url。

import { KeycloakProvider } from "@keycloak/keycloak-ui-shared";

//...

<KeycloakProvider environment={{
      serverBaseUrl: "http://localhost:8080",
      realm: "master",
      clientId: "security-admin-console"
  }}>
  {/* rest of you application */}
</KeycloakProvider>
Copy to Clipboard Toggle word wrap

4.3. 转换页面

页面使用 i18next 库进行转换。您可以根据其 [website](https://react.i18next.com/)进行设置。如果要使用提供的翻译,则需要将 i18next-http-backend 添加到项目中并添加:

backend: {
  loadPath: `http://localhost:8080/resources/master/account/{lng}}`,
  parse: (data: string) => {
    const messages = JSON.parse(data);

    const result: Record<string, string> = {};
    messages.forEach((v) => (result[v.key] = v.value)); //need to convert to record
    return result;
  },
},
Copy to Clipboard Toggle word wrap

4.4. 使用页面

所有"页面"都是 React 组件,可在您的应用程序中使用。要查看可用的组件,请参阅 [source](https://github.com/keycloak/keycloak/blob/main/js/apps/account-ui/src/index.ts)。或者查看 [quick start](https://github.com/keycloak/keycloak-quickstarts/tree/main/extension/extend-admin-console-node)以了解如何使用它们。

4.5. theme 选择器

默认情况下,使用为域配置的主题,但客户端无法覆盖登录主题。此行为可以通过 Theme Selector SPI 来更改。

这可用于通过查看用户代理标头来选择桌面和移动设备的不同主题,例如:

要创建需要实现 ThemeSelectorProviderFactoryThemeSelectorProvider Provider 的自定义主题选择器。

4.6. 主题资源

在 Red Hat build of Keycloak 中实施自定义供应商时,可能需要添加额外的模板、资源和消息捆绑包。

加载额外主题资源的最简单方法是,在me-resources/resources 中的 me-resources/resources 和 messages 捆绑包中的 me -resources/templates 资源中创建模板

如果您希望更灵活的方式加载可通过 ThemeResourceSPI 实现的模板和资源。通过实施 ThemeResourceProviderFactoryThemeResourceProvider,您可以决定如何准确加载模板和资源。

4.7. 区域设置选择器

默认情况下,使用 DefaultLocaleSelectorProvider 选择区域设置,它实现了 LocaleSelectorProvider 接口。当禁用国际化时,英语是默认语言。

启用国际化后,会根据 服务器管理指南 中所述的逻辑解析区域设置。

此行为可以通过 LocaleSelector Provider 和 LocaleSelectorProvider Factory 来更改。

LocaleSelectorProvider 接口具有单一方法 resolveLocale,它必须返回给定 RealmModel 和 nullable UserModel 的区域设置。实际请求可以通过 KeycloakSession#getContext 方法获得。

自定义实现可以扩展 DefaultLocaleSelectorProvider,以便重复使用部分默认行为。例如,要忽略 Accept-Language 请求标头,自定义实施可以扩展默认提供程序,覆盖它的 getAcceptLanguageHeaderLocale,并返回 null 值。因此,区域设置选择将回退到域的默认语言。

第 5 章 身份代理 API

红帽构建的 Keycloak 可以将身份验证委托给父 IDP 以进行登录。其中一个典型的示例是您希望用户通过一个社交供应商(如 Facebook 或 Google)登录。您还可以将现有帐户链接到代理的 IDP。本节论述了应用程序可以与身份代理相关的一些 API。

5.1. 检索外部 IDP 令牌

红帽构建的 Keycloak 允许您使用外部 IDP 存储来自身份验证过程的令牌和响应。为此,您可以在 IDP 的设置页面中使用 Store Token 配置选项。

应用程序代码可以检索这些令牌和响应,以拉取额外的用户信息,或者安全地调用外部 IDP 上的请求。例如,应用程序可能希望使用 Google 令牌在其他 Google 服务和 REST API 上调用。要检索特定身份提供程序的令牌,您需要发送请求,如下所示:

GET /realms/{realm}/broker/{provider_alias}/token HTTP/1.1
Host: localhost:8080
Authorization: Bearer <KEYCLOAK ACCESS TOKEN>
Copy to Clipboard Toggle word wrap

应用程序必须使用红帽构建的 Keycloak 进行身份验证,并收到访问令牌。此访问令牌需要设置 代理 客户端级角色 read-token。这意味着,用户必须拥有此角色的角色映射,并且客户端应用程序必须在其范围内拥有该角色。在这种情况下,如果您在红帽构建的 Keycloak 中访问受保护的服务,您需要在用户身份验证过程中发送由红帽构建 Keycloak 发布的访问令牌。在代理配置页面中,您可以通过打开 Stored Tokens Readable 开关,自动将此角色分配给新导入的用户。

这些外部令牌可以通过提供程序再次登录或使用启动的帐户链接 API 重新建立。

5.2. 客户端启动帐户链接

有些应用程序希望与 Facebook 等社交提供商集成,但不想通过这些社交提供商登录。红帽 Keycloak 的构建提供了基于浏览器的 API,应用程序可以使用该 API 将现有用户帐户链接到特定的外部 IDP。这称为客户端发起的帐户链接。帐户链接只能由 OIDC 应用程序启动。

它的工作方式是,应用程序将用户的浏览器转发到红帽构建的 Keycloak 服务器的 URL,要求其要将用户的帐户链接到特定的外部提供程序(如bookbook)。服务器使用外部提供程序启动登录。浏览器从外部提供程序登录,并重定向到服务器。服务器建立链接,并通过确认重定向到应用程序。

在启动此协议前,客户端应用程序必须满足一些条件:

  • 必须在管理控制台中为用户域配置并启用所需的身份提供程序。
  • 用户帐户必须通过 OIDC 协议以现有用户登录
  • 用户必须具有 account.manage-accountaccount.manage-account-links 角色映射。
  • 必须在其访问令牌内授予这些角色的范围
  • 应用必须有权访问其访问令牌,因为它需要其中的信息来生成重定向 URL。

若要启动登录,应用必须结构化 URL,并将用户的浏览器重定向到此 URL。URL 类似如下:

/{auth-server-root}/realms/{realm}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash}
Copy to Clipboard Toggle word wrap

以下是每个路径和查询参数的描述:

provider
这是您在管理控制台 Identity Provider 部分中定义的外部 IDP 的供应商别名。
client_id
这是应用程序的 OIDC 客户端 ID。在 admin 控制台中将应用程序注册为客户端时,您必须指定此客户端 ID。
redirect_uri
这是您要在帐户链接建立后要重定向到的应用程序回调 URL。它必须是有效的客户端重定向 URI 模式。换句话说,它必须与您在管理控制台中注册客户端时定义的有效 URL 模式之一匹配。
nonce
这是应用程序必须生成的随机字符串
hash
这是一个 Base64 URL 编码哈希。此哈希由 Base64 URL 编码为 一个非ce + token.getSessionState () + token.getIssuedFor () + provider 的 SHA_256 哈希生成。令牌变量从 OIDC 访问令牌获取。您基本上是对随机非ce、用户会话 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("/realms/{realm}/broker/{provider}/link")
                    .queryParam("nonce", nonce)
                    .queryParam("hash", hash)
                    .queryParam("client_id", clientId)
                    .queryParam("redirect_uri", redirectUri).build(realm, provider).toString();
Copy to Clipboard Toggle word wrap

为什么包含此哈希?我们这样做的目的是保证身份验证服务器可以保证客户端应用程序发起请求,而其他相关应用程序都没有随机要求用户帐户链接到特定提供程序。身份验证服务器将首先通过检查登录时设置的 SSO cookie 来检查用户是否已登录。然后,它将尝试根据当前登录来重新生成哈希,并将其与应用程序发送的哈希匹配。

链接帐户后,身份验证服务器将重定向到 redirect_uri。如果提供链接请求存在问题,则 auth 服务器可能会或可能无法重定向到 redirect_uri。浏览器可能只是在错误页面结束,而不是重新重定向到应用。如果存在错误条件,并且 auth 服务器会足够安全重定向到客户端应用,则额外的 错误 查询参数将附加到 redirect_uri 中。

警告

虽然此 API 保证应用程序启动请求,但它不会完全防止 CSRF 攻击此操作。应用程序仍负责保护 CSRF 攻击目标。

5.2.1. 刷新外部令牌

如果您使用通过登录到供应商(例如 Facebook 或 GitHub 令牌)生成的外部令牌,您可以通过重新发起帐户链接 API 来刷新此令牌。

第 6 章 服务供应商接口(SPI)

红帽 Keycloak 的构建旨在在不需要自定义代码的情况下涵盖大多数用例,但我们希望它能够自定义。为了实现此红帽构建的 Keycloak 具有多个服务提供商接口(SPI),您可在其中实施自己的供应商。

6.1. 实施 SPI

要实施 SPI,您需要实施其 ProviderFactory 和 Provider 接口。您还需要创建服务配置文件。

例如,要实施 Theme Selector SPI,您需要实现 ThemeSelectorProviderFactory 和 ThemeSelectorProvider,并提供文件 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";
    }
}
Copy to Clipboard Toggle word wrap

建议您的供应商工厂实施通过方法 getId () 返回唯一 id。但是,覆盖供应商 部分中所述,此规则可能存在一些例外情况。

注意

红帽 Keycloak 的构建创建了单一供应商工厂实例,从而可以为多个请求存储状态。通过在工厂中为各个请求调用 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() {
    }
}
Copy to Clipboard Toggle word wrap

服务配置文件示例(META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory):

org.acme.provider.MyThemeSelectorProviderFactory
Copy to Clipboard Toggle word wrap

要配置您的供应商,请参阅配置供应商 章节。

例如,要配置供应商,您可以设置选项,如下所示:

bin/kc.[sh|bat] --spi-theme-selector-my-theme-selector-enabled=true --spi-theme-selector-my-theme-selector-theme=my-theme
Copy to Clipboard Toggle word wrap

然后,您可以在 ProviderFactory init 方法中检索配置:

public void init(Config.Scope config) {
    String themeName = config.get("theme");
}
Copy to Clipboard Toggle word wrap

如果需要,您的供应商也可以查找其他供应商。例如:

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();
    }
}
Copy to Clipboard Toggle word wrap

SPI 的 pom.xml 文件需要一个 dependencyManagement 部分,其中包含红帽构建的用于 SPI 的 Keycloak 版本的导入引用。在本例中,将 VERSION 替换为 26.0.15.redhat-00001,这是 Red Hat build of Keycloak 的当前版本。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>test-lib</artifactId>
  <version>1.0-SNAPSHOT</version>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-parent</artifactId>
        <version>VERSION</version> 
1

        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.keycloak</groupId>
      <artifactId>keycloak-model-jpa</artifactId>
      <scope>provided</scope>
    </dependency>
  </dependencies>

</project>
Copy to Clipboard Toggle word wrap

<.> 将 VERSION 替换为 Red Hat build of Keycloak 的当前版本

6.1.1. 覆盖内置供应商

如上所述,建议您的 ProviderFactory 实现使用唯一的 ID。但是,同时覆盖红帽构建的 Keycloak 内置供应商可能很有用。推荐方法是带有唯一 ID 的 ProviderFactory 实现,然后为实例设置默认供应商,如 配置提供程序 章节中所述。另一方面,这可能并不总是可行。

例如,当您需要对默认的 OpenID Connect 协议进行一些自定义,并且您要覆盖 OIDCLoginProtocolFactory 的 Keycloak 实现的默认红帽构建,您需要保留相同的 providerId。例如,admin 控制台、OIDC 协议已知的端点和其他一些因素依赖协议工厂的 ID 为 openid-connect

在这种情况下,强烈建议实现自定义实现的方法 顺序(),并确保它的顺序高于内置实施。

public class CustomOIDCLoginProtocolFactory extends OIDCLoginProtocolFactory {

    // Some customizations here

    @Override
    public int order() {
        return 1;
    }
}
Copy to Clipboard Toggle word wrap

如果有多个具有相同供应商 ID 的实现,红帽构建的 Keycloak 运行时只能使用具有最高顺序的实现。

6.1.2. 显示管理控制台中您的 SPI 实施的信息

有时,向红帽构建的 Keycloak 管理员显示有关您的供应商的附加信息。您可以显示供应商构建时间信息(例如,当前安装的自定义供应商版本)、提供程序的当前配置(例如,您的供应商与远程系统通信的远程系统的 url)或一些操作信息(来自您供应商的响应的平均时间)。Red Hat build of Keycloak Admin Console 提供了 Server Info 页面来显示此类信息。

若要显示您的提供程序的信息,足以在您的 ProviderFactory 中实施 org.keycloak.provider.ServerInfoAware ProviderFactory 接口。

上例中的 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;
    }
}
Copy to Clipboard Toggle word wrap

6.2. 使用可用的供应商

在供应商实现中,您可以使用红帽构建的 Keycloak 中提供的其他供应商。现有的提供程序通常可以通过使用 KeycloakSession 来检索,该提供程序可用于您的提供程序,如 实施 SPI 部分中所述

Red Hat build of Keycloak 有两个供应商类型:

  • 单实施供应商类型 - 红帽构建的 Keycloak 运行时只能有一个特定供应商类型的活跃实现。

    例如 HostnameProvider 指定红帽构建的 Keycloak 以及为整个红帽构建的 Keycloak 服务器共享的主机名。因此,只能针对红帽构建的 Keycloak 服务器激活此供应商的单一实现。如果服务器运行时有多个提供程序实施,则其中一个需要指定为默认值。

例如:

bin/kc.[sh|bat] build --spi-hostname-provider=default
Copy to Clipboard Toggle word wrap

用作 default -provider 的值的默认值必须与特定供应商工厂实现的 ProviderFactory.getId () 返回的 ID 匹配。在代码中,您可以获取 keycloakSession.getProvider (HostnameProvider.class)等供应商

  • 多个实现供应商类型 - 称为供应商类型,允许在红帽构建的 Keycloak 运行时提供多个实现。

    例如,EventListener 供应商允许有多个可用的和注册,这意味着特定的事件可以发送到所有监听器(jboss-logging、sysout 等)。在代码中,您可以获取指定的供应商实例,如 session.getProvider (EventListener.class, "jboss-logging")。您需要将 provider_id 指定为第二个参数,因为可以有此提供程序类型的多个实例,如上所述。

    供应商 ID 必须与特定供应商工厂实施的 ProviderFactory.getId () 返回的 ID 匹配。某些提供程序类型可以通过使用 ComponentModel 作为第二个参数来检索,一些(如 Authenticator),甚至需要使用 KeycloakSessionFactory 检索。不建议以这种方式实施自己的供应商,因为它可能会在以后被弃用。

6.3. 注册供应商实现

通过将 JAR 文件复制到提供程序目录,将 提供程序 注册到服务器。

如果您的供应商需要没有由 Keycloak 提供的额外依赖项,请将它们复制到 供应商 目录中。

注册新供应商或依赖项 Keycloak 后,需要使用非优化启动或 kc.[sh|bat] build 命令重建。

注意

供应商 JAR 不加载在隔离的类加载器中,因此不要包含与内置资源或类冲突的供应商 JAR 中的资源或类。特别是包含 application.properties 文件或覆盖 commons-lang3 依赖项,如果删除了供应商 JAR,则 auto-build 会导致 auto-build 失败。如果您包含冲突的类,您可能会在服务器的启动日志中看到 split package 警告。不幸的是,并非所有内置 lib jar 通过 split 软件包警告逻辑检查,因此您需要在捆绑或包含传输依赖关系之前检查 lib 目录 JAR。如果存在冲突,可以通过删除或重新打包出错类来解决。

如果您已有冲突的资源文件,则不会发出警告。您应该确保 JAR 的资源文件具有包含该提供程序唯一内容的路径名称,或者您可以检查 "install root"/lib/lib/main 目录下的 JAR 内容中是否存在 some.file,如下所示:

find . -type f -name "*.jar" -exec unzip -l {} \; | grep some.file
Copy to Clipboard Toggle word wrap

如果发现您的服务器因为与已删除的供应商 JAR 相关的 NoSuchFileException 错误而无法启动,则运行:

./kc.sh -Dquarkus.launch.rebuild=true --help
Copy to Clipboard Toggle word wrap

这将强制 Quarkus 重建相关的索引文件。您可以从那里执行非优化的启动或构建,而无需例外。

6.3.1. 禁用供应商

您可以通过将供应商的 enabled 属性设置为 false 来禁用供应商。例如,禁用 Infinispan 用户缓存提供程序使用:

bin/kc.[sh|bat] build --spi-user-cache-infinispan-enabled=false
Copy to Clipboard Toggle word wrap

6.4. JavaScript 提供程序

注意

脚本 是技术预览,且不受支持。此功能默认为禁用。

使用-- features=preview or-- features=scripts启动服务器

Red Hat build of Keycloak 能够在运行时执行脚本,以便管理员能够自定义特定的功能:

  • 身份验证器
  • JavaScript Policy
  • OpenID Connect 协议映射程序
  • SAML 协议映射程序

6.4.1. 身份验证器

身份验证脚本必须至少提供以下功能之一:auth (..),它从 Authenticator"authenticate (AuthenticationFlowContext)action (..) 操作调用,它从 Authenticator askaction (AuthenticationFlowContext)调用。

自定义 验证器应至少提供身份验证 (..) 函数。您可以在代码中使用 javax.script.Bindings 脚本。

script
用于访问脚本元数据的 ScriptModel
realm
RealmModel
user
当前的 UserModel。请注意,当脚本验证器以另一个验证器成功建立 用户身份 后触发的身份验证流中配置时,用户就可用,并将用户设置为身份验证会话。
会话
活跃的 KeycloakSession
authenticationSession
当前 AuthenticationSessionModel
httpRequest
the current org.jboss.resteasy.spi.HttpRequest
LOG
org.jboss.logging.Logger 范围为 ScriptBasedAuthenticator
注意

您可以从传递给 authentication (context ) action ( context ) 函数的 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();
}
Copy to Clipboard Toggle word wrap
6.4.1.1. 在何处添加脚本验证器

脚本验证器的可能用途是在身份验证结束时进行一些检查。请注意,如果您希望使用身份 cookie 在 SSO 重新身份验证期间始终触发脚本验证器(即使是实例),您可能需要在身份验证流末尾将其添加为 REQUIRED,并将现有的验证器封装到单独的 REQUIRED 身份验证子流中。这是因为 REQUIRED 和 ALTERNATIVE 执行应该不在同一级别上。例如,身份验证流配置应如下所示:

- User-authentication-subflow REQUIRED
-- Cookie ALTERNATIVE
-- Identity-provider-redirect ALTERNATIVE
...
- Your-Script-Authenticator REQUIRED
Copy to Clipboard Toggle word wrap

6.4.2. OpenID Connect 协议映射程序

OpenID Connect 协议映射程序脚本是 javascript 脚本,允许您更改 ID Token 和/或 访问令牌的内容。

您可以在代码中使用 javax.script.Bindings 脚本。

user
当前 UserModel
realm
RealmModel
token
当前的 IDToken。只有在为 ID 令牌配置了映射程序时,它才可用。
tokenResponse
当前的 AccessTokenResponse。只有在为访问令牌配置了映射程序时,它才可用。
userSession
active UserSessionModel
keycloakSession
活跃的 KeycloakSession

脚本的导出将用作令牌声明的值。

// prints can be used to log information for debug purpose.
print("STARTING CUSTOM MAPPER");

var inputRequest = keycloakSession.getContext().getHttpRequest();
var params = inputRequest.getDecodedFormParameters();
var output = params.getFirst("user_input");
exports = output;
Copy to Clipboard Toggle word wrap

以上脚本允许从授权请求检索 user_input。这将可用于映射在映射程序中配置的 Token Claim Name 中。

6.4.3. 使用要部署的脚本创建 JAR

注意

JAR 文件是带有 .jar 扩展名的常规 ZIP 文件。

为了让脚本可用于红帽构建的 Keycloak,您需要将它们部署到服务器中。为此,您应该创建一个具有以下结构的 JAR 文件:

META-INF/keycloak-scripts.json

my-script-authenticator.js
my-script-policy.js
my-script-mapper.js
Copy to Clipboard Toggle word wrap

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"
        }
    ]
}
Copy to Clipboard Toggle word wrap

此文件应引用您要部署的不同类型的脚本供应商:

  • authenticators

    用于 OpenID Connect 脚本身份验证器。您可以在同一 JAR 文件中具有一个或多个验证器

  • policies

    对于使用红帽构建的 Keycloak 授权服务时的 JavaScript 策略。在同一 JAR 文件中可以拥有一个或多个策略

  • mappers

    用于 OpenID Connect 脚本协议映射程序。您可以在同一 JAR 文件中具有一个或多个映射程序

  • saml-mappers

    对于 SAML 脚本协议映射程序。您可以在同一 JAR 文件中具有一个或多个映射程序

对于 JAR 文件中的每个脚本文件,您需要在 META-INF/keycloak-scripts.json 中对应的条目,用于将脚本文件映射到特定的提供程序类型。为此,您应该为每个条目提供以下属性:

  • name

    通过红帽构建的 Keycloak 管理控制台来显示脚本的友好名称。如果没有提供,则使用脚本文件的名称替代

  • description

    更好地描述脚本文件计划的可选文本

  • fileName

    脚本文件的名称。此属性 是必需的,应映射到 JAR 中的文件。

6.4.4. 部署脚本 JAR

有描述符和您要部署的脚本的 JAR 文件后,您只需要将 JAR 复制到红帽构建的 Keycloak 供应商/目录,然后运行 bin/ kc.[sh|bat] 构建

6.5. 可用的 SPI

如果要在运行时查看所有可用 SPI 列表,您可以在管理门户中检查 Server Info 页面,如 Admin Console 部分所述。

第 7 章 用户存储 SPI

您可以使用用户存储 SPI 为红帽构建的 Keycloak 编写扩展,以连接到外部用户数据库和凭证存储。内置的 LDAP 和 ActiveDirectory 支持是此 SPI 在操作中实现的。开箱即用,红帽构建的 Keycloak 使用其本地数据库来创建、更新和查找用户并验证凭证。通常,机构已有现有的外部专有用户数据库,它们无法迁移到红帽构建的 Keycloak 的数据模型。对于这样的情况,应用程序开发人员可以编写用户存储 SPI 的实现来桥接外部用户存储,以及红帽构建的 Keycloak 用来登录和管理它们的内部用户对象模型。

当红帽构建的 Keycloak 运行时需要查找用户时,比如当用户登录时,它会执行多个步骤来定位用户。首先查看用户是否在用户缓存中;如果用户找到了,则使用该内存中表示。然后,它会在红帽构建的 Keycloak 本地数据库中查找用户。如果没有找到用户,则会循环通过 User Storage SPI 供应商实施来执行用户查询,直到其中一个用户返回运行时查找的用户。供应商查询外部用户存储的用户,并将用户的外部数据表示映射到红帽构建的 Keycloak 用户 metamodel。

用户存储 SPI 供应商实现也可以执行复杂的条件查询,为用户执行 CRUD 操作,验证和管理凭证,或者一次性执行许多用户的批量更新。它取决于外部存储的功能。

用户存储 SPI 提供程序实施的打包并部署与 Jakarta EE 组件类似。默认情况下,它们不会被启用,但必须在管理控制台的 User Federation 选项卡中为每个域启用和配置。

警告

如果您的用户提供程序实施使用一些用户属性作为链接/建立用户身份的元数据属性,请确保用户无法编辑属性,并且相应的属性是只读。示例是 LDAP_ID 属性,它内置的红帽 Keycloak LDAP 供应商构建用于将用户 ID 存储在 LDAP 服务器端。请参阅 Threat 模型缓解章节中 的详细信息。

红帽构建的 Keycloak Quickstarts Repository 中有两个示例项目。每个快速入门都有一个 README 文件,其中包含如何构建、部署和测试示例项目的说明。下表提供了可用用户存储 SPI 快速入门的简单描述:

Expand
表 7.1. 用户存储 SPI Quickstarts
Name描述

user-storage-jpa

演示使用 JPA 实施用户存储提供程序。

user-storage-simple

演示使用包含用户名/密码密钥对的简单属性文件实施用户存储提供程序。

7.1. 供应商接口

在构建用户存储 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) {

    }

}
Copy to Clipboard Toggle word wrap

您可能会认为 UserStorageProvider 接口是稀疏的?本章稍后会看到,您的供应商可以实施其他混合接口来支持用户集成的机制。

UserStorageProvider 实例会在每次事务创建一次。当事务完成后,会调用 UserStorageProvider.close () 方法,然后收集实例。实例由供应商工厂创建。Provider factories 实施 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);
...
}
Copy to Clipboard Toggle word wrap

在实施 UserStorageProviderFactory 时,供应商工厂类必须将 concrete 供应商类指定为 template 参数。这必须作为运行时内省此类,以扫描其功能(它所实施的其他接口)。例如,如果您的供应商类命名为 FileProvider,则 factory 类应类似如下:

public class FileProviderFactory implements UserStorageProviderFactory<FileProvider> {

    public String getId() { return "file-provider"; }

    public FileProvider create(KeycloakSession session, ComponentModel model) {
       ...
    }
Copy to Clipboard Toggle word wrap

getId () 方法返回 User Storage 提供程序的名称。当您要为特定域启用供应商时,管理控制台的 User Federation 页面中会显示此 id。

create () 方法负责分配提供程序类的实例。它采用 org.keycloak.models.KeycloakSession 参数。此对象可用于查找其他信息和元数据,并提供对运行时中各种其他组件的访问。ComponentModel 参数代表如何在特定域中启用和配置提供程序。它包含已启用供应商的实例 ID,以及在通过管理控制台启用时为它指定的任何配置。

UserStorageProviderFactory 还有其他功能,我们将在本章后续部分中介绍它们。

7.2. 供应商功能接口

如果您仔细检查了 UserStorageProvider 接口,您可能会发现它不会定义任何查找或管理用户的方法。这些方法实际上会在其他功能 接口 中定义,具体取决于外部用户存储可以提供和执行的功能范围。例如,一些外部存储是只读的,只能执行简单的查询和凭证验证。您只需要为能够以下功能实施 功能接口。您可以实现这些接口:

Expand
SPI描述

org.keycloak.storage.user.UserLookupProvider

如果您想能够使用来自此外部存储的用户登录,则需要此接口。大多数(全部?)提供商实施此接口。

org.keycloak.storage.user.UserQueryMethodsProvider

定义用于查找一个或多个用户的复杂查询。如果要从管理控制台查看和管理用户,则必须实施此接口。

org.keycloak.storage.user.UserCountMethodsProvider

如果您的供应商支持计数查询,实施此接口。

org.keycloak.storage.user.UserQueryProvider

这个界面是 UserQueryMethodsProviderUserCountMethodsProvider 的组合功能。

org.keycloak.storage.user.UserRegistrationProvider

如果您的供应商支持添加和删除用户,实施此接口。

org.keycloak.storage.user.UserBulkUpdateProvider

如果您的供应商支持批量更新一组用户,实施此接口。

org.keycloak.credential.CredentialInputValidator

如果您的供应商可以验证一个或多个不同的凭证类型(例如,如果您的供应商可以验证密码),实施此接口。

org.keycloak.credential.CredentialInputUpdater

如果您的供应商支持更新一个或多个不同的凭证类型,实施此接口。

7.3. 模型接口

功能 接口 中定义的大多数方法都返回,或以用户的表示形式传递。这些表示由 org.keycloak.models.UserModel 接口定义。需要应用程序开发人员才能实施此接口。它为外部用户存储和红帽构建的 Keycloak 使用的用户 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);
...
}
Copy to Clipboard Toggle word wrap

UserModel 实现提供对用户的读取和更新元数据的访问,包括用户名、名称、电子邮件、角色和组映射等内容,以及其他任意属性。

org.keycloak.models 软件包中的其他模型类代表 Red Hat build of Keycloak metamodel: RealmModel,RoleModel,GroupModel, 和 ClientModel

7.3.1. 存储 Ids

UserModel 的一个重要方法是 getId () 方法。在实施 UserModel 时,开发人员必须了解用户 ID 格式。格式必须是:

"f:" + component id + ":" + external id
Copy to Clipboard Toggle word wrap

红帽 Keycloak 运行时构建通常必须根据用户 ID 查找用户。用户 id 包含足够的信息,因此运行时不必查询系统中的每个单一 UserStorageProvider 来查找用户。

组件 id 是从 ComponentModel.getId () 返回的 id。组件Model 在创建供应商类时作为参数传递,以便您可以从那里获取它。外部 id 是您的供应商类需要在外部存储中查找用户的信息。这通常是用户名或 uid。例如,它可能类似如下:

f:332a234e31234:wburke
Copy to Clipboard Toggle word wrap

当运行时按 id 进行查找时,将解析 id 来获取组件 ID。组件 id 用于找到最初用于加载用户的 UserStorageProvider。然后,该提供程序通过 id。此提供程序再次解析 id 以获取外部 ID,它将使用 在外部用户存储中定位用户。

这个格式具有可为外部存储用户生成长 ID 的缺陷。这在与 WebAuthn 身份验证 结合使用时,这特别重要,这会将用户处理 ID 限制为 64 字节。因此,如果存储用户使用 WebAuthn 身份验证,则务必要将完整的存储 ID 限制为 64 个字符。method validateConfiguration 可用于在创建时为提供程序组件分配简短 ID,为 64 字节限制的用户 ID 提供一些空间。

    @Override
    void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
            throws ComponentValidationException
    {
        // ...
        if (model.getId() == null) {
            // On creation use short UUID of 22 chars, 40 chars left for the user ID
            model.setId(KeycloakModelUtils.generateShortId());
        }
    }
Copy to Clipboard Toggle word wrap

7.4. 打包和部署

要让红帽构建的 Keycloak 可识别提供程序,您需要向 JAR 添加一个文件: META-INF/services/org.keycloak.storage.UserStorageProviderFactory。此文件必须包含 UserStorageProviderFactory 实现的完全限定类名称列表:

org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
Copy to Clipboard Toggle word wrap

要部署此 jar,请将它复制到 providers/ 目录,然后运行 bin/kc.[sh|bat] build

7.5. 简单只读、查找示例

为了说明实施用户存储 SPI 的基础知识,让我们来逐步介绍一个简单的示例。在本章中,您将看到一个简单的 UserStorageProvider 的实现,该方法可在简单的属性文件中查找用户。属性文件包含用户名和密码定义,并硬编码到 classpath 上的特定位置。提供程序将能够按照 ID 和用户名查找用户,并可验证密码。源自此提供程序的用户将只读。

7.5.1. Provider 类

首先,我们将介绍 UserStorageProvider 类。

public class PropertyFileUserStorageProvider implements
        UserStorageProvider,
        UserLookupProvider,
        CredentialInputValidator,
        CredentialInputUpdater
{
...
}
Copy to Clipboard Toggle word wrap

我们的供应商类 PropertyFileUserStorageProvider 实现了许多接口。它实现了 UserStorageProvider,因为这是 SPI 的基本要求。它实现了 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;
    }
Copy to Clipboard Toggle word wrap

此提供程序类的构造器将存储对 KeycloakSessionComponentModel 和 property 文件的引用。我们稍后将使用所有这些内容。另请注意,有加载的用户映射。每当我们发现一个用户时,我们将将其存储在此映射中,以便我们避免在同一事务中再次重新创建它。这是遵循许多提供商需要执行此操作的良好做法(即,任何与 JPA 集成的任何供应商)。另请记住,每个事务后都会创建提供程序类实例,并在事务完成后关闭。

7.5.1.1. UserLookupProvider 实现
    @Override
    public UserModel getUserByUsername(RealmModel realm, String username) {
        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(RealmModel realm, String id) {
        StorageId storageId = new StorageId(id);
        String username = storageId.getExternalId();
        return getUserByUsername(realm, username);
    }

    @Override
    public UserModel getUserByEmail(RealmModel realm, String email) {
        return null;
    }
Copy to Clipboard Toggle word wrap

当用户登录时,Red Hat build of Keycloak 登录页面会调用 getUserByUsername () 方法。在我们的实现中,我们首先检查 loadUsers 映射,以查看该用户是否已加载在这个事务中。如果没有加载,我们会在属性文件中查看 username。如果存在,我们创建一个 UserModel 的实现,将其保存在 loadUsers 中以备将来参考,并返回这个实例。

createAdapter () 方法使用帮助程序类 org.keycloak.storage.adapter.AbstractUserAdapter。这为 UserModel 提供了一个基础实施。它使用用户的用户名作为外部 ID,根据所需的存储 id 格式自动生成用户 ID。

"f:" + component id + ":" + username
Copy to Clipboard Toggle word wrap

每一个 get 方法的 AbstractUserAdapter 都会返回 null 或空集合。但是,返回角色和组映射的方法将返回为每个用户配置的默认角色和组。AbstractUserAdapter 的每个集合方法都会抛出 org.keycloak.storage.ReadOnlyException。因此,如果您试图在 Admin 控制台中修改用户,则会出现错误。

getUserById () 方法使用 org.keycloak.storage.StorageId helper 类解析 id 参数。调用 StorageId.getExternalId () 方法,以获取嵌入在 id 参数中的用户名。然后,方法委派为 getUserByUsername ()

不会存储电子邮件,因此 getUserByEmail () 方法返回 null。

7.5.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());
    }
Copy to Clipboard Toggle word wrap

isConfiguredFor () 方法由运行时调用,以确定是否为用户配置了特定的凭证类型。此方法检查是否为用户设置密码。

supportsCredentialType () 方法返回特定凭证类型是否支持验证。我们检查凭据类型是 密码

isValid () 方法负责验证密码。CredentialInput 参数只是所有凭证类型的抽象接口。我们确保支持凭证类型,并且它是 UserCredentialModel 的实例。当用户通过登录页面登录时,密码输入的纯文本会被放入 UserCredentialModel 实例中。isValid () 方法针对存储在属性文件中的纯文本密码检查这个值。返回值为 true 表示密码有效。

7.5.1.3. CredentialInputUpdater 实现

如前所述,我们在本示例中实施 CredentialInputUpdater 接口的唯一原因是禁止修改用户密码。我们必须执行此操作的原因是,运行时允许在红帽构建的 Keycloak 本地存储中覆盖密码。在本章后文中我们将进一步讨论。

    @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 Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
        return Stream.empty();
    }
Copy to Clipboard Toggle word wrap

updateCredential () 方法只检查凭证类型是否为 password。如果是,则抛出 ReadOnlyException

7.5.2. 供应商工厂实现

现在,供应商类已经完成,我们现在转意供应商工厂类。

public class PropertyFileUserStorageProviderFactory
                 implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {

    public static final String PROVIDER_NAME = "readonly-property-file";

    @Override
    public String getId() {
        return PROVIDER_NAME;
    }
Copy to Clipboard Toggle word wrap

首先要注意的是,在实施 UserStorageProviderFactory 类时,您必须将 concrete 供应商类实现作为模板参数传递。在这里,我们指定我们之前定义的供应商类: PropertyFileUserStorageProvider

警告

如果没有指定 template 参数,您的供应商将无法正常工作。运行时进行类内省,以确定提供程序实施 的功能接口

getId () 方法标识运行时中的工厂,当您要为域启用用户存储供应商时,也会是 admin 控制台中显示的字符串。

7.5.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);
    }
Copy to Clipboard Toggle word wrap

UserStorageProviderFactory 接口有可选的 init () 方法,您可以实现。当红帽构建的 Keycloak 启动时,每个供应商工厂只创建一个实例。另外,还会在这些工厂实例上调用 init () 方法。还有一个 postInit () 方法,您也可以实施。调用每个工厂的 init () 方法后,会调用它们的 postInit () 方法。

init () 方法实现中,我们从 classpath 找到包含用户声明的属性文件。然后,我们将使用用户名和密码组合来加载 properties 字段。

Config.Scope 参数是通过服务器配置的工厂配置。

例如,使用以下参数运行服务器:

kc.[sh|bat] start --spi-storage-readonly-property-file-path=/other-users.properties
Copy to Clipboard Toggle word wrap

我们可以指定用户属性文件的 classpath,而不是硬编码它。然后,您可以在 PropertyFileUserStorageProviderFactory.init () 中检索配置:

public void init(Config.Scope config) {
    String path = config.get("path");
    InputStream is = getClass().getClassLoader().getResourceAsStream(path);

    ...
}
Copy to Clipboard Toggle word wrap
7.5.2.2. 创建方法

我们创建供应商工厂的最后一步是 create () 方法。

    @Override
    public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new PropertyFileUserStorageProvider(session, model, properties);
    }
Copy to Clipboard Toggle word wrap

我们只是分配 PropertyFileUserStorageProvider 类。这个创建方法将为每个事务调用一次。

7.5.3. 打包和部署

我们提供程序实施的类文件应放在 jar 中。您还必须在 META-INF/services/org.keycloak.storage.UserStorageProviderFactory 文件中声明供应商工厂类。

org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
Copy to Clipboard Toggle word wrap

要部署此 jar,请将它复制到 providers/ 目录,然后运行 bin/kc.[sh|bat] build

7.5.4. 在管理门户中启用提供程序

您可以在 Admin Console 的 User Federation 页面中为每个域启用用户存储供应商。

用户联邦

empty user federation page

流程

  1. 从列表中选择刚才创建的提供程序: readonly-property-file

    此时会显示我们的提供程序的配置页面。

  2. 单击 Save,因为我们没有配置任何内容。

    配置供应商

    storage provider created

  3. 返回到主 User Federation 页面

    现在,您会看到您的供应商被列出。

    用户联邦

    user federation page

现在,您将能够使用 users.properties 文件中声明的用户登录。此用户只能在登录后查看帐户页面。

7.6. 配置技术

我们的 PropertyFileUserStorageProvider 示例是一个位拥塞。它被硬编码到提供程序的 jar 中嵌入的属性文件,这并不有用。我们可能希望使此文件的位置可由提供程序的每个实例配置。换句话说,我们可能希望在多个不同的域中多次重复使用此提供程序,并指向完全不同的用户属性文件。我们还将在管理控制台 UI 中执行此配置。

UserStorageProviderFactory 有额外的方法,您可以实现处理提供程序配置的方法。您可以描述您要为每个提供程序配置的变量,Admin Console 会自动呈现一个通用输入页面来收集此配置。实施后,回调方法也会在保存前、首次创建供应商以及更新时验证配置。UserStorageProviderFactoryorg.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) {

    }
Copy to Clipboard Toggle word wrap

ComponentFactory.getConfigProperties () 方法返回 org.keycloak.provider.ProviderConfigProperty 实例列表。这些实例声明呈现和存储提供程序的每个配置变量所需的元数据。

7.6.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;
    }
Copy to Clipboard Toggle word wrap

ProviderConfigurationBuilder 类是一个很好的帮助程序类,用于创建配置属性列表。在这里,我们指定一个名为 path 的变量,它是 String 类型。在此提供程序的 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");
        }
    }
Copy to Clipboard Toggle word wrap

validateConfiguration () 方法提供 ComponentModel 中的配置变量,以验证磁盘上是否存在该文件。请注意,使用 org.keycloak.common.util.EnvUtil.replace () 方法。通过此方法,包含 ${} 的任何字符串会将该值替换为系统属性值。${jboss.server.config.dir} 字符串对应于服务器的 conf/ 目录,对于本例中非常有用。

接下来,我们需要做的是删除旧的 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);
    }
Copy to Clipboard Toggle word wrap

当然,这种逻辑是效率低下,因为每个事务从磁盘读取整个用户属性文件,但希望以简单的方式演示,如何对配置变量中的 hook 进行 hook。

7.6.2. 在管理门户中配置提供程序

现在,在启用了配置时,您可以在 Admin 控制台中配置供应商时设置 path 变量。

7.7. 添加/删除用户和查询功能接口

我们使用我们的示例的一个操作是允许它添加和删除用户或更改密码。在我们示例中定义的用户也可在管理门户中查询或查看。要添加这些改进,我们的示例供应商必须实现 UserQueryMethodsProvider (或 UserQueryProvider)和 UserRegistrationProvider 接口。

7.7.1. 实现 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);
        }
    }
Copy to Clipboard Toggle word wrap

然后,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;
        }
    }
Copy to Clipboard Toggle word wrap

请注意,在添加用户时,我们将属性映射的 password 值设置为 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());
    }
Copy to Clipboard Toggle word wrap

由于我们现在可以保存属性文件,因此允许密码更新也很有意义。

PropertyFileUserStorageProvider

    @Override
    public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
        if (!(input instanceof UserCredentialModel)) return false;
        if (!input.getType().equals(PasswordCredentialModel.TYPE)) return false;
        UserCredentialModel cred = (UserCredentialModel)input;
        synchronized (properties) {
            properties.setProperty(user.getUsername(), cred.getValue());
            save();
        }
        return true;
    }
Copy to Clipboard Toggle word wrap

现在,我们可以实施禁用密码。

PropertyFileUserStorageProvider

    @Override
    public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
        if (!credentialType.equals(PasswordCredentialModel.TYPE)) return;
        synchronized (properties) {
            properties.setProperty(user.getUsername(), UNSET_PASSWORD);
            save();
        }

    }

    private static final Set<String> disableableTypes = new HashSet<>();

    static {
        disableableTypes.add(PasswordCredentialModel.TYPE);
    }

    @Override
    public Stream<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {

        return disableableTypes.stream();
    }
Copy to Clipboard Toggle word wrap

通过实现这些方法,您现在可以在管理控制台中更改和禁用用户的密码。

7.7.2. 实现 UserQueryProvider

UserQueryProviderUserQueryMethodsProviderUserCountMethodsProvider 的组合。如果没有实施 UserQueryMethodsProvider,Admin 控制台将无法查看和管理由我们的示例供应商载入的用户。让我们来看看实施此接口。

PropertyFileUserStorageProvider

    @Override
    public int getUsersCount(RealmModel realm) {
        return properties.size();
    }

    @Override
    public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
        Predicate<String> predicate = "*".equals(search) ? username -> true : username -> username.contains(search);
        return properties.keySet().stream()
                .map(String.class::cast)
                .filter(predicate)
                .skip(firstResult)
                .map(username -> getUserByUsername(realm, username))
                .limit(maxResults);
    }
Copy to Clipboard Toggle word wrap

searchForUserStream () 的第一个声明采用 String 参数。在本例中,参数表示您要搜索的用户名。这个字符串可以是子字符串,它解释了在执行搜索时的 String.contains () 方法的选择。请注意,使用 * 表示请求所有用户的列表。该方法迭代属性文件的密钥集,委派至 getUserByUsername () 以加载用户。请注意,我们会根据 firstResultmaxResults 参数对这个调用进行索引。如果您的外部存储不支持分页,则必须执行类似的逻辑。

PropertyFileUserStorageProvider

    @Override
    public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {
        // only support searching by username
        String usernameSearchString = params.get("username");
        if (usernameSearchString != null)
            return searchForUserStream(realm, usernameSearchString, firstResult, maxResults);

        // if we are not searching by username, return all users
        return searchForUserStream(realm, "*", firstResult, maxResults);
    }
Copy to Clipboard Toggle word wrap

采用 Map 参数的 searchForUserStream () 方法可以根据名字、姓氏、用户名和电子邮件搜索用户。仅存储用户名,因此搜索仅基于用户名,当 Map 参数不包含 username 属性时除外。在这种情况下,所有用户都会被返回。在这种情况下,使用 searchForUserStream (realm, search, firstResult, maxResults)

PropertyFileUserStorageProvider

    @Override
    public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
        return Stream.empty();
    }

    @Override
    public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
        return Stream.empty();
    }
Copy to Clipboard Toggle word wrap

组或属性不会存储,因此其他方法会返回一个空流。

7.8. 增加外部存储

PropertyFileUserStorageProvider 示例真正有限。虽然我们将能够使用存储在属性文件中的用户登录,但我们将无法执行其他操作。如果此提供程序加载的用户需要特殊的角色或组映射来完全访问特定应用程序,则无法向这些用户添加额外的角色映射。您还可以修改或添加其他重要属性,如电子邮件、名字和姓氏。

对于这些类型的情况,红帽构建的 Keycloak 允许您在红帽构建的 Keycloak 数据库中存储额外信息来增加外部存储。这称为联邦用户存储,并封装在 org.keycloak.storage.federated.UserFederatedStorageProvider 类中。

UserFederatedStorageProvider

package org.keycloak.storage.federated;

public interface UserFederatedStorageProvider extends Provider,
        UserAttributeFederatedStorage,
        UserBrokerLinkFederatedStorage,
        UserConsentFederatedStorage,
        UserNotBeforeFederatedStorage,
        UserGroupMembershipFederatedStorage,
        UserRequiredActionsFederatedStorage,
        UserRoleMappingsFederatedStorage,
        UserFederatedUserCredentialStore {
    ...

}
Copy to Clipboard Toggle word wrap

UserFederatedStorageProvider 实例在 UserStorageUtil.userFederatedStorage (KeycloakSession) 方法上可用。它具有存储属性、组和角色映射、不同凭证类型和所需操作的所有不同类型的方法。如果您的外部存储的数据型号不支持红帽构建的 Keycloak 功能集,则该服务可能会填补差距。

红帽 Keycloak 的构建附带一个帮助程序类 org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage,它将把每个单用户 Model 方法委托给用户联邦存储。覆盖您需要覆盖的方法,以委派给您的外部存储表示法。强烈建议您阅读此类的 javadoc,因为它有较小的保护方法,您可能需要覆盖。专门围绕组成员资格和角色映射。

7.8.1. 8 月示例

在我们的 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();
                }
            }
        };
    }
Copy to Clipboard Toggle word wrap

我们改为定义 AbstractUserAdapterFederatedStorage 的匿名类实现。setUsername () 方法更改属性文件并保存它。

7.9. 导入实施策略

在实施用户存储提供程序时,您可以采用另外一种策略。您可以在 Red Hat build of Keycloak 内置用户数据库中本地创建用户,并将外部存储中的属性复制到这个本地副本中,而不是使用用户联邦存储。这种方法有很多优点。

  • 红帽构建的 Keycloak 基本上成为外部存储的持久性用户缓存。导入用户后,您将不会再达到外部存储,从而退出它。
  • 如果您要作为官方用户存储并弃用旧的外部存储红帽构建的 Keycloak,您可以慢慢地迁移应用程序以使用红帽构建的 Keycloak。当所有应用都已迁移后,取消链接导入的用户,然后停用旧的外部存储。

使用导入策略时有一些明显的缺点:

  • 第一次查找用户需要多次更新红帽构建的 Keycloak 数据库。这可能会给负载造成大量性能损失,并在红帽构建的 Keycloak 数据库中造成大量压力。用户联合存储方法将仅存储额外的数据,并且可能永远不会根据外部存储的功能使用。
  • 通过导入方法,您必须保留本地红帽构建的 Keycloak 存储和外部存储同步。用户存储 SPI 具有您可以实现支持同步的功能接口,但可能会快速变得困难且出现问题。

若要实施导入策略,只需先检查以查看用户是否已在本地导入。如果返回本地用户,如果没有在本地创建用户,并从外部存储导入数据。您还可以代理本地用户,以便自动同步大多数更改。

这将是稍微的,但我们可以扩展 PropertyFileUserStorageProvider 来采用这种方法。首先修改 createAdapter () 方法。

PropertyFileUserStorageProvider

    protected UserModel createAdapter(RealmModel realm, String username) {
        UserModel local = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, username);
        if (local == null) {
            local = UserStoragePrivateUtil.userLocalStorage(session).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);
            }
        };
    }
Copy to Clipboard Toggle word wrap

在这个方法中,我们调用 UserStoragePrivateUtil.userLocalStorage (session) 方法来获取对本地红帽构建的 Keycloak 用户存储的引用。我们可以看到用户是否存储在本地,如果没有,我们将在本地添加。不要设置本地用户的 id。让红帽构建 Keycloak 会自动生成 id。另请注意,我们调用 UserModel.setFederationLink (),并传递我们供应商的 ComponentModel 的 ID。这会设置供应商和导入的用户之间的链接。

注意

删除用户存储提供程序时,也会删除它导入的任何用户。这是调用 UserModel.setFederationLink () 的一个目的。

需要注意的是,如果本地用户链接了某一本地用户,您的存储提供程序仍将被委派为从 CredentialInputValidatorCredentialInputUpdater 接口实现的方法。从验证或更新返回 false 时,只有红帽构建 Keycloak 以查看它是否可以使用本地存储验证或更新。

另请注意,我们使用 org.keycloak.models.utils.UserModelDelegate 类代理本地用户。此类是 UserModel 的实施。每个方法仅委托给 用户模型,用它实例化。我们覆盖此委派类的 setUsername () 方法,以与属性文件自动同步。对于供应商,您可以使用它 截获 本地 UserModel 上的其他方法,以执行与外部存储同步。例如,get 方法可以确保本地存储处于同步状态。设置方法使外部存储与本地存储保持同步。需要注意的是,getId () 方法应始终返回您在本地创建用户时自动生成的 id。您不应该返回联邦 id,如其它非导入示例所示。

注意

如果您的供应商实现 UserRegistrationProvider 接口,则您的 removeUser () 方法不需要从本地存储中删除该用户。运行时会自动执行此操作。另请注意,在从本地存储中删除 removeUser () 之前,会先调用 removeUser ()。

7.9.1. ImportedUserValidation 接口

如果您记得在本章的前面部分,我们讨论如何查询用户工作。如果找到了用户,则会首先查询本地存储,然后查询结束。这是以上实施的一个问题,因为我们希望代理本地 UserModel,以便我们可以使用户名保持同步。用户存储 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);
}
Copy to Clipboard Toggle word wrap

每当加载链接的本地用户时,如果用户存储类实施此接口,则调用 validate () 方法。您可以在此处代理作为参数传递的本地用户并返回它。这将使用新的 UserModel。您还可以选择检查来查看用户是否存在于外部存储中。如果 validate () 返回 null,则本地用户将从数据库中删除。

7.9.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);
}
Copy to Clipboard Toggle word wrap

这个接口由供应商工厂实现。当此接口由供应商工厂实现后,供应商的管理控制台管理页面会显示其他选项。您可以点击按钮来手动强制同步。这会调用 ImportSynchronization.sync () 方法。另外,会显示额外的配置选项,供您自动调度同步。自动同步调用 syncSince () 方法。

7.10. 用户缓存

当用户对象通过 ID、用户名或电子邮件查询加载时,会缓存它。当缓存用户对象时,它会迭代整个 UserModel 接口,并将这些信息拉取到本地的内存中缓存。在集群中,此缓存仍然是本地的,但会变得失效缓存。修改用户对象时,它将被驱除。此驱除事件被传播到整个集群,以便其他节点的用户缓存也无效。

7.10.1. 管理用户缓存

您可以通过调用 KeycloakSession.getProvider (UserCache.class) 来访问用户缓存。

/**
 * 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();
}
Copy to Clipboard Toggle word wrap

有可驱除特定用户、特定域中包含的用户或整个缓存的方法。

7.10.2. OnUserCache 回调接口

您可能需要缓存特定于您的供应商实现的额外信息。每当用户缓存时,User Storage SPI 都有一个回调: org.keycloak.models.cache.OnUserCache

public interface OnUserCache {
    void onCache(RealmModel realm, CachedUserModel user, UserModel delegate);
}
Copy to Clipboard Toggle word wrap

如果您希望此回调,您的供应商类应该实现这个接口。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();
}
Copy to Clipboard Toggle word wrap

CachedUserModel 接口允许您从缓存中驱除用户并获取供应商 UserModel 实例。getCachedWith () 方法返回一个映射,允许您缓存与用户相关的其他信息。例如,凭证不是 UserModel 接口的一部分。如果要在内存中缓存凭证,则需要实施 OnUserCache,并使用 getCachedWith () 方法缓存用户的凭据。

7.10.3. 缓存策略

在用户存储供应商的管理控制台管理页面中,您可以指定一个唯一的缓存策略。

7.11. 利用 Jakarta EE

自版本 20 起,Keycloak 仅依赖于 Quarkus。与 WildFly 不同,Quarkus 不是应用服务器。

因此,用户存储提供程序无法打包在任何 Jakarta EE 组件中,或者使其成为 EJB,当 Keycloak 在以前的版本中通过 WildFly 运行时。

提供商实施必须是实现合适的用户存储 SPI 接口的普通 java 对象,如上一节中所述。它们必须按照迁移指南中所述进行打包和部署。请参阅 迁移自定义提供程序

您仍然可以实施您的自定义 UserStorageProvider 类,它能够由 JPA Entity Manager 集成外部数据库,如下例所示:

不支持 CDI。

7.12. REST 管理 API

您可以通过管理员 REST API 创建、删除和更新用户存储供应商部署。用户存储 SPI 基于通用组件接口构建,因此您将使用该通用 API 来管理您的提供程序。

REST 组件 API 位于您的 realm admin 资源下。

/admin/realms/{realm-name}/components
Copy to Clipboard Toggle word wrap

我们仅显示此 REST API 与 Java 客户端的交互。希望您可以从此 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();
}
Copy to Clipboard Toggle word wrap

要创建用户存储供应商,您必须指定 provider id、字符串 org.keycloak.storage.UserStorageProvider 的供应商类型,以及配置。

import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RealmRepresentation;
...

Keycloak keycloak = Keycloak.getInstance(
    "http://localhost:8080",
    "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();
Copy to Clipboard Toggle word wrap

7.13. 从早期的用户联邦 SPI 迁移

注意

只有在您使用之前(及现已删除)用户联邦 SPI 实施了供应商时,本章才适用。

在 Keycloak 版本 2.4.0 及更早版本中,有一个 User Federation SPI。Red Hat Single Sign-On 版本 7.0 也支持,但早期的 SPI 也可用。这个以前的 User Federation SPI 已从 Keycloak 版本 2.5.0 和 Red Hat Single Sign-On 版本 7.1 中删除。但是,如果您使用这个早期的 SPI 编写供应商,本章讨论了您可以用来端口它的一些策略。

7.13.1. 导入与非导入

较早的用户联邦 SPI 要求您在 Red Hat build of Keycloak 的数据库中创建用户的本地副本,并将信息从外部存储导入到本地副本。但是,这不再是必需的。您仍然可以将较早的供应商移植为原样,但您应该考虑非导入策略是否为更好的方法。

导入策略的优点:

  • 红帽构建的 Keycloak 基本上成为外部存储的持久性用户缓存。导入用户后,您将不会再达到外部存储,从而关闭它。
  • 如果您要作为官方用户存储并弃用较早的外部存储红帽构建的 Keycloak,您可以慢慢地迁移应用程序以使用红帽构建的 Keycloak。所有应用迁移后,取消链接导入的用户,然后停用早期的传统外部存储。

使用导入策略时有一些明显的缺点:

  • 第一次查找用户需要多次更新红帽构建的 Keycloak 数据库。这可能会给负载造成大量性能损失,并在红帽构建的 Keycloak 数据库中造成大量压力。用户联合存储方法将仅存储额外的数据,且可能永远不会根据外部存储的功能使用。
  • 通过导入方法,您必须保留本地红帽构建的 Keycloak 存储和外部存储同步。用户存储 SPI 具有您可以实现支持同步的功能接口,但可能会快速变得困难且出现问题。

7.13.2. UserFederationProvider versus UserStorageProvider

首先要注意的是,UserFederationProvider 是一个完整的接口。您在此界面中实施每个方法。但是,UserStorageProvider 会根据需要把这个接口分为多个您实现的功能接口。

UserFederationProvider.getUserByUsername ()getUserByEmail () 在新的 SPI 中具有完全对应的项。两者之间的差别在于您导入的方式。如果您要继续导入策略,您不再调用 KeycloakSession.userStorage ().addUser () 来在本地创建用户。相反,您调用 KeycloakSession.userLocalStorage ().addUser ()userStorage () 方法不再存在。

UserFederationProvider.validateAndProxy () 方法已移至可选的功能接口 ImportedUserValidation。如果您要将早期供应商按原样移植,则需要实施此接口。另请注意,在之前的 SPI 中,每次访问用户时都会调用此方法,即使本地用户位于缓存中。在后续的 SPI 中,只有从本地存储加载本地用户时,才会调用此方法。如果缓存本地用户,则不会调用 ImportedUserValidation.validate () 方法。

后续 SPI 不再存在 UserFederationProvider.isValid () 方法。

UserFederationProvider 方法 synchronizeRegistrations (), registerUser (), 和 removeUser () 已移到 UserRegistrationProvider 功能接口。这个新接口是可选的,因此如果您的供应商不支持创建和删除用户,您不必实施它。如果您的之前的供应商已切换支持来注册新用户,新的 SPI 支持,如果供应商不支持添加用户,则从 UserRegistrationProvider.addUser () 返回 null

以前基于凭证的 UserFederationProvider 方法现在封装在 CredentialInputValidatorCredentialInputUpdater 接口中,它们也是可选的,具体取决于您支持验证或更新凭证。用于存在于 UserModel 方法的凭证管理。它们也已移至 CredentialInputValidatorCredentialInputUpdater 接口。请注意,如果您没有实现 CredentialInputUpdater 接口,则您的供应商提供的任何凭证都可以在红帽构建的 Keycloak 存储本地覆盖。因此,如果您希望凭证为只读,请实施 CredentialInputUpdater.updateCredential () 方法并返回 ReadOnlyException

UserFederationProvider 查询方法,如 searchByAttributes ()getGroupMembers () 现在被封装在可选的 interface UserQueryProvider 中。如果没有实现这个接口,则无法在管理控制台中查看用户。但是,您仍可以登录。

早期 SPI 中的同步方法现在封装在可选的 ImportSynchronization 接口中。如果您实施了同步逻辑,则新的 UserStorageProviderFactory 实现了 ImportSynchronization 接口。

7.13.4. 升级至新模型

用户存储 SPI 实例存储在不同的关系表中。红帽构建的 Keycloak 会自动运行迁移脚本。如果为域部署任何较早的用户联邦供应商,它们将转换为后续存储模型,包括数据的 id。只有具有与早期用户联邦提供商相同的供应商 ID (即"ldap"、"kerberos")存在用户存储提供程序时,才会发生此迁移。

因此,您可以采取不同的方法。

  1. 您可以删除之前的 Red Hat build of Keycloak 部署中的供应商。这将删除您导入的所有用户的本地链接副本。然后,当您升级红帽构建的 Keycloak 时,只为您的域部署和配置新供应商。
  2. 第二个选项是编写新提供程序,确保它具有相同的供应商 ID: UserStorageProviderFactory.getId ()。确保此提供程序已部署到服务器。引导服务器,并让内置迁移脚本从较早的数据模型转换为后续的数据模型。在这种情况下,您之前所有链接的用户都可以正常工作,且相同。

如果您决定获得导入策略并重写您的用户存储供应商,我们建议您在升级 Red Hat build of Keycloak 前删除之前的供应商。这将删除您导入的任何用户的链接本地导入副本。

7.14. 基于流的接口

红帽构建的 Keycloak 中的许多用户存储接口包含可以返回潜在大量对象的查询方法,这可能会在内存消耗和处理时间方面造成显著影响。当查询方法的逻辑中只使用对象的内部状态时,这尤其如此。

为了为开发人员提供了更有效的、处理这些查询方法中大型数据集的替代选择,已将 Streams 子接口添加到用户存储接口中。这些 Streams 子接口将超级接口中的基于原始集合的方法替换为基于流的变体,使基于集合的方法被默认。基于集合的查询方法的默认实现调用其 对应部分,并将结果收集到正确的集合类型。

Streams 子接口允许实现重点放在基于流的方法处理数据集,并从该方法的潜在内存和性能优化中受益。提供要实施的 Streams 子接口的接口包括几个 功能接口org.keycloak.storage.federated 软件包中所有接口,以及可能根据自定义存储实施的范围来实施的其他接口。

请参阅此为开发人员提供 Streams 子接口的接口列表。

Expand

软件包

org.keycloak.credential

CredentialInputUpdater(*)

org.keycloak.models

GroupModel,RoleMapperModel,UserModel

org.keycloak.storage.federated

所有接口

org.keycloak.storage.user

UserQueryProvider(*)

AssumeRole 表示接口是一个 功能接口

要从流方法中受益的自定义用户存储实现应该只实现 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
    }
...
}
Copy to Clipboard Toggle word wrap

第 8 章 Vault SPI

8.1. Vault 供应商

您可以使用 org.keycloak.vault 软件包中的 vault SPI 为红帽构建的 Keycloak 编写自定义扩展,以连接到任意密码库实施。

内置 files-plaintext 提供程序是此 SPI 的实施示例。通常应用以下规则:

  • 要防止 secret 在域间泄漏,您可能需要隔离或限制域可以检索的 secret。在这种情况下,您的供应商在查找 secret 时应考虑 realm 名称,例如,使用 realm 名称作为前缀。例如,一个表达式 ${vault.key} 将通常会评估不同的条目名称,具体取决于它在 realm A 或 realm B 中使用。要区分不同的域,需要将域从 VaultProvider Factory.create () 方法传递至创建的 VaultProvider 实例,该方法可以从 KeycloakSession 参数获得。
  • vault 供应商需要实施单一方法 get Secret,它为给定 secret 名称返回 VaultRawSecret。该类包含 secret 的表示,可以是 byte[]ByteBuffer,并且应该在需要时在两者之间进行转换。请注意,在用法后,此缓冲区将丢弃,如下所述。

有关如何打包和部署自定义提供程序的详情,请参考 服务提供商接口 章节。

8.2. 从 vault 中消耗值

库包含敏感数据,红帽构建的 Keycloak 会相应地对待 secret。在访问机密时,该机密将从密码库获取,并且仅在必要时间保留在 JVM 内存中。然后,所有可能都会尝试从 JVM 内存丢弃其内容。这可以通过仅在 try-with-resources 语句中使用 vault secret 来实现,如下所示:

    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
Copy to Clipboard Toggle word wrap

这个示例使用 KeycloakSession.vault () 作为访问 secret 的入口点。直接使用 VaultProvider.obtainSecret 方法也可以实现。但是,vault () 方法具有将原始 secret (通常是字节阵列)作为字符数组的功能(通过 vault ().getCharSecret ())或 String (通过 vault ().getStringSecret ())来获取原始的未解释值(通过 vault ().getRawSecret () 方法)。

请注意,由于 String 对象不可变,因此无法通过使用随机垃圾覆盖来丢弃其内容。虽然在默认的 VaultStringSecret 实现中已采取措施以防止内部化 String,但 String 对象中存储的 secret 至少会到下一个 GC 舍入。因此,首选使用普通字节和字符数组和缓冲区。

法律通告

Copyright © 2025 Red Hat, Inc.
根据 Apache 许可证(版本 2.0)授权(License");除非遵守许可证,您可能不能使用此文件。您可以在以下位置获取许可证副本
除非适用法律或同意编写,许可证下的软件将由"AS IS"BASIS 分发,WITHOUT WARRANTIES 或 CONDITIONS OF ANY KIND,可以是表达或表示的。有关许可证下的权限和限制的具体语言,请参阅许可证。
返回顶部
Red Hat logoGithubredditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

通过我们的产品和服务,以及可以信赖的内容,帮助红帽用户创新并实现他们的目标。 了解我们当前的更新.

让开源更具包容性

红帽致力于替换我们的代码、文档和 Web 属性中存在问题的语言。欲了解更多详情,请参阅红帽博客.

關於紅帽

我们提供强化的解决方案,使企业能够更轻松地跨平台和环境(从核心数据中心到网络边缘)工作。

Theme

© 2025 Red Hat