前言

在某些示例列表中,要在一行上显示的内容不适合可用页面宽度。 这些线已经被打破了。 行尾的 “\” 表示已引入中断以适合页面,并缩进了以下行。 所以:

让我们假装有一个非常 \
长长的队伍 \
不适合
这个短

是真的:

让我们假装有一个非常长的队伍,不适合
这个短

管理REST API

Keycloak带有功能齐全的Admin REST API,具有管理控制台提供的所有功能。

要调用API,您需要获得具有相应权限的访问令牌。 所需的权限描述在 服务器管理指南

可以通过使用Keycloak对您的应用程序进行身份验证来获得令牌; 请参阅 保护应用程序和服务指南。 您也可以使用直接访问授权来获取访问令牌。

有关完整的文档,请参见API文档

使用卷曲的示例

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

为领域中的用户获取访问令牌大师使用用户名管理员和密码密码:

卷曲 \
  -d "客户端 _ id = 管理-cli" \
  -d "用户名 = 管理员" \
  -d "密码 = 密码" \
  -d "grant_type = 密码" \
  “http:// localhost:8080/auth/realms/master/protocol/openid-connect/token”
默认情况下,此令牌将在1分钟后过期

结果将是一个JSON文档。 要调用API,您需要提取访问令牌财产。 然后,您可以通过包括 中的值授权对API的请求头。

下面的示例演示如何获取主领域的详细信息:

卷曲 \
  -H “授权: bearer eyJhbGciOiJSUz...” \
  "http:// localhost:8080/auth/admin/realms/master"

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

在能够使用客户id和一个客户 _ 秘密您需要确保客户端的配置如下:

  • 客户id是一个机密属于领域的客户端大师

  • 客户id已启用服务帐户选项已启用

  • 客户id有一个自定义的 “观众” 映射器

    • 包括客户受众:安全-管理-控制台

最后,检查一下客户id在 “服务帐户角色” 选项卡中分配了角色 “管理员”。

之后,您将能够使用以下方式获得Admin REST API的访问令牌客户id客户 _ 秘密:

卷曲 \
  -d "客户id = <您的客户id>" \
  -d "客户秘密 = <你的客户秘密>" \
  -d "授权类型 = 客户凭证" \
  “http:// localhost:8080/auth/realms/master/protocol/openid-connect/token”

使用Java的示例

有一个用于Admin REST API的Java客户端库,它使Java易于使用。 要从您的应用程序中使用它,请添加对 Keycloak-管理员-客户端图书馆。

下面的示例演示如何使用Java客户端库来获取主领域的详细信息:

导入 组织。Keycloak。管理。客户端。Keycloak;
导入 组织。Keycloak。代表。idm。真实代表;
...

Keycloak keycloak = Keycloak.getInstance (
    http:// localhost:8080/auth,
    大师,
    管理员,
    密码,
    管理-cli);
真实表示领域 = Keycloak。领域 (大师)。提交 ();

管理员客户端的完整Javadoc可在API文档

主题

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

登录日出
带有日出示例主题的登录页面

主题类型

主题可以提供一种或多种类型,以自定义Keycloak的不同方面。 可用的类型有:

  • 帐户-帐户管理

  • 管理-管理控制台

  • 电子邮件-电子邮件

  • 登录-登录表单

  • 欢迎-欢迎页面

配置主题

除了欢迎,所有主题类型都是通过管理控制台。 要更改用于领域的主题,请打开管理控制台,选择 你的境界从左上角的下拉框。 下领域设置点击主题

大师管理控制台您需要为大师境界。 查看对管理控制台的更改 刷新页面。

要更改欢迎主题,您需要编辑独立。xml,standalone-ha.xml,或者域。xml

添加欢迎主题到主题元素,例如:

<主题>
    ...
    <欢迎主题>自定义主题</欢迎主题>
    ...
</主题>

如果服务器正在运行,则需要重新启动服务器,以使对欢迎主题的更改生效。

默认主题

Keycloak与服务器根目录中的默认主题捆绑在一起主题目录。 为了简化升级,您不应该编辑捆绑的主题 直接。 而是创建自己的主题,以扩展捆绑的主题之一。

创建主题

主题包括:

除非你计划替换每一页,否则你应该扩展另一个主题。 您很可能希望扩展Keycloak主题,但也可以 如果您正在显著改变页面的外观和感觉,请考虑扩展基本主题。 基本主题主要由HTML模板和 消息包,而Keycloak主题主要包含图像和样式表。

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

创建主题时,最好禁用缓存,因为这样可以直接从主题目录没有 重新启动Keycloak。 要进行此编辑独立。xml。 对于主题设置静态最大值-1和两者 缓存模板缓存主题:

<主题>
    <静态最大值>-1</静态最大值>
    <缓存主题></缓存主题>
    <缓存模板></缓存模板>
    ...
</主题>

请记住在生产中重新启用缓存,因为它将显着影响性能。

要创建新主题,请先在主题目录。 目录的名称成为主题的名称。 例如到 创建一个名为我的主题创建目录主题/我的主题

在主题目录中为主题要提供的每种类型创建一个目录。 例如,将登录类型添加到我的主题 主题创建目录主题/我的主题/登录

为每种类型创建一个文件主题。属性这允许为主题设置一些配置。 例如配置主题主题/我的主题/登录 我们刚刚创建的扩展基本主题和导入一些公共资源创建文件主题/我的主题/登录/主题。属性具有以下内容:

父 = 基
导入 = 通用/Keycloak

您现在已经创建了一个支持登录类型的主题。 要检查它是否工作正常,请打开管理控制台。 选择你的境界,然后点击主题。 对于登录主题选择我的主题并点击保存。 然后打开境界的登录页面。

您可以通过您的应用程序登录或打开帐户管理控制台 (/领域/{领域名称}/账户)。

要查看更改父主题的效果,请设置父 = Keycloak主题。属性并刷新登录页面。

主题属性

主题属性在文件中设置<主题类型>/主题。属性在主题目录中。

  • 要扩展的父-父主题

  • 导入-从另一个主题导入资源

  • 样式-要包含的空格分隔样式列表

  • 区域设置-逗号-支持的区域设置的分隔列表

有一个属性列表,可用于更改用于某些元素类型的css类。 有关这些属性的列表,请查看主题。属性 keycloak主题对应类型的文件 (主题/Keycloak/<主题类型>/主题。属性)。

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

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

  • $ {一些。系统。属性}-对于系统属性

  • $ {环境。环境 _ var}-用于环境变量。

如果找不到系统属性或环境变量,也可以提供默认值${foo:defaultValue}

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

下面是一个可能的例子:

javaVersion =${ java.version}

unixHome =${ env.HOME: 未找到Unix home}
windowsHome =${ env.HOMEPATH:Windows home not found}

样式表

一个主题可以有一个或多个样式表。 要添加样式表,请在<主题类型>/资源/css您的主题目录。 然后将其添加到风格 中的财产主题。属性

例如添加样式。css我的主题创建主题/我的主题/登录/资源/css/样式具有以下内容:

。登录-pf 身体{
    背景:迪姆格雷 ;
}

然后编辑主题/我的主题/登录/主题。属性并添加:

样式 = css/样式

要查看更改,请打开您领域的登录页面。 您会注意到,唯一应用的样式是来自自定义样式表的样式。 包括 来自父主题的样式您也需要从该主题加载样式。 通过编辑来做到这一点主题/我的主题/登录/主题。属性和变化风格 致:

styles = web_modules/@ fortawesome/fontawesome-free/css/icons/all.css web_modules/@ patternfly/react-core/dist/styles/base.css
网络模块/@ 模式飞行/反应核心/dist/样式/应用程序。css节点 _ 模块/模式飞行/dist/css/模式飞行
节点 _ 模块/图案/dist/css/patternfly-additions.min.css css/login.css css/样式
要覆盖父样式表中的样式,请务必在最后列出样式表。

脚本

一个主题可以有一个或多个脚本,要添加一个脚本,请在<主题类型>/资源/js您的主题目录。 然后将其添加到脚本 中的财产主题。属性

例如添加脚本.js我的主题创建主题/mytheme/登录/资源/js/script.js具有以下内容:

警报 ('你好');

然后编辑主题/我的主题/登录/主题。属性并添加:

scripts = js/script.js

图像

要使图像可用于主题,请将其添加到<主题类型>/资源/img您的主题目录。 这些可以在样式表中使用,或者 直接在HTML模板中。

例如,将图像添加到我的主题将图像复制到主题/我的主题/登录/资源/img/image.jpg

然后,您可以在自定义样式表中使用此图像:

身体{
    背景图像:url ('../img/image.jpg');
    背景尺寸:;
}

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

<img src=${url.resourcesPath}/img/image.jpg>

消息

模板中的文本是从消息包加载的。 扩展另一个主题的主题将继承父级消息包中的所有消息,您可以 通过添加覆盖单个消息<主题类型>/消息 _ en.属性你的主题。

例如更换用户名在登录表单上您的用户名对于我的主题创建文件 主题/我的主题/登录/消息具有以下内容:

用户名或电子邮件 = 您的用户名

在消息值中,如{0}{1}使用消息时,将其替换为参数。 例如 {0} in登录到 {0}被替换为名称 这个领域。

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

国际化

Keycloak支持国际化。 为了实现一个领域的国际化,请参见服务器管理指南。 这个 部分描述了如何添加自己的语言。

要添加新语言,请创建文件<主题类型>/消息 _ <区域设置>。属性在你的主题目录中。 然后将其添加到语言环境中的财产 <主题类型>/主题。属性。 对于一种语言,用户可以使用该领域登录,账户电子邮件主题必须支持语言,所以你 需要为这些主题类型添加您的语言。

例如,要将挪威语翻译添加到我的主题主题创建文件主题/我的主题/登录/消息 _ 编号属性与 以下内容:

用户名称或电子邮件 = 布鲁克纳文
密码 = 密码

您未提供翻译的所有邮件将使用默认的英语翻译。

然后编辑主题/我的主题/登录/主题。属性并添加:

语言环境 = en,否

你也需要对账户电子邮件主题类型。 要做到这一点,创建主题/我的主题/帐户/消息 _ 编号属性主题/我的主题/电子邮件/消息 _ 编号属性。 将这些文件留空将导致使用英文消息。 然后复制 主题/我的主题/登录/主题。属性主题/我的主题/帐户/主题。属性主题/我的主题/电子邮件/主题。属性

最后,您需要为语言选择器添加翻译。 这是通过在英文翻译中添加消息来完成的。 为此,将以下内容添加到 主题/我的主题/帐户/消息主题/我的主题/登录/消息:

locale_no = 挪威

默认情况下,消息属性文件应使用ISO-8859-1进行编码。 也可以使用特殊的标头指定编码。 例如,要使用UTF-8编码:

# 编码: UTF-8
用户名或电子邮件 = ....

区域设置选择器关于如何选择当前语言环境的详细信息。

自定义身份提供者图标

Keycloak支持为自定义身份提供者添加图标,这些图标显示在登录屏幕上。 你只需要在你的登录定义图标类主题。属性文件 (即主题/我的主题/登录/主题。属性) 带有关键图案kcLogoIdP-<别名>。 对于具有别名的身份提供者我的提供者,您可以添加一行,如下所示主题。属性自定义主题的文件。

kcLogoIdP-myProvider = fa fa-lock

所有图标都可以在patternfly4的官方网站上找到。 社交提供商的图标已经在基本登录主题属性中定义 (主题/Keycloak/登录/主题。属性),在那里你可以激励自己。

HTML模板

Keycloak使用Freemarker模板为了生成HTML。 您可以通过以下方式覆盖自己主题中的单个模板 创建<主题类型>/<模板>。ftl。 有关使用的模板列表,请参见主题/基础/<主题类型>

创建自定义模板时,最好将模板从基本主题复制到您自己的主题,然后应用所需的修改。 忍受 升级到新版本的Keycloak时,请注意,如果 适用。

例如,为我的主题主题副本主题/基础/登录.ftl主题/我的主题/登录在编辑器中打开它。 在第一行 (<# import &hellip; >) 之后添加<h1> 你好世界!</h1>就像这样:

<# 导入 “模板.ftl” 作为布局>
<h1>你好,世界!</h1>
...

看看自由市场手册有关如何编辑模板的更多详细信息。

电子邮件

要编辑电子邮件的主题和内容,例如密码恢复电子邮件,请向电子邮件你的主题类型。 每封邮件有三条信息。 一个用于主题,一个用于纯文本主体,一个用于html主体。

要查看所有可用的电子邮件,请查看主题/基础/电子邮件/消息

例如,更改密码恢复电子邮件的我的主题主题创建主题/我的主题/电子邮件/消息使用以下内容 内容:

Passwordreset主题 = 我的密码恢复
passwordResetBody = 重置密码链接: {0}
密码重置密码

部署主题

通过将主题目录复制到Keycloak,可以将主题部署到主题或者可以部署为存档。 在开发过程中,您可以复制 主题为主题目录,但在生产中,您可能需要考虑使用存档。 一个存档使拥有的版本副本变得更简单 主题,尤其是当您有多个Keycloak实例时,例如与群集。

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

例如我的主题主题创建我的主题。罐子内容:

  • META-INF/keycloak-themes.json

  • 主题/我的主题/登录/主题。属性

  • 主题/我的主题/登录

  • 主题/我的主题/登录/资源/css/样式

  • 主题/我的主题/登录/资源/img/image.png

  • 主题/我的主题/登录/消息

  • 主题/我的主题/电子邮件/消息

的内容META-INF/keycloak-themes.json在这种情况下,将是:

{
    主题: [{
        名称:我的主题,
        类型: [登录,电子邮件]
    }]
}

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

要将存档部署到Keycloak,只需将其放入独立/部署/目录 Keycloak,它将自动加载。

主题选择器

默认情况下,使用为领域配置的主题,但客户端能够覆盖登录除外 主题。 可以通过主题选择器SPI更改此行为。

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

要创建自定义主题选择器,您需要实现电子供应商工厂电子供应商

按照中的步骤操作服务提供商接口有关如何创建和部署自定义的更多详细信息 提供商。

主题资源

在Keycloak中实现自定义提供程序时,通常可能需要添加其他模板,资源和消息包。

一个示例用例是自定义身份验证器这需要额外的模板和资源。

加载额外主题资源的最简单方法是创建一个包含模板的JAR主题-资源/模板 中的资源主题-资源/资源和邮件捆绑包主题-资源/消息把它放到独立/部署/Keycloak的目录。

如果您想要一种更灵活的方式来加载模板和资源,可以通过ThemeResourceSPI实现。 通过实施供应商工厂供应商您可以决定如何加载模板 和资源。

按照中的步骤操作服务提供商接口有关如何创建和部署自定义的更多详细信息 提供商。

区域设置选择器

默认情况下,使用默认localeselectorprovider它实现了本地电子供应商接口。 禁用国际化时,英语是默认语言。 在启用国际化的情况下,根据服务器管理指南

这种行为可以通过本地电子spi通过实施本地电子供应商本地电子供应商工厂

本地电子供应商接口只有一个方法,resolveLocale,它必须返回一个区域设置,给定真实模型和可空的用户模型。 实际请求可从KeycloakSession # getContext方法。

自定义实现可以扩展默认localeselectorprovider为了重用默认行为的一部分。 例如,忽略接受语言请求头,一个自定义实现可以扩展默认提供程序,覆盖它的获取接受语言headerlocale,并返回一个null值。 结果,区域设置选择将取决于领域的默认语言。

按照中的步骤操作服务提供商接口有关如何创建和部署自定义提供程序的更多详细信息。

自定义用户属性

您可以使用自定义主题将自定义用户属性添加到注册页面和帐户管理控制台。 本章介绍如何添加属性 到自定义主题,但您应该参考主题关于如何创建自定义主题的章节。

注册页面

为了能够在注册页面中输入自定义属性,请复制模板主题/基础/登录/注册.ftl到您的自定义主题的登录类型。 然后 在编辑器中打开副本。

作为将手机号码添加到注册页面的示例,将以下代码片段添加到表单中:

<div =表格组>
   <div =$ {属性。kcLabelWrapperClass!}>
       <标签 对于=用户属性.移动 =$ {属性。kcLabelClass!}>手机号码</label>
   </div>

   <div =$ {属性。kcinputrapperclass!}>
       <输入 类型=文本 =$ {属性。kcInputClass!} id=用户属性.移动 名称=用户属性.移动 =${(register.formData['user.Attributes.mobi le']!'')}/>
   </div>
</div>

确保输入html元素的名称以用户。属性。。 在上面的示例中,属性将由Keycloak存储,名称为移动

要查看更改,请确保您的领域将您的自定义主题用于登录主题,并打开注册页面。

账号管理控制台

要能够在帐户管理控制台的用户配置文件页面中管理自定义属性,请复制模板主题/基础/账户.ftl到 自定义主题的帐户类型。 然后在编辑器中打开副本。

作为将手机号码添加到帐户页面的示例,将以下代码片段添加到表单中:

<div =表格组>
   <div =col-sm-2 col-md-2>
       <标签 对于=用户属性.移动 =控制标签>手机号码</label>
   </div>

   <div =col-sm-10 col-md-10>
       <输入 类型=文本 =表单控制 id=用户属性.移动 名称=用户属性.移动 =${(account.attributes.mobi le!'')}/>
   </div>
</div>

确保输入html元素的名称以用户。属性。

要查看更改,请确保您的领域将您的自定义主题用于帐户主题,并在帐户管理控制台中打开 “用户配置文件” 页面。

身份经纪api

Keycloak可以将身份验证委托给父IDP进行登录。 一个典型的例子就是这种情况 您希望用户能够通过Facebook或Google等社交提供商登录的地方。 Keycloak 还允许您将现有帐户链接到经纪的国内流离失所者。 本节讨论您的应用程序的一些api 可以使用,因为它与身份经纪有关。

检索外部IDP令牌

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

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

GET /auth/realms/{realm}/broker/{provider_alias}/令牌HTTP/1.1
主机: 本地主机: 8080
授权: 承载 <密钥斗篷访问令牌>

应用程序必须已通过Keycloak进行身份验证,并已收到访问令牌。 此访问令牌 将需要有经纪人客户级角色读取令牌集。 这意味着用户必须具有此角色的角色映射 并且客户端应用程序必须在其范围内具有该角色。 在这种情况下,鉴于您正在Keycloak中访问受保护的服务,您需要在用户身份验证期间发送Keycloak发出的访问令牌。 在broker配置页面中,您可以通过打开存储的令牌可读开关。

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

客户发起的帐户链接

一些应用程序希望与Facebook等社交提供商集成,但不想提供通过登录的选项 这些社会提供者。 Keycloak提供了一个基于浏览器的API,应用程序可以使用它来链接现有的 特定外部IDP的用户帐户。 这被称为客户端发起的帐户链接。 账号链接只能由OIDC应用发起。

它的工作方式是应用程序将用户的浏览器转发到Keycloak服务器上的URL,请求 它希望将用户的帐户链接到特定的外部提供商 (即Facebook)。 服务器 启动与外部提供程序的登录。 浏览器在外部提供程序登录并重定向 回到服务器。 服务器建立链接,并通过确认重定向回应用程序。

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

  • 必须在管理控制台中为用户的领域配置和启用所需的身份提供程序。

  • 用户帐户必须已经通过OIDC协议以现有用户身份登录

  • 用户必须有一个帐户。管理-帐户或者帐户。管理-帐户-链接角色映射。

  • 必须向应用程序授予其访问令牌中这些角色的范围

  • 应用程序必须有权访问其访问令牌,因为它需要其中的信息来生成重定向URL。

要启动登录,应用程序必须制作一个URL,并将用户的浏览器重定向到该URL。 URL看起来像这样:

/{auth-server-root}/auth/realms/{realm}/broker/{provider}/link?client_id ={id}& redirect_uri ={uri}& nonce ={nonce}& hash ={hash}

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

提供者

这是您在身份提供者管理控制台的部分。

客户id

这是您的应用程序的OIDC客户端id。 当您在管理控制台中将应用程序注册为客户端时, 您必须指定此客户端id。

重定向uri

这是建立帐户链接后要重定向到的应用程序回调URL。 它必须是有效的 客户端重定向URI模式。 换句话说,它必须匹配您注册时定义的有效URL模式之一 管理控制台中的客户端。

随机性

这是一个随机字符串,您的应用程序必须生成

哈希

这是一个Base64 URL编码的哈希。 此哈希是由Base64 URL编码的SHA_256哈希生成的随机性+token.getSessionState()+token.getIssuedFor()+提供者。 令牌变量从OIDC访问令牌中获取。 基本上,您正在散列随机随机数,用户会话id,客户端id和身份 您要访问的提供商别名。

下面是一个Java Servlet代码的示例,该代码生成用于建立帐户链接的URL。

   KeycloakSecurityContext会话 = (KeycloakSecurityContext) httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName());
   AccessToken token = session.getToken();
   字符串clientId = 令牌.getIssuedFor();
   字符串随机数 =UUID。Randoluuid ()。toString();
   MessageDigestmd =null;
   尝试{
      md =MessageDigest。getInstance (SHA-256);
   }抓住(NoSuchAlgorithmExceptione) {
      投掷 新的 运行时间异常(e);
   }
   字符串输入 = 随机数 + 令牌。getSessionState() + clientId + 提供程序;
   字节[]check = md.digest(input.getBytes(StandardCharsets.UTF_8));
   字符串hash = Base64Url.encode (检查);
   请求。getSession()。setAttribute (哈希,哈希);
   字符串重定向 =...;
   字符串accountLinkUrl = KeycloakUriBuilder.fromUri(authServerRootUrl)
                    。路径 (/auth/realms/{realm}/broker/{provider}/link)
                    。queryParam (随机性,随机数)
                    。queryParam (哈希,哈希)
                    。queryParam (客户id,clientId)
                    。queryParam (重定向uri,redirectUri)。构建 (领域,提供程序)。toString();

为什么包含这个哈希? 我们这样做是为了保证auth服务器知道客户端应用程序启动了请求,而没有其他流氓应用程序 只是随机要求将用户帐户链接到特定的提供商。 auth服务器将首先通过检查SSO来检查用户是否已登录 登录时设置的cookie。 然后,它将尝试根据当前登录重新生成哈希,并将其与应用程序发送的哈希匹配。

链接帐户后,auth服务器将重定向回重定向uri。 如果服务链接请求有问题, 身份验证服务器可能会也可能不会重定向回重定向uri。 浏览器可能只是在错误页面结束,而不是被重定向回来 到应用程序。 如果出现错误情况,并且身份验证服务器认为它足够安全,可以重定向回客户端应用程序,则需要额外的 错误查询参数将附加到重定向uri

虽然此API保证应用程序启动了请求,但它并不能完全阻止此操作的CSRF攻击。 应用程序 仍然负责防范CSRF攻击目标本身。

刷新外部令牌

如果您使用的是通过登录提供程序生成的外部令牌 (即Facebook或GitHub令牌),则可以通过重新启动帐户链接API来刷新此令牌。

服务提供商接口 (SPI)

Keycloak旨在涵盖大多数用例,而不需要自定义代码,但我们也希望它是可定制的。 要实现此功能,Keycloak具有许多服务提供商接口 (SPI),您可以为其实现自己的提供商。

实施SPI

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

例如,要实现主题选择器SPI,您需要实现ThemeSelectorProviderFactory和ThemeSelectorProvider,并提供文件 元信息/服务/组织。Keycloak。主题。电子产品提供工厂

示例电子提供工厂:

包装 org.acme.provider;

导入...

公众  我的电子供应商工厂 工具 电子供应商工厂{

    @ 覆盖
    公众 电子供应商 创建(KeycloakSession 会话) {
        返回 新的 我的电子供应商(会话);
    }

    @ 覆盖
    公众 虚空init (配置。范围配置) {
    }

    @ 覆盖
    公众 虚空postInit(KeycloakSessionFactory factory) {
    }

    @ 覆盖
    公众 虚空关闭 () {
    }

    @ 覆盖
    公众 字符串getId() {
        返回 神秘选民;
    }
}
Keycloak创建提供者工厂的单个实例,从而可以为多个请求存储状态。 提供程序实例是通过为每个请求在工厂上调用create来创建的,因此这些实例应该是轻量级对象。

示例电子提供商:

包装 org.acme.provider;

导入...

公众  我的电子供应商 工具 电子供应商{

    公众 我的电子供应商(KeycloakSession 会话) {
    }


    @ 覆盖
    公众 字符串 获取名称(主题。类型 类型) {
        返回 我的主题;
    }

    @ 覆盖
        公众 虚空关闭 () {
    }
}

示例服务配置文件 (元信息/服务/组织。Keycloak。主题。电子产品提供工厂):

组织。acme。提供者。MyThemeSelectorProviderFactory

您可以通过以下方式配置您的提供商独立。xml,standalone-ha.xml,或者域。xml

例如,将以下内容添加到独立。xml:

<spi 名称=电子选择器>
    <提供者 名称=神秘选民 已启用=>
        <属性>
            <属性 名称=主题 =我的主题/>
        </属性>
    </提供者>
</spi>

然后,您可以在供应商工厂init方法:

公众 虚空init (配置。范围配置) {
    字符串themeName = config.get (主题);
}

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

公众  我的电子供应商 工具电子供应商 {

    私人KeycloakSession会话;

    公众MyThemeSelectorProvider(KeycloakSession会话) {
        这个。session = session;
    }

    @ 覆盖
    公众 字符串获取名称 (主题。类型) {
        返回会话。getContext()。getRealm()。getLoginTheme();
    }
}

在管理控制台中显示来自您的SPI实现的信息

有时,向Keycloak管理员显示有关您的提供商的其他信息很有用。 您可以显示提供商构建时间信息 (例如 当前已安装的自定义提供程序),提供程序的当前配置 (例如,提供程序与之对话的远程系统的url) 或一些操作信息 (您的提供商与之交谈的远程系统的平均响应时间)。 Keycloak管理控制台提供服务器信息页面来显示此类信息。

要显示来自您的提供商的信息,就足以实现组织。Keycloak。提供者。服务器信息提供工厂您的界面供应商工厂

的示例实现我的电子供应商工厂从上一个例子中:

包装 org.acme.provider;

导入...

公众  我的电子供应商工厂 工具 电子供应商工厂,服务器信息提供工厂{
    ...

    @ 覆盖
    公众 地图<字符串,字符串>获取操作信息() {
        地图<字符串,字符串>ret=新的 链接地图<>();
        ret.put (主题名称,我的主题);
        返回ret;
    }
}

使用可用的提供商

在您的提供程序实现中,您可以使用Keycloak中可用的其他提供程序。 现有的提供程序通常可以用 的用法KeycloakSession,如本节所述,您的提供商可以使用实施SPI

Keycloak有两种提供者类型:

  • 单实现提供程序类型-在Keycloak运行时中只能有一个特定提供程序类型的单个活动实现。 例如主机名称提供程序指定Keycloak要使用的主机名,并为整个Keycloak服务器共享。 因此,对于Keycloak服务器,此提供程序只能有一个单独的实现。 如果服务器运行时有多个提供程序实现可用, 其中一个需要在keycloak子系统配置中指定独立。xml作为默认的。 例如:

<spi名称 = "主机名">
    <默认提供程序> 默认 </默认提供程序>
    ...
</spi>

默认用作默认提供程序必须与返回的ID匹配供应商工厂。getId()特定提供商工厂实现的。 在代码中,您可以获取提供程序,例如keycloakSession.getProvider (主机名称提供程序.类)

  • 多种实现提供程序类型-这些是提供者类型,允许多个实现可用并一起工作 在Keycloak运行时。 例如事件监听器提供程序允许有多个实现可用和注册,这意味着 该特定事件可以发送到所有侦听器 (jboss-logging,sysoout等)。 在代码中,您可以获取提供程序的指定实例 例如,例如session.getProvider(EventListener.class,“jboss-logging”)。 您需要指定供应商 _ 标识作为第二个参数的提供者 如上所述,此提供程序类型可以有多个实例。 提供者ID必须与供应商工厂。getId()的 特定提供商工厂实现。 某些提供程序类型可以使用组件模型作为第二个参数和一些 (例如认证器) 甚至 需要用的用法检索Keycloak会议工厂。 不建议以这种方式实现您自己的提供商,因为将来可能会被弃用。

注册提供程序实现

注册提供程序实现有两种方法。 在大多数情况下,最简单的方法是使用Keycloak deploer 方法,因为这会自动为您处理许多依赖项。 它还支持热部署和重新部署。

另一种方法是作为一个模块进行部署。

如果要创建自定义SPI,则需要将其部署为模块,否则我们建议使用Keycloak deploer方法。

使用Keycloak部署器

如果您将提供商jar复制到Keycloak独立/部署/目录,您的提供商将自动部署。 热部署也有效。 此外,您的提供商jar的工作方式与部署在WildFly中的其他组件类似 环境,因为他们可以使用像jboss-deployment-structure.xml文件。 这个文件允许你 设置对其他组件的依赖关系,并加载第三方jar和模块。

提供者罐子也可以包含在其他可部署的单位中,例如耳朵和战争。 用耳朵部署实际上使 使用第三方罐子真的很容易,因为你可以把这些库放在耳朵里lib/目录。

使用模块注册提供程序

要使用模块注册提供程序,请首先创建一个模块。 为此,您可以使用jboss-cli脚本或在其中手动创建文件夹KEYCLOAK_HOME/modules加上你的罐子和一个模块.xml。 例如,使用jboss-cli脚本执行:

KEYCLOAK_HOME/bin/jboss-cli.sh-命令 = "模块添加-名称 = org.acme.provider-资源 = 目标/提供者.jar-依赖 = org.keycloak-core,org.keycloak-server-spi"

或者手动创建它,首先创建文件夹KEYCLOAK_HOME/modules/org/acme/provider/main。 然后复制提供者。jar到此文件夹并创建模块.xml具有以下内容:

<?xml version = "1.0" encoding = "UTF-8"?>
<模块 xmlns=urn:jboss: 模块: 1.3 名称=org.acme.provider>
    <资源>
        <资源-根 路径=提供者。jar/>
    </资源>
    <依赖>
        <模块 名称=组织。Keycloak-核心/>
        <模块 名称=组织。Keycloak-服务器-spi/>
    </依赖>
</模块>

创建模块后,您需要使用Keycloak注册此模块。 这是通过编辑的keycloak-server子系统部分来完成的 独立。xml,standalone-ha.xml,或者域。xml,并将其添加到提供者:

<子系统 xmlns=urn:jboss: 域: keycloak-服务器: 1.1>
    <网络上下文>auth</web-context>
    <提供者>
        <提供者>模块: org.keycloak.examples.event-sysoout</提供者>
    </提供者>
    ...

禁用提供商

您可以通过将提供程序的enabled属性设置为false来禁用提供程序 在独立。xml,standalone-ha.xml,或者域。xml。 例如,禁用Infinispan用户缓存提供程序添加:

<spi 名称=用户缓存>
    <提供者 名称=infinispan 已启用=/>
</spi>

利用Java EE

只要您设置了元信息/服务 正确归档以指向您的提供商。 例如,如果您的提供商需要使用第三方库,则您 可以将您的提供商打包在ear中,并将这些第三方库存储在ear的lib/目录。 另请注意,提供商罐子可以利用jboss-deployment-structure.xmlEjb、战争和耳朵的文件 可以在野蝇环境中使用。 有关此文件的更多详细信息,请参见WildFly文档。 它 允许您在其他细颗粒动作中引入外部依赖。

供应商工厂实现要求是纯java对象。 但是,我们目前也支持 将提供程序类实现为有状态ejb。 这是你会怎么做:

@ 有状态的
@ 本地(EjbExampleUserStorageProvider.class)
公众  EjbExampleUserStorageProvider 工具用户存储提供程序,
        UserLookupProvider,
        UserRegistrationProvider,
        用户查询提供程序,
        证书,
        凭据输入验证器,
        OnUserCache
{
    @ PersistenceContext
    受保护实体经理em;

    受保护组件模型模型;
    受保护KeycloakSession会话;

    公众 虚空setModel (组件模型) {
        这个。模型 = 模型;
    }

    公众 虚空setSession(KeycloakSession会话) {
        这个。session = session;
    }


    @ 移除
    @ 覆盖
    公众 虚空关闭 () {
    }
...
}

你必须定义@ 本地注释并在那里指定您的提供者类。 如果你不这样做,EJB将 不能正确代理提供程序实例,您的提供程序将无法正常工作。

你必须把@ 移除上的注释关闭 ()您的提供商的方法。 如果你不这样做,有状态的豆子 永远不会被清理,您最终可能会看到错误消息。

的实现供应商工厂被要求是普通的java对象。 你的工厂班会 在其中执行有状态EJB的JNDI查找创建 ()方法。

公众  EjbExampleUserStorageProviderFactory
        工具用户存储提供工厂 <EjbExampleUserStorageProvider> {

    @ 覆盖
    公众EjbExampleUserStorageProvider创建 (KeycloakSession会话,组件模型) {
        尝试{
            初始上下文ctx =新的 初始上下文();
            EjbExampleUserStorageProvider = (EjbExampleUserStorageProvider)ctx.lookup (
                     java: 全局/用户-存储-jpa-示例/+ EjbExampleUserStorageProvider.class.getSimpleName());
            provider.setModel (模型);
            provider.setSession (会话);
            返回提供者;
        }抓住(异常e) {
            投掷 新的 运行时间异常(e);
        }
    }

JavaScript提供商

Keycloak能够在运行时执行脚本,以允许管理员自定义特定功能:

  • 认证器

  • JavaScript策略

  • OpenID连接协议映射器

认证器

身份验证脚本必须至少提供以下功能之一: 认证 (..),它是从身份验证器 # 身份验证 (身份验证flowcontext) 行动 (..),它是从验证器 # 操作 (AuthenticationFlowContext)

自定义认证器至少应该提供认证 (..)功能。 您可以使用javax。脚本。绑定代码中的脚本。

脚本

脚本模型访问脚本元数据

境界

真实模型

用户

当前用户模型

会话

活跃的KeycloakSession

身份验证会话

当前认证会话模型

httpRequest

当前org.jboss.resteasy.spi.HttpRequest

日志

a组织。jboss。日志记录。记录器范围为脚本基础认证器

您可以从上下文参数传递给身份验证 (上下文) 行动 (上下文)功能。
AuthenticationFlowError = Java。类型 (组织。密钥斗篷。身份验证错误);

函数 验证(上下文) {

  日志信息 (脚本.名称 +-> 跟踪身份验证:+ 用户。用户名);

  如果(用户。用户名 ===测试仪
      & & 用户.getAttribute (一些属性)
      & & 用户.getAttribute (一些属性).包含 (someValue)) {

      上下文。失败 (AuthenticationFlowError.INVALID_USER);
      返回;
  }

  上下文。成功 ();
}

使用要部署的脚本创建一个JAR

JAR文件是常规的ZIP文件,带有。罐子延。

为了使您的脚本可用于Keycloak,您需要将它们部署到服务器。 为此,你应该创建 a罐子具有以下结构的文件:

META-INF/keycloak-scripts.json

my-script-authenticator.js
my-script-policy.js
my-script-mapper.js

META-INF/keycloak-scripts.json是一个文件描述符,它提供有关要部署的脚本的元数据信息。 它是一个JSON文件,结构如下:

{
    认证器: [
        {
            名称:我的认证器,
            文件名:my-script-authenticator.js,
            描述:我的身份验证器来自一个JS文件
        }
    ],
    政策: [
        {
            名称:我的政策,
            文件名:my-script-policy.js,
            描述:我的政策来自一个JS文件
        }
    ],
    映射器: [
        {
            名称:我的映射器,
            文件名:my-script-mapper.js,
            描述:我的映射器来自一个JS文件
        }
    ]
}

此文件应引用要部署的不同类型的脚本提供程序:

  • 认证器

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

  • 政策

    对于使用Keycloak授权服务时的JavaScript策略 您可以在同一个JAR文件中有一个或多个策略

  • 映射器

    对于OpenID连接脚本协议映射器。 您可以在同一个JAR文件中有一个或多个映射器

对于您的每个脚本文件罐子文件您必须在META-INF/keycloak-scripts.json将脚本文件映射到特定的提供商类型。 为此,您应该为每个条目提供以下属性:

  • 名称

    一个友好的名称,将用于通过Keycloak管理控制台显示脚本。 如果没有提供,名称 将使用脚本文件的

  • 描述

    更好地描述脚本文件意图的可选文本

  • 文件名

    脚本文件的名称。 这个属性是强制性并且应该映射到JAR内的文件。

部署脚本JAR

一旦您有了带有描述符的JAR文件和要部署的脚本,您只需要将JAR复制到Keycloak独立/部署/目录。

使用Keycloak管理控制台上传脚本

不建议使用通过管理控制台上传脚本的功能,并且将在以后的Keycloak版本中删除

管理员不能将脚本上传到服务器。 这种行为防止了对系统的潜在伤害,以防万一 恶意脚本被意外执行。 管理员应始终使用 JAR文件,用于在运行时运行脚本时防止攻击。

可以显式启用上传脚本的功能。 这应该非常小心地使用,应该制定计划 尽快将所有脚本直接部署到服务器。

有关如何启用上传脚本特征。 请看看配置文件

可用SPIs

如果要在运行时查看所有可用spi的列表,可以检查服务器信息管理控制台中的页面,如中所述管理控制台节。

扩展服务器

Keycloak SPI框架提供了实现或覆盖特定内置提供商的可能性。 然而Keycloak 还提供了扩展其核心功能和域的功能。 这包括以下可能性:

  • 将自定义REST端点添加到Keycloak服务器

  • 添加您自己的自定义SPI

  • 将自定义JPA实体添加到Keycloak数据模型

添加自定义REST端点

这是一个非常强大的扩展,它允许您将自己的REST端点部署到Keycloak服务器。 它启用各种扩展,例如 在Keycloak服务器上触发功能的可能性,该功能无法通过默认的内置Keycloak REST端点集获得。

要添加自定义REST端点,您需要实现RealmResourceProviderFactoryRealmResourceProvider接口。 RealmResourceProvider有一个重要的方法:

对象getResource();

它允许您返回一个对象,该对象充当JAX-RS资源。 有关更多详细信息,请参见Javadoc和我们的示例。 中的示例分布中有一个非常简单的示例提供者/休息还有一个更先进的例子提供商/域扩展, 它显示了如何添加经过身份验证的REST端点和其他功能,如添加您自己的SPI 或者使用您自己的JPA实体扩展数据模型

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

添加您自己的自定义SPI

这尤其有用自定义REST端点。 要添加您自己的SPI,您需要 实现接口组织。Keycloak。提供者。Spi并定义您的SPI的ID和供应商工厂提供者类。 看起来像这样:

包装 组织。Keycloak。例子。域名扩展。spi;

导入...

公众  示例spi 工具 Spi{

    @ 覆盖
    公众 布尔 内部() {
        返回 ;
    }

    @ 覆盖
    公众 字符串getName() {
        返回 示例;
    }

    @ 覆盖
    公众 <? 延伸 提供者> 获取提供类 () {
        返回ExampleService.class;
    }

    @ 覆盖
    @ 抑制警告(原始类型)
    公众 <? 延伸提供工厂> 获取提供工厂 () {
        返回ExampleServiceProviderFactory.class;
    }

}

然后你需要创建文件META-INF/服务/org.keycloak.provider.Spi并将您的SPI的类添加到其中。 例如:

组织。keycloak。示例。域扩展。spi。示例spi

下一步是创建接口示例服务提供工厂,从供应商工厂示例服务,从提供者。 的示例服务通常会包含您的用例所需的业务方法。 请注意,示例服务提供工厂实例 始终是每个应用程序的范围,但是示例服务为每个请求作用域 (或更准确地为每个请求KeycloakSession生命周期)。

最后,您需要以与服务提供商接口章。

有关更多详细信息,请查看以下示例分布:提供商/域扩展,它显示了一个类似于上面的示例SPI。

将自定义JPA实体添加到Keycloak数据模型

如果Keycloak数据模型与您想要的解决方案不完全匹配,或者如果您想向Keycloak添加一些核心功能, 或者当您拥有自己的REST端点时,您可能希望扩展Keycloak数据模型。 我们使您能够添加您的 拥有KeycloakJPA的JPA实体实体经理

要添加您自己的JPA实体,您需要实现JpaEntityProviderFactoryJpentityprovider。 的Jpentityprovider 允许您返回自定义JPA实体的列表,并提供Liquibase变更日志的位置和id。 一个示例实现可以是这样的:

这是一个不受支持的API,这意味着您可以使用它,但不能保证它不会在没有警告的情况下被删除或更改。
公众  示例jparentityprovider 工具Jpentityprovider {

    // 您的JPA实体列表。
    @ 覆盖
    公众 列表<<?>> 获取实体 () {
        返回 收藏。<<?>>singletonList (公司.类);
    }

    // 这用于返回Liquibase changelog文件的位置。
    // 如果您不希望Liquibase创建和更新数据库架构,则可以返回null。
    @ 覆盖
    公众 字符串getChangelogLocation() {
            返回 META-INF/example-changelog.xml;
    }

    // Helper方法,它将由Liquibase内部使用。
    @ 覆盖
    公众 字符串getFactoryId() {
        返回 样品;
    }

    ...
}

在上面的示例中,我们添加了一个由类表示的单个JPA实体公司。 在您的REST端点的代码中,您可以使用类似 这个来检索实体经理并在其上调用DB操作。

EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
公司我的公司 = em.find (公司.类,123);

方法getChangelogLocationgetFactoryId对于支持通过Liquibase自动更新您的实体很重要。 液体碱 是一个更新数据库模式的框架,Keycloak内部使用它来创建数据库模式并在版本之间更新数据库模式。 您可能需要使用 它以及为您的实体创建一个变更日志。 请注意,您自己的Liquibase变更日志的版本控制是独立的 Keycloak版本。 换句话说,当您更新到新的Keycloak版本时,您不会被迫更新您的 同时架构。 反之亦然,即使不更新Keycloak版本,您也可以更新架构。 Liquibase更新 总是在服务器启动时完成的,所以要触发模式的数据库更新,你只需要将新的变更集添加到你的Liquibase变更日志文件中 (在上面的例子中 是文件META-INF/example-changelog.xml必须与JPA实体打包在同一个罐子中,并且示例jparentityprovider),然后重新启动服务器。 数据库架构将在启动时自动更新。

有关更多详细信息,请查看示例中的示例分布提供商/域扩展,它显示了示例jparentityproviderexample-changelog.xml如上所述。

在对Liquibase变更日志进行任何更改并触发数据库更新之前,请不要忘记始终备份数据库。

身份验证SPI

Keycloak包括一系列不同的身份验证机制: kerberos,password,otp和其他。 这些机制可能无法满足您的所有要求,您可能希望插入自己的自定义机制。 Keycloak提供了一个身份验证SPI,您可以使用它来编写新的插件。 管理控制台支持应用、排序和配置这些新机制。

Keycloak还支持简单的注册表格。 可以启用和禁用此表单的不同方面,即 Recaptcha支持可以关闭和打开。 可以使用相同的身份验证SPI将另一个页面添加到注册流或完全重新实现它。 还有一个额外的细粒度SPI,您可以用来向内置注册表单添加特定的验证和用户扩展。

Keycloak中的必需操作是用户在进行身份验证后必须执行的操作。 成功执行该操作后,用户不必再次执行该操作。 Keycloak带有一些内置的必需操作,例如 “重置密码”。 此操作迫使用户在登录后更改密码。 您可以编写并插入自己所需的操作。

如果您的身份验证器或所需的操作实现使用某些用户属性作为用于链接/建立用户身份的元数据属性, 然后请确保用户无法编辑属性,并且相应的属性是只读的。 请参阅中的详细信息威胁模型缓解章节

条款

为了首先了解身份验证SPI,让我们来看看一些用来描述它的术语。

认证流程

流是登录或注册期间必须进行的所有身份验证的容器。 如果转到管理控制台身份验证页面,则可以查看系统中所有已定义的流以及它们由哪些身份验证器组成。 流可以包含其他流。 您还可以为浏览器登录、直接授予访问权限和注册绑定新的不同流。

认证器

身份验证器是可插拔的组件,它保存用于在流程中执行身份验证或操作的逻辑。 通常是单身。

执行

执行是将身份验证器绑定到流,将身份验证器绑定到身份验证器的配置的对象。 流包含执行条目。

执行要求

每个执行都定义了身份验证器在流中的行为方式。 该要求定义了身份验证器是启用的、禁用的、有条件的、必需的还是替代的。 另一种要求意味着身份验证器足以验证其所进入的流程,但这不是必需的。 例如,在内置的浏览器流中,cookie身份验证,身份提供程序重定向器以及 表单子流都是替代的。 由于它们以从上到下的顺序执行,因此,如果其中一个成功,则流程为 成功,并且不评估流 (或子流) 中的任何后续执行。

验证器配置

此对象为身份验证器的配置定义了身份验证流程中特定执行的配置。 每个执行可以有不同的配置。

所需的操作

身份验证完成后,用户可能有一个或多个一次性操作,他必须完成才能被允许登录。 用户可能需要设置OTP令牌生成器或重置过期密码,甚至接受条款和条件文档。

算法概述

让我们来谈谈这一切是如何工作的浏览器登录。 让我们假设以下流、执行和子流。

饼干-替代
Kerberos-替代方案
表单子流-替代
           用户名/密码表单-必需
           条件OTP子流-条件
                      条件-用户配置-必需
                      OTP表格-必填

在表单的顶层,我们有3个执行,其中所有执行都是必需的。 这意味着如果其中任何一个成功,那么其他的就不必执行。 如果设置了SSO Cookie或成功的Kerberos登录,则不会执行用户名/密码表单。 让我们逐步了解从客户端首次重定向到keycloak以对用户进行身份验证的步骤。

  1. OpenID Connect或SAML协议提供程序解包相关数据,验证客户端和任何签名。 它创建了一个身份验证会话模型。 它查找浏览器流应该是什么,然后开始执行该流。

  2. 流查看cookie执行,并看到它是替代方案。 它加载cookie提供程序。 它检查cookie提供程序是否要求用户已经与身份验证会话相关联。 Cookie提供程序不需要用户 如果这样做,流程将中止,用户将看到错误屏幕。 Cookie提供程序然后执行。 它的目的是查看是否有SSO cookie集。 如果有一个集合,则对其进行验证,并验证UserSessionModel并将其与AuthenticationSessionModel关联。 如果SSO Cookie存在并已验证,则cookie提供程序将返回success() 状态。 由于cookie提供程序返回成功,并且在流的此级别上的每次执行都是替代的,因此不会执行其他执行,这将导致成功登录。 如果没有SSO cookie,则cookie提供程序返回,状态为尝试 ()。 这意味着没有错误条件,但也没有成功。 提供者尝试过,但请求只是没有设置来处理此身份验证器。

  3. 接下来,流程查看Kerberos执行。 这也是一种选择。 kerberos提供程序也不需要已经设置用户并将其与AuthenticationSessionModel相关联,因此执行此提供程序。 Kerberos使用SPNEGO浏览器协议。 这需要服务器和客户端交换协商头之间的一系列挑战/响应。 kerberos提供程序没有看到任何协商标头,因此它假定这是服务器和客户端之间的第一次交互。 因此,它会创建对客户端的HTTP挑战响应,并设置forceChallenge() 状态。 forceChallenge() 意味着此HTTP响应不能被流忽略,必须返回给客户端。 如果提供程序返回了challenge() 状态,则流程将保留challenge响应,直到尝试所有其他替代方案为止。 因此,在此初始阶段,流程将停止,挑战响应将被发送回浏览器。 如果浏览器随后以成功的协商标头进行响应,则提供者将用户与AuthenticationSession关联起来,并且流结束,因为该流级别上的其余执行都是替代方案。 否则,kerberos提供程序再次设置尝试 () 状态,并且流继续。

  4. 下一个执行是一个称为Forms的子流。 加载此子流的执行,并出现相同的处理逻辑。

  5. Forms子流中的第一个执行是UsernamePassword提供程序。 此提供程序也不要求用户已经与流关联。 此提供程序创建一个挑战HTTP响应,并将其状态设置为挑战 ()。 此执行是必需的,因此flow会满足此挑战,并将HTTP响应发送回浏览器。 此响应是用户名/密码HTML页面的呈现。 用户输入用户名和密码,然后单击提交。 此HTTP请求被定向到UsernamePassword提供程序。 如果用户输入了无效的用户名或密码,则会创建一个新的质询响应,并为此执行设置了failureChallenge() 的状态。 failureChallenge() 表示存在挑战,但是流程应将其记录为错误日志。 此错误日志可用于锁定登录失败过多的帐户或ip地址。 如果用户名和密码有效,则提供程序将用户模型与AuthenticationSessionModel关联起来,并返回success() 状态。

  6. 下一个执行是称为条件OTP的子流。 加载此子流的执行,并出现相同的处理逻辑。 它的要求是 有条件的。 这意味着流将首先评估它包含的所有条件执行器。 条件执行者是身份验证者,其 实施条件认证器,并且必须实施该方法布尔匹配条件 (AuthenticationFlowContext上下文)。 条件子流将 打电话给匹配条件它包含的所有条件执行的方法,并且如果所有条件执行都评估为true,则它的作用就好像它是必需的子流一样。 如果 不是,它会像禁用子流一样。 条件验证器仅用于此目的,而不用作验证器。 这意味着即使条件身份验证器评估为 “true”,这也不会将流或子流标记为成功。 例如, 仅包含具有条件身份验证器的条件子流的流将永远不允许用户登录。

  7. 条件OTP子流的第一次执行是条件用户配置的。 此提供程序要求用户已与流关联。 满足此要求是因为UsernamePassword提供程序已经将用户与流相关联。 该提供商的匹配条件方法将评估配置为方法用于其当前子流中的所有其他身份验证器。 如果子流程包含 将其要求设置为 “必需” 的执行者,则匹配条件方法仅在所有必需的authenticators的情况下评估为true配置为 方法评估为true。 否则,匹配条件如果任何替代身份验证器评估为true,则该方法将评估为true。

  8. 下一次执行是OTP形式。 此提供程序还要求用户已与流关联。 满足此要求是因为UsernamePassword提供程序已经将用户与流相关联。 由于此提供程序需要用户,因此还询问提供程序是否将用户配置为使用此提供程序。 如果未配置用户,则流程将设置用户在完成身份验证后必须执行的必需操作。 对于OTP,这意味着OTP设置页面。 如果配置了用户,将要求他输入otp代码。 在我们的场景中,因为有条件的 子流程,用户永远不会看到OTP登录页面,除非条件OTP子流程设置为必需。

  9. 流程完成后,身份验证处理器将创建一个UserSessionModel,并将其与AuthenticationSessionModel相关联。 然后,它检查是否需要用户在登录之前完成任何必需的操作。

  10. 首先,调用每个必需的动作的evaluateTriggers() 方法。 这允许所需的操作提供程序确定是否存在可能触发操作的某种状态。 例如,如果您的领域具有密码到期策略,则可能由此方法触发。

  11. 与调用其requiredActionChallenge() 方法的用户关联的每个必需的操作。 在这里,提供者设置一个HTTP响应,该响应将页面呈现为所需的操作。 这是通过设置挑战状态来完成的。

  12. 如果所需的操作最终成功,则将从用户的所需操作列表中删除所需的操作。

  13. 解决所有必需的操作后,用户最终将登录。

验证器SPI遍历

在本节中,我们将看一下身份验证器接口。 为此,我们将实现一个身份验证器,该身份验证器要求用户输入诸如 “您母亲的娘家姓是什么?” 之类的秘密问题的答案。 此示例已完全实现,并包含在Keycloak的演示分发的示例/提供者/身份验证器目录中。

要创建身份验证器,您必须至少实现org.keycloak.AuthenticatorFactory和身份验证器接口。 身份验证器接口定义了逻辑。 身份验证器工厂负责创建身份验证器的实例。 它们都扩展了更通用的提供者和提供者工厂接口集,就像其他Keycloak组件 (如用户联盟) 所做的那样。

某些身份验证器 (例如CookieAuthenticator) 不依赖用户拥有或知道的凭据来对用户进行身份验证。 但是,某些身份验证器 (例如PasswordForm身份验证器或OTPFormAuthenticator) 依赖于用户输入一些 信息,并对照 数据库。 例如,对于PasswordForm,身份验证器将根据存储在数据库中的哈希来验证密码的哈希,而 OTPFormAuthenticator将根据从数据库中存储的共享机密生成的OTP来验证收到的OTP。

这些类型的身份验证器被称为CredentialValidators,并且需要您实现更多的几个类:

  • 扩展org.keycloak.credential.CredentialModel的类,并且可以在数据库中生成正确格式的凭据

  • 一个实现org.keycloak.credential.CredentialProvider和接口的类,以及一个实现其CredentialProviderFactory工厂接口的类。

我们将在此过程中看到的SecretQuestionAuthenticator是一个CredentialValidator,因此我们将看到如何实现所有这些类。

打包类和部署

您将把您的类打包在一个罐子里。 这个jar必须包含一个名为组织。密钥斗篷。身份验证工厂并且必须包含在元信息/服务/您的jar的目录。 此文件必须列出您在jar中拥有的每个AuthenticatorFactory实现的完全限定的类名。 例如:

org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
org.keycloak.examples.authenticator.AnotherProviderFactory

Keycloak使用此服务/文件扫描必须加载到系统中的提供程序。

要部署此jar,只需将其复制到providers目录即可。

扩展CredentialModel类

在Keycloak中,凭据存储在凭据表中的数据库中。 它具有以下结构:

-----------------------------
| ID |
-----------------------------
| 用户标识 |
-----------------------------
| 凭据 _ 类型 |
-----------------------------
| 创建日期 |
-----------------------------
| 用户标签 |
-----------------------------
| 秘密数据 |
-----------------------------
| 凭证 _ 数据 |
-----------------------------
| 优先级 |
-----------------------------

其中:

  • ID是凭证的主键。

  • 用户标识是将凭据链接到用户的外键。

  • 凭证类型是创建过程中必须引用现有凭据类型的字符串。

  • 创建日期是凭证的创建时间戳 (长格式)。

  • 用户标签是用户对凭据的可编辑名称

  • 秘密数据包含一个静态json,其中包含无法在Keycloak之外传输的信息

  • 凭证 _ 数据包含一个json,其中包含可以在管理控制台中或通过REST API共享的凭据的静态信息。

  • 优先级定义证书对用户来说有多 “首选”,以确定当用户有多个选择时要呈现哪个证书。

由于secret_data和credential_data字段旨在包含json,因此由您决定如何构造,读取和写入 这些领域,让你有很大的灵活性。

对于此示例,我们将使用非常简单的凭据数据,仅包含向用户询问的问题:

{
  “问题”: “问题”
}

同样简单的秘密数据,只包含秘密答案:

{
  “答案”: “答案”
}

为了简单起见,这里的答案将以纯文本形式保存在数据库中,但是也可以为答案添加盐分哈希, 与Keycloak中的密码一样。 在这种情况下,秘密数据还必须包含盐的字段和凭证数据信息 关于算法如使用的算法类型和使用的迭代次数。 有关更多详细信息,您可以查阅 org.keycloak.mo dels.凭据.密码凭据模型类。

在我们的例子中,我们创建了类秘密质疑证书模型:

公众  秘密质疑证书模型 延伸凭证模型 {
    公众 静态 决赛 字符串类型 =秘密问题;

    私人 决赛SecretQuestionCredentialData;
    私人 决赛SecretQuestionSecretData;

哪里类型是我们在数据库中编写的credential_type。 为了保持一致性,我们确保此字符串始终是在以下情况下引用的字符串 获取此凭据的类型。 班级SecretQuestionCredentialData秘密问题秘密数据用于对json进行编组和解封:

公众  SecretQuestionCredentialData{

    私人 决赛 字符串问题;

    @ JsonCreator
    公众SecretQuestionCredentialData (@ JsonProperty(问题)字符串问题) {
        这个。问题 = 问题;
    }

    公众 字符串问题 () {
        返回问题;
    }
}
公众  秘密问题秘密数据{

     私人 决赛 字符串回答;

    @ JsonCreator
     公众SecretQuestionSecretData (@ JsonProperty(答案)字符串回答) {
         这个。答案 = 答案;
     }

    公众 字符串获取答案 () {
        返回回答;
    }
}

为了完全可用,SecretQuestionCredentialModel对象都必须包含来自其父类的原始json数据, 以及其自身属性中的未编组对象。 这导致我们创建一个从简单的凭证模型读取的方法, 如从数据库读取时创建的,以使秘密质疑证书模型:

私人SecretQuestionCredentialData credentialData,SecretQuestionSecretData secretData) {
    这个。credentialData = credentialData;
    这个。secretData = secretData;
}

公众 静态Secretquestioncreatefromcredentialmodel (CredentialModel credentialModel){
    尝试{
        SecretQuestionCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(),SecretQuestionCredentialData.class);
        SecretQuestionSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(),SecretQuestionSecretData.class);

        SecretQuestionCredentialModel =新的SecretQuestionCredentialModel(credentialData,secretData);
        secretQuestionCredentialModel.setUserLabel(credentialModel.getUserLabel());
        secretQuestionCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
        secretQuestionCredentialModel.setType (类型);
        secretQuestionCredentialModel.setId(credentialModel.getId());
        secretQuestionCredentialModel.setSecretData(credentialModel.getSecretData());
        secretQuestionCredentialModel.setCredentialData(credentialModel.getCredentialData());
        返回secretQuestionCredentialModel;
    }抓住(IOExceptione){
        投掷 新的 运行时间异常(e);
    }
}

以及创建一个SecretQuestionCredentialModel从问答中:

私人SecretQuestionCredentialModel (字符串问题,字符串回答) {
    凭证数据 =新的SecretQuestionCredentialData (问题);
    秘密数据 =新的SecretQuestionSecretData (答案);
}

公众 静态SecretQuestionCredentialModel创建ecretquestion (字符串问题,字符串回答) {
    SecretQuestionCredentialModel证书模型 =新的SecretQuestionCredentialModel (问题、答案);
    credentialModel。填充credentialmodelfields ();
    返回凭证模型;
}

私人 虚空填充凭据模型字段 (){
    尝试{
        setCredentialData(JsonSerialization.writeValueAsString(credentialData));
        setSecretData(JsonSerialization.writeValueAsString(secretData));
        setType (类型);
        设置创建日期 (时间。currentTimeMillis());
    }抓住(IOExceptione) {
        投掷 新的 运行时间异常(e);
    }
}

实现凭据提供程序

与所有提供程序一样,要允许Keycloak生成CredentialProvider,我们需要一个CredentialProviderFactory。 对于这个要求,我们创建 SecretQuestionCredentialProviderFactory,其创建当要求SecretQuestionCredentialProvider时,将调用方法:

公众  SecretQuestionCredentialProviderFactory 工具凭据提供工厂 <SecretQuestionCredentialProvider> {

    公众 静态 决赛 字符串供应商id =秘密问题;

    @ 覆盖
    公众 字符串getId() {
        返回供应商id;
    }

    @ 覆盖
    公众CredentialProvider create(KeycloakSession会话) {
        返回 新的SecretQuestionCredentialProvider (会话);
    }
}

CredentialProvider接口采用扩展CredentialModel的通用参数。 在我们的例子中,我们使用我们创建的SecretQuestionCredentialModel:

公众  秘密质疑证书提供者 工具凭据提供程序 <SecretQuestionCredentialModel>,凭据inputvalidator {
    私人 静态 决赛 记录器记录器 =记录器。getLogger(SecretQuestionCredentialProvider.class);

    受保护KeycloakSession会话;

    公众SecretQuestionCredentialProvider(KeycloakSession会话) {
        这个。session = session;
    }

    私人用户凭证存储getCredentialStore() {
        返回session.userCredentialManager();
    }

我们还希望实现CredentialInputValidator接口,因为这使Keycloak知道此提供程序也可以用于验证 身份验证器的凭据。 对于CredentialProvider接口,需要实现的第一个方法是getType()方法。 这将简单地 返回 'secretquestioncredentialmodel' 的类型字符串:

@ 覆盖
公众 字符串getType() {
    返回SecretQuestionCredentialModel。类型;
}

第二种方法是创建一个SecretQuestionCredentialModel凭证模型。 对于这种方法,我们简单地调用现有的静态方法 来自秘密质疑证书模型:

@ 覆盖
公众SecretQuestionCredentialModel getCredentialFromModel(CredentialModel) {
    返回SecretQuestionCredentialModel。创建自credentialmodel (模型);
}

最后,我们有创建凭据和删除凭据的方法。 这些方法调用KeycloakSession的用户凭证管理器,其中 负责知道在哪里读取或写入凭据,例如本地存储或联合存储。

@ 覆盖
公众CredentialModel createCredential(RealmModel领域,用户模型用户,SecretQuestionCredentialModel) {
    如果(凭据模型。getCreatedDate() = =null) {
        credentialModel.setCreatedDate (时间。currentTimeMillis());
    }
    返回getCredentialStore().createCredential (领域,用户,credentialModel);
}

@ 覆盖
公众 布尔删除 (RealmModel领域,用户模型用户,字符串凭据id) {
    返回getCredentialStore()。removeStoredCredential (领域,用户,credentialId);
}

对于CredentialInputValidator,要实现的主要方法是是有效的,它测试凭据是否对 给定领域中的给定用户。 这是身份验证器在试图验证用户输入时调用的方法。 在这里,我们 只需检查输入字符串是否记录在凭据中:

@ 覆盖
公众 布尔有效 (真实模型领域、用户模型用户、凭证输入) {
    如果(!(输入实例用户凭证模型)) {
        记录器。de bug (CredentialInput的UserCredentialModel的预期实例);
        返回 ;
    }
    如果(!输入。getType()。等于 (getType() {
        返回 ;
    }
    字符串挑战响应 = 输入。获取挑战响应 ();
    如果(挑战响应 = =null) {
        返回 ;
    }
    CredentialModel credentialModel = getCredentialStore().getStoredCredentialById (领域,用户,输入.getCredentialId());
    SecretQuestionCredentialModel sqcm = getCredentialFromModel(credentialModel);
    返回sqcm.getSecretQuestionSecretData().getAnswer().equals(challengeResponse);
}

要实现的其他两种方法是测试CredentialProvider是否支持给定的credential类型,以及要检查的测试 如果为给定用户配置了凭据类型。 对于我们的情况,后一种测试仅意味着检查用户是否具有凭据 SECRET_QUESTION类型:

@ 覆盖
公众 布尔支持scredentialtype (字符串凭据类型) {
    返回getType().equals(credentialType);
}

@ 覆盖
公众 布尔isConfiguredFor (真实模型领域,用户模型用户,字符串凭据类型) {
    如果(!支持scredentialtype (凭据类型))返回 ;
    返回!getCredentialStore()。getstoredcredentialbytype (领域,用户,credentialType)。isEmpty();
}

实现身份验证器

当实现使用凭据对用户进行身份验证的身份验证器时,您应该让身份验证器实现 CredentialValidator接口。 此接口将扩展CredentialProvider的类作为参数,并将 允许Keycloak直接从CredentialProvider调用方法。 唯一需要实现的方法是 获取证书提供程序方法,在我们的示例中,它允许SecretQuestionAuthenticator检索SecretQuestionCredentialProvider:

公众SecretQuestionCredentialProvider getCredentialProvider(KeycloakSession会话) {
    返回(SecretQuestionCredentialProvider)session.getProvider(CredentialProvider.class,SecretQuestionCredentialProviderFactory.PROVIDER_ID);
}

在实现Authenticator接口时,第一个需要实现的方法是requiresUser() 方法。 对于我们的示例,此方法必须返回true,因为我们需要验证与用户关联的秘密问题。 像kerberos这样的提供者将从该方法返回false,因为它可以从协商标头中解析用户。 但是,此示例正在验证特定用户的特定凭据。

下一个要实现的方法是configuredFor() 方法。 此方法负责确定用户是否为此特定身份验证器配置。 在我们的例子中, 我们可以只调用SecretQuestionCredentialProvider中实现的方法

@ 覆盖
公众 布尔configuredFor(KeycloakSession会话,RealmModel领域,用户模型用户) {
    返回getCredentialProvider (会话).isConfiguredFor (领域,用户,getType (会话));
}

下一个要在身份验证器上实现的方法是setRequiredActions()。 如果configuredFor() 返回false,而我们的示例身份验证器 在流中需要时,将调用此方法,但前提是关联的authentatorfactory的Isusersetupallow方法返回true。 setRequiredActions() 方法负责注册用户必须执行的任何必需操作。 在我们的示例中,我们需要注册一个所需的操作,该操作将迫使用户设置秘密问题的答案。 我们将在本章稍后实施此必需的操作提供者。 这里是setRequiredActions() 方法的实现。

    @ 覆盖
    公众 虚空setRequiredActions(KeycloakSession会话,RealmModel领域,用户模型用户) {
        用户。附加要求操作 (秘密 _ 问题 _ 配置);
    }

现在,我们正在深入了解身份验证器实现的内容。 下一个要实现的方法是authenticate()。 这是流在首次访问执行时调用的初始方法。 我们想要的是,如果用户已经在浏览器的机器上回答了这个秘密问题,那么用户就不会 必须再次回答这个问题,使该机器 “受信任”。 authenticate() 方法不负责处理秘密问题表单。 它的唯一目的是渲染页面或继续流程。

@ 覆盖
公众 虚空身份验证 (身份验证flowcontext上下文) {
    如果(hasCookie (上下文)) {
        上下文。成功 ();
        返回;
    }
    响应挑战 = 上下文。形式 ()
            。创建形式 (secret-question.ftl);
    上下文。挑战 (挑战);
}

受保护 布尔hasCookie(AuthenticationFlowContext上下文) {
    Cookie cookie = 上下文。getthttprequest ()。getthttpheaders ()。getCookies()。get (秘密问题回答);
    布尔结果 = cookie!=null;
    如果(结果) {
        系统。输出。打印 (绕过秘密问题,因为cookie已设置);
    }
    返回结果;
}

hasCookie() 方法检查浏览器上是否已经设置了cookie,这表明秘密问题已经被回答。 如果返回true,我们只需使用AuthenticationFlowContext.SUCCESS () 方法并从authentication() 方法返回,将此执行的状态标记为成功。

如果hasCookie() 方法返回false,我们必须返回一个呈现秘密问题HTML表单的响应。 AuthenticationFlowContext有一个form() 方法,该方法使用构建表单所需的适当基础信息初始化Freemarker页面构建器。 这个页面生成器被称为组织。Keycloak。登录。日志信息提供者。 LoginFormsProvider.createForm() 方法从您的登录主题加载一个Freemarker模板文件。 此外,如果要将其他信息传递给Freemarker模板,可以调用LoginFormsProvider.setAttribute() 方法。 我们稍后再讨论这个。

调用LoginFormsProvider.createForm() 返回一个JAX-RS响应对象。 然后,我们调用AuthenticationFlowContext.challenge() 传递此响应。 这将执行状态设置为挑战,如果需要执行,此JAX-RS响应对象将被发送到浏览器。

因此,向用户显示请求秘密问题答案的HTML页面,用户输入答案并单击提交。 HTML表单的操作URL将向流发送HTTP请求。 流最终将调用我们的身份验证器实现的action() 方法。

@ 覆盖
公众 虚空操作 (AuthenticationFlowContext上下文) {
    布尔已验证 = 验证答案 (上下文);
    如果(!已验证) {
        响应挑战 = 上下文。形式 ()
                。setError (坏秘密)
                。创建形式 (secret-question.ftl);
        上下文。失败挑战 (AuthenticationFlowError.INVALID_CREDENTIALS,挑战);
        返回;
    }
    setCookie (上下文);
    上下文。成功 ();
}

如果答案无效,我们将使用其他错误消息重建HTML表单。 然后,我们调用AuthenticationFlowContext.failureChallenge() 传递值的原因和JAX-RS响应。 failureChallenge() 的工作原理与challenge() 相同,但它也记录了故障,因此可以通过任何攻击检测服务进行分析。

如果验证成功,那么我们设置一个cookie来记住秘密问题已经回答,我们调用AuthenticationFlowContext.success()。

验证本身获取从表单接收的数据,并从SecretQuestionCredentialProvider调用isValid方法。 你会注意到 代码中有一段关于获取凭证Id的内容。 这是因为如果将Keycloak配置为允许多种类型的替代 身份验证器,或者如果用户可以记录SECRET_QUESTION类型的多个凭据 (例如,如果我们允许从多个问题中进行选择, 并且我们允许用户对其中一个以上的问题有答案),然后Keycloak需要知道使用哪个凭据来记录用户。 如果有多个凭据,Keycloak允许用户在登录期间选择正在使用哪个凭据,并且信息由 认证器的表单。 如果表单不显示此信息,则使用的凭据id由凭据提供程序的默认getDefaultCredential方法,它将 返回正确用户类型的 “最首选” 凭证,

受保护 布尔验证答案 (AuthenticationFlowContext上下文) {
    多值映射 <字符串,字符串> formData = 上下文。getthttprequest ()。getdecodededformparameters ();
    字符串secret = formData.getFirst (秘密 _ 回答);
    字符串credentialId = formData.getFirst (凭证id);
    如果(凭证id = =null| | credentialId.isEmpty()) {
        credentialId = getCredentialProvider(context.getSession())
                。getDefaultCredential(context.getSession(),context.getRealm(),context.getUser())。getId();
    }

    用户凭证模型输入 =新的UserCredentialModel(credentialId,getType(context.getSession()),secret);
    返回getCredentialProvider(context.getSession()).isValid(context.getRealm(),context.getUser(),input);
}

下一个方法是setCookie()。 这是为认证器提供配置的示例。 在这种情况下,我们希望cookie的最大年龄是可配置的。

受保护 虚空setCookie(AuthenticationFlowContext上下文) {
    AuthenticatorConfigModel config = context.getAuthenticatorConfig();
    int最大烹饪 =60*60*24*30;// 30天
    如果(配置!=null) {
        最大烹饪 =整数。值 (config.getConfig().get (饼干。最大年龄));

    }
    URIuri = 上下文。getUriInfo()。getBaseUriBuilder()。路径 (领域).path (上下文.getRealm().getName()).build();
    addCookie (上下文,秘密问题回答,,
            uri.getRawPath(),
            null,null,
            maxCookieAge,
            ,);
}

我们从AuthenticationFlowContext.getAuthenticatorConfig() 方法中获取一个AuthenticatorConfigModel。 如果存在配置,我们将从配置中提取最大年龄配置。 当我们谈论AuthenticatorFactory实现时,我们将看到如何定义应该配置的内容。 如果您在AuthenticatorFactory实现中设置了配置定义,则可以在管理控制台中定义配置值。

@ 覆盖
    公众凭证类型数据数据获取凭证类型数据 () {
        返回凭据类型数据。构建器 ()
                。类型 (getType())
                。类别 (凭据类型数据。类别。二因素)
                。显示名称 (SecretQuestionCredentialProviderFactory.PROVIDER_ID)
                。helpText (秘密问题文本)
                。创建操作 (secretquestionauthentatorfactory.PROVIDER_ID)
                。可拆卸 ()
                。构建 (会话);
    }

SecretQuestionCredentialProvider类中的最后一个方法是getcredentialtypemedata (),这是CredentialProvider的抽象方法 接口。 每个凭证提供者都必须提供并实现此方法。 该方法返回一个credentialtypemedata的实例, 其中至少应包括身份验证器,显示名称和可移动项目的类型和类别。 在此示例中,构建器 从方法getType() 中获取身份验证器的类型,类别是两个因素 (身份验证器可以用作身份验证的第二个因素) 和可移动,设置为false (用户无法删除某些先前注册的凭据)。

builder的其他项目是helpText (将在各种屏幕上显示给用户),createAction (所需操作的providerID, 用户可以使用它来创建新的凭据) 或updateAction (与createAction相同,但它不会创建新的凭据,而是会更新凭据)。

实现身份验证工厂

此过程的下一步是实现AuthenticatorFactory。 该工厂负责实例化身份验证器。 它还提供关于身份验证器的部署和配置元数据。

getId() 方法只是组件的唯一名称。 运行时调用create() 方法来分配和处理身份验证器。

公众  Secretquestionation认证工厂 工具认证工厂,配置认证工厂 {

    公众 静态 决赛 字符串供应商id =秘密问题认证器;
    私人 静态 决赛Secretquestionation authenticator SINGLETON =新的SecretQuestionAuthenticator();

    @ 覆盖
    公众 字符串getId() {
        返回供应商id;
    }

    @ 覆盖
    公众 认证器创建 (KeycloakSession会话) {
        返回辛格尔顿;
    }

工厂负责的下一件事是指定允许的需求开关。 虽然有四种不同的需求类型: 替代,必需,条件,禁用,AuthenticatorFactory实现可以限制 定义流时,管理控制台中会显示需求选项。 有条件的只应该总是用于子流,除非有一个好的 否则,对身份验证器的要求应该是必需的,替代的和禁用的:

    私人 静态认证执行模型。要求[]要求 _ 选择 = {
            认证执行模型。要求。必需的,
            认证执行模型。要求。替代方案,
            认证执行模型。要求。已禁用
    };
    @ 覆盖
    公众认证执行模型。要求[]获取需求选择 () {
        返回要求 _ 选择;
    }

AuthenticatorFactory.isUserSetupAllowed() 是一个标志,它告诉流管理器是否将调用Authenticator.Setrequired actions () 方法。 如果未为用户配置身份验证器,则流管理器将检查isusersetupalloid ()。 如果为false,则流中止并出现错误。 如果它返回true,那么流管理器将调用Authenticator.setRequiredActions()。

    @ 覆盖
    公众 布尔Isusersetupaland () {
        返回 ;
    }

接下来的几个方法定义了如何配置身份验证器。 isConfigurable() 方法是一个标志,它向管理控制台指定是否可以在流中配置身份验证器。 getConfigProperties() 方法返回ProviderConfigProperty对象的列表。 这些对象定义了一个特定的配置属性。

    @ 覆盖
    公众 列表<ProviderConfigProperty> getConfigProperties() {
        返回配置属性;
    }

    私人 静态 决赛 列表<ProviderConfigProperty> configProperties =新的 阵列列表<ProviderConfigProperty>();

    静态{
        ProviderConfigProperty属性;
        属性 =新的ProviderConfigProperty();
        属性。setName (饼干。最大年龄);
        属性。setLabel (饼干最大年龄);
        属性。设置类型 (ProviderConfigProperty.STRING_TYPE);
        属性。setHelpText (秘密问题cookie的最大年龄 (以秒为单位)。);
        configProperties.add (属性);
    }

每个ProviderConfigProperty定义config属性的名称。 这是存储在AuthenticatorConfigModel中的配置映射中使用的密钥。 标签定义了config选项将如何在管理控制台中显示。 类型定义它是字符串、布尔值还是其他类型。 管理控制台将根据类型显示不同的UI输入。 帮助文本将显示在管理控制台中config属性的工具提示中。 有关更多详细信息,请阅读ProviderConfigProperty的javadoc。

其余方法用于管理控制台。 getHelpText() 是在选择要绑定到执行的身份验证器时将显示的工具提示文本。 getDisplayType() 是列出身份验证器时将在管理控制台中显示的文本。 getReferenceCategory() 只是认证者所属的一个类别。

添加验证器表单

Keycloak带有Freemarker主题和模板引擎。 您在Authenticator类的authenticate() 中调用的createForm() 方法,从登录主题中的文件构建HTML页面:secret-question.ftl。 此文件应添加到主题-资源/模板在你的罐子里,看主题资源提供商有关更多详细信息。

让我们来看看secret-question.ftl这里有一个小的代码片段:

        <表格id =kc-totp-登录-表单 =$ {属性。kcFormClass!} 行动=${url.日志不作为}方法 =邮政>
            <div=$ {属性。kcFormGroupClass!}>
                <分部 =$ {属性。kcLabelWrapperClass!}>
                    <标签 对于=totp =$ {属性。kcLabelClass!}>${味精 (loginSecretQuestion)}</标签>
                </div>

                <div=$ {属性。kcinputrapperclass!}>
                    <输入id =totp名称 =秘密 _ 回答类型 =文本 =$ {属性。kcInputClass!} />
                </分部>
            </div>
        </form>

附上的任何文本${}对应于属性或模板功能。 如果你看到表单的动作,你会看到它指向${url.日志不作为}。 当您调用AuthenticationFlowContext.form() 方法时,会自动生成此值。 您也可以通过在Java代码中调用AuthenticationFlowContext.getActionURL() 方法来获取此值。

你也会看到$ {属性。someValue}。 这些对应于主题中定义的属性。我们主题的属性文件。 $ {味精 (“someValue”)}对应于登录主题消息/目录中包含的国际化消息包 (.properties文件)。 如果您只是使用英语,则可以添加loginSecretQuestion。 这应该是你想问用户的问题。

当您调用AuthenticationFlowContext.form() 时,这为您提供了一个LoginFormsProvider实例。 如果你打电话来,Loginformssprovider.setAttribute (“foo”,“bar”),“foo” 的值将以您的形式提供参考${foo}。 属性的值也可以是任何Java bean。

如果您查看文件的顶部,您会看到我们正在导入一个模板:

<#导入 选择。ftl 作为 布局>

导入此模板,而不是标准的模板。ftl允许Keycloak显示一个下拉框,允许用户选择 不同的凭证或执行。

将身份验证器添加到流中

必须在管理控制台中向流添加身份验证器。 如果转到身份验证菜单项并转到流选项卡,则可以查看当前定义的流。 您不能修改内置流,因此,要添加我们创建的身份验证器,您必须复制现有流或创建自己的流。 我们希望用户界面足够清晰,以便您可以确定如何创建流程并添加身份验证器。 对于 更多详细信息,请参见身份验证流程中的章节服务器管理指南

创建流程后,必须将其绑定到要绑定的登录操作。 如果转到 “身份验证” 菜单并转到 “绑定” 选项卡,您将看到将流绑定到浏览器、注册或直接授予流的选项。

所需的动作演练

在本节中,我们将讨论如何定义所需的操作。 在身份验证器部分,你可能想知道,“我们将如何获得用户对输入系统的秘密问题的答案?”。 如我们在示例中所示,如果未设置答案,则将触发所需的操作。 本节讨论如何实现秘密问题验证器所需的操作。

打包类和部署

您将把您的类打包在一个罐子里。 此jar不必与其他提供程序类分开,但它必须包含一个名为组织。密钥斗篷。身份验证。要求操作工厂并且必须包含在元信息/服务/您的jar的目录。 此文件必须列出您在jar中拥有的每个RequiredActionFactory实现的完全限定的类名。 例如:

org.keycloak.examples.authenticator.Secretisquestionedactionfactory

Keycloak使用此服务/文件扫描必须加载到系统中的提供程序。

要部署此jar,只需将其复制到独立/部署目录。

实现RequiredActionProvider

所需的操作必须首先实现RequiredActionProvider接口。 RequiredActionProvider.requiredActionChallenge() 是流管理器对所需操作的初始调用。 此方法负责呈现将驱动所需动作的HTML表单。

    @ 覆盖
    公众 虚空要求操作挑战 (要求操作上下文上下文) {
        响应挑战 = 上下文。形式 ()。创建形式 (秘密 _ 问题 _ 配置.Ftl);
        上下文。挑战 (挑战);

    }

您看到RequiredActionContext具有与AuthenticationFlowContext相似的方法。 form() 方法允许您从Freemarker模板呈现页面。 操作URL是通过调用this form() 方法预设的。 你只需要在你的HTML表单中引用它。 我过会儿给你看这个。

challenge() 方法通知流管理器必须执行所需的操作。

下一个方法负责处理来自所需操作的HTML表单的输入。 表单的操作URL将被路由到RequiredActionProvider.processAction() 方法

    @ 覆盖
    公众 虚空processAction (要求操作上下文上下文) {
        字符串答案 = (上下文。getthttprequest ()。getDecodedFormParameters()。getFirst (答案));
        用户凭证值模型 =新的UserCredentialValueModel();
        model.setValue (答案);
        model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);
        上下文。getUser()。直接更新 (模型);
        上下文。成功 ();
    }

答案从表格帖子中拉出。 创建UserCredentialValueModel,并设置凭据的类型和值。 然后调用usermmodel.Updatecredentential直接 ()。 最后,RequiredActionContext.success() 通知容器所需的操作成功。

实现RequiredActionFactory

这堂课真的很简单。 它只负责创建所需的操作提供程序实例。

公众  秘密要求操作工厂 工具要求操作工厂 {

    私人 静态 决赛Secretquestiones要求动作单例 =新的Secretquestiones requiredaction ();

    @ 覆盖
    公众RequiredActionProvider create(KeycloakSession会话) {
        返回辛格尔顿;
    }


    @ 覆盖
    公众 字符串getId() {
        返回Secretquestionquiredaction.PROVIDER_ID;
    }

    @ 覆盖
    公众 字符串getDisplayText() {
        返回 秘密问题;
    }

getDisplayText() 方法仅用于管理控制台,当它想要显示所需操作的友好名称时。

启用所需的操作

您要做的最后一件事是进入管理控制台。 单击左侧身份验证菜单。 单击所需的操作选项卡。 单击 “注册” 按钮,然后选择所需的新操作。 您的新的必需操作现在应该显示并在必需操作列表中启用。

修改/扩展注册表

您完全有可能使用一组身份验证器来实现自己的流程,以完全更改在Keycloak中进行注册的方式。 但是,您通常要做的只是在开箱即用的注册页面上添加一点验证。 创建了一个额外的SPI来执行此操作。 它基本上允许您在页面上添加表单元素的验证,以及在用户注册后初始化UserModel属性和数据。 我们将研究用户配置文件注册处理的实现以及注册Google Recaptcha插件。

实现FormAction接口

您必须实现的核心接口是FormAction接口。 FormAction负责渲染和处理页面的一部分。 渲染在buildPage() 方法中完成,验证在validate() 方法中完成,后期验证操作在success() 中完成。 让我们先看看Recaptcha插件的buildPage() 方法。

    @ 覆盖
    公众 虚空构建页面 (FormContext上下文,loginformssprovider表单) {
        AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
        如果(验证码 = =null| | 验证码。getConfig() = =null
                | | captchaConfig.getConfig().get(SITE_KEY) = =null
                | | captchaConfig.getConfig().get(SITE_SECRET) = =null
                ) {
            表格。addError (新的Formessage (null,消息。RECAPTCHA_NOT_CONFIGURED));
            返回;
        }
        字符串siteKey = captchaConfig.getConfig().get(SITE_KEY);
        表单。setAttribute (被夺回,);
        表单。setAttribute (夺回钥匙,siteKey);
        表单。添加脚本 (https://www.google.com/recaptcha/api.js);
    }

Recaptcha buildPage() 方法是表单流的回调,以帮助呈现页面。 它接收一个表单参数,它是一个LoginFormsProvider。 您可以向表单提供程序添加其他属性,以便它们可以显示在注册Freemarker模板生成的HTML页面中。

上面的代码来自registration recaptcha插件。 Recaptcha需要一些特定的设置,这些设置必须从配置中获得。 FormActions的配置与身份验证器的配置完全相同。 在此示例中,我们从配置中提取Google Recaptcha站点密钥,并将其作为属性添加到表单提供程序。 我们的注册模板文件现在可以读取此属性。

Recaptcha还具有加载JavaScript脚本的要求。 您可以通过在URL中调用LoginFormsProvider.addScript() 来做到这一点。

对于用户配置文件处理,没有其他信息需要添加到表单中,因此其buildPage() 方法为空。

这个接口的下一个重要部分是validate() 方法。 收到表格后立即调用。 让我们先看看Recaptcha的插件。

    @ 覆盖
    公众 虚空验证 (验证上下文上下文) {
        多值映射 <字符串,字符串> formData = 上下文。getthttprequest ()。getdecodededformparameters ();
        列表<格式消息> 错误 =新的 阵列列表<>();
        布尔成功 =;

        字符串验证码 = formData.getFirst(G_RECAPTCHA_RESPONSE);
        如果(!验证。isBlank (验证码)) {
            AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
            字符串secret = captchaConfig.getConfig().get(SITE_SECRET);

            success = validateRecaptcha (上下文,成功,验证码,秘密);
        }
        如果(成功) {
            上下文。成功 ();
        }其他{
            错误。添加 (新的Formessage (null,消息。RECAPTCHA_FAILED));
            formData.remove(G_RECAPTCHA_RESPONSE);
            上下文。验证错误 (formData,错误);
            返回;


        }
    }

在这里,我们获取Recaptcha小部件添加到表单中的表单数据。 我们从配置中获取Recaptcha密钥。 然后,我们验证recaptcha。 如果成功,则调用ValidationContext.success()。 如果没有,我们调用ValidationContext.validationError() 传入formData (这样用户就不必重新输入数据),我们还指定了一个我们想要显示的错误消息。 错误消息必须指向国际化消息捆绑包中的消息捆绑包属性。 对于其他注册扩展名,validate() 可能正在验证表单元素的格式,即 另一种电子邮件属性。

让我们还看一下注册时用于验证电子邮件地址和其他用户信息的用户配置文件插件。

    @ 覆盖
    公众 虚空验证 (验证上下文上下文) {
        多值映射 <字符串,字符串> formData = 上下文。getthttprequest ()。getdecodededformparameters ();
        列表<格式消息> 错误 =新的 阵列列表<>();

        字符串eventError = 错误。无效注册;

        如果(验证.isBlank(formData.getFirst((RegistrationPage.FIELD_FIRST_NAME) {
            错误。添加 (新的Formessage (RegistrationPage.FIELD_FIRST_NAME,Messages.MISSING_FIRST_NAME));
        }

        如果(验证.isBlank(formData.getFirst((RegistrationPage.FIELD_LAST_NAME) {
            错误。添加 (新的Formessage (RegistrationPage.FIELD_LAST_NAME,Messages.MISSING_LAST_NAME));
        }

        字符串email = formData.getFirst (验证.FIELD_EMAIL);
        如果(验证。isBlank (电子邮件)) {
            错误。添加 (新的Formessage (RegistrationPage.FIELD_EMAIL,message.MISSING_EMAIL));
        }其他 如果(!验证。isEmailValid (电子邮件)) {
            formData。删除 (验证.FIELD_EMAIL);
            错误。添加 (新的Formessage (RegistrationPage.FIELD_EMAIL,Messages.INVALID_EMAIL));
        }

        如果(context.getSession().users().getUserByEmail(email,context.getRealm()) !=null) {
            formData。删除 (验证.FIELD_EMAIL);
            错误。添加 (新的Formessage (RegistrationPage.FIELD_EMAIL,message.EMAIL_EXISTS));
        }

        如果(错误。大小 () >0) {
            上下文。验证错误 (formData,错误);
            返回;

        }其他{
            上下文。成功 ();
        }
    }

如您所见,此用户配置文件处理的validate() 方法可确保在表单中填写电子邮件,名字和姓氏。 它还可以确保电子邮件格式正确。 如果这些验证中的任何一个失败,则会排队等待呈现错误消息。 任何错误的字段都将从表单数据中删除。 错误消息由formessage类表示。 该类的构造函数的第一个参数采用HTML元素id。 重新呈现表单时,错误的输入将突出显示。 第二个参数是消息引用id。 此id必须对应于本地化消息捆绑文件之一中的属性。 在主题中。

在处理完所有验证之后,表单流将调用FormAction.success() 方法。 对于recaptcha来说,这是禁止操作的,所以我们不会再讨论了。 对于用户配置文件处理,此方法填写注册用户中的值。

    @ 覆盖
    公众 虚空成功 (FormContext上下文) {
        UserModel user = context.getUser();
        多值映射 <字符串,字符串> formData = 上下文。getthttprequest ()。getdecodededformparameters ();
        user.setFirstName(formData.getFirst(RegistrationPage.FIELD_FIRST_NAME));
        user.setLastName(formData.getFirst(RegistrationPage.FIELD_LAST_NAME));
        user.setEmail(formData.getFirst(RegistrationPage.FIELD_EMAIL));
    }

非常简单的实现。 从FormContext获得新注册用户的用户模型。 调用适当的方法来初始化UserModel数据。

最后,还需要定义FormActionFactory类。 这个类的实现类似于AuthenticatorFactory,所以我们不会再讨论它了。

包装动作

您将把您的类打包在一个罐子里。 这个jar必须包含一个名为组织。Keycloak。身份验证。FormActionFactory并且必须包含在元信息/服务/您的jar的目录。 此文件必须列出您在jar中拥有的每个FormActionFactory实现的完全限定的类名。 例如:

组织。密钥斗篷。身份验证。表单。注册配置文件
组织。Keycloak。身份验证。表单。注册recaptcha

Keycloak使用此服务/文件扫描必须加载到系统中的提供程序。

要部署此jar,只需将其复制到独立/部署目录。

将FormAction添加到注册流程

必须在管理控制台中向注册页面流添加FormAction。 如果转到身份验证菜单项并转到流选项卡,则可以查看当前定义的流。 您不能修改内置流,因此,要添加我们创建的身份验证器,您必须复制现有流或创建自己的流。 我希望UI足够直观,以便您可以自己弄清楚如何创建流程并添加模板。

基本上你必须复制注册流程。 然后单击注册表右侧的 “操作” 菜单,然后选择 “添加执行” 以添加新的执行。 您将从选择列表中选择FormAction。 如果在 “注册用户创建” 之后尚未列出您的FormAction,请使用向下按钮将其移动,以确保您的FormAction出现在 “注册用户创建” 之后。 您希望您的FormAction在用户创建之后出现,因为注册用户创建的success() 方法负责创建新的用户模型。

创建流程后,必须将其绑定到注册。 如果转到 “身份验证” 菜单并转到 “绑定” 选项卡,您将看到将流绑定到浏览器、注册或直接授予流的选项。

修改忘记密码/凭证流

Keycloak还具有特定的身份验证流程,用于忘记密码,或者由用户发起的凭据重置。 如果转到 “管理控制台流程” 页面,则会出现 “重置凭据” 流程。 默认情况下,Keycloak询问用户的电子邮件或用户名,并向他们发送电子邮件。 如果用户单击链接,则他们可以重置密码和OTP (如果已设置OTP)。 您可以通过禁用流程中的 “重置OTP” 身份验证器来禁用自动OTP重置。

您也可以在此流中添加其他功能。 例如,除了发送带有链接的电子邮件之外,许多部署都希望用户回答一个或多个秘密问题。 您可以扩展发行版附带的秘密问题示例,并将其合并到重置凭证流中。

如果要扩展重置凭据流,需要注意的一件事。 第一个 “认证器” 只是一个页面来获取用户名或电子邮件。 如果用户名或电子邮件存在,则AuthenticationFlowContext.getUser() 将返回定位的用户。 否则这将是null。 这张表格不会如果以前的电子邮件或用户名不存在,请重新要求用户输入电子邮件或用户名。 您需要防止攻击者能够猜测有效用户。 因此,如果AuthenticationFlowContext.getUser() 返回null,则应继续进行流程,以使其看起来像选择了有效用户。 我建议,如果您想在此流中添加秘密问题,则应在电子邮件发送后提出这些问题。 换句话说,在 “发送重置电子邮件” 身份验证器之后添加您的自定义身份验证器。

修改第一个代理登录流程

在首次登录期间使用某些身份提供者使用第一代理登录流程。 术语首次登录表示尚未存在与特定经过身份验证的身份提供者帐户链接的Keycloak帐户。 有关此流程的更多详细信息,请参见身份经纪中的章节服务器管理指南

客户端的身份验证

Keycloak实际上支持可插拔身份验证OpenID连接客户端应用程序。 在发送任何反向通道请求时,Keycloak适配器会在后台使用客户端 (应用程序) 的身份验证 到Keycloak服务器 (如成功身份验证后请求交换代码访问令牌或请求刷新令牌)。 但是客户端身份验证也可以直接由您在直接访问赠款(由OAuth2表示资源所有者密码凭据流) 或者在服务账户身份验证 (由OAuth2表示客户端凭证流)。

有关Keycloak适配器和OAuth2流的更多详细信息,请参见保护应用程序和服务指南

默认实现

实际上Keycloak有2个客户端身份验证的默认实现:

使用client_id和client_secret的传统身份验证

这是默认机制中提到的OpenID连接或者OAuth2规格和Keycloak支持它,因为它是早期的。 公共客户需要包括客户id参数,其ID在POST请求中 (因此它实际上没有经过身份验证),机密客户端需要包括授权: 基本标题,其中clientId和clientSecret用作用户名和密码。

使用签名的JWT进行身份验证

这是基于OAuth 2.0的JWT承载令牌配置文件规格。 客户端/适配器生成JWT用他的私钥签名。 然后,Keycloak使用客户端的公钥验证已签名的JWT,并基于它对客户端进行身份验证。

请参见演示示例,尤其是示例/预配置-演示/产品-应用对于示例应用程序显示 使用带有签名JWT的客户端身份验证的应用程序。

实现您自己的客户端身份验证器

对于插入您自己的客户端身份验证器,您需要在客户端 (适配器) 和服务器端实现几个接口。

客户端

在这里你需要实现org.keycloak.适配器.身份验证.客户端凭据提供程序并把实现要么:

  • 你的战争文件进入WEB-INF/类。 但是在这种情况下,该实现可以仅用于此单个WAR应用程序

  • 一些JAR文件,它将被添加到你的战争的WEB-INF/lib

  • 一些JAR文件,它将用作jboss模块,并在您的战争jboss-deployment-structure.xml配置。 在所有情况下,您还需要创建文件META-INF/services/org.keycloak.适配器.身份验证.ClientCredentialsProvider要么在战争中,要么在你的罐子里。

服务器端

在这里你需要实现组织。Keycloak。身份验证。客户认证工厂组织。Keycloak。身份验证。客户认证。 您还需要添加文件META-INF/services/org.keycloak.身份验证.Clientaiticatorfactory与实现类的名称。 见认证器有关更多详细信息。

动作令牌SPI

操作令牌是Json Web令牌 (JWT) 的特殊实例,它允许其承载执行某些操作,例如。 重置密码或验证电子邮件地址。 它们通常以指向端点的链接形式发送给用户 处理特定领域的动作令牌。

Keycloak提供了四种基本令牌类型,允许承载:

  • 重置凭据

  • 确认电子邮件地址

  • 执行所需的操作

  • 确认帐户与外部身份提供商中的帐户的链接

除此之外,还可以实现启动或修改身份验证会话的任何功能 使用操作令牌SPI,其详细信息将在下面的文本中描述。

动作令牌的解剖

Action token是用active realm key签名的标准Json Web Token,其中有效负载包含多个字段:

  • 典型-识别行动 (例如验证-电子邮件)

  • iat经验-令牌有效期的时间

  • 潜艇-用户的ID

  • azp-客户名称

  • 国际空间站-发行者-发行领域的URL

  • 澳元-包含发行领域URL的受众列表

  • asid-身份验证会话的ID (可选)

  • 随机性-随机随机数,以保证在操作只能执行一次的情况下使用的唯一性 (可选)

此外,操作令牌可以包含可序列化为JSON的任意数量的自定义字段。

动作令牌处理

当将操作令牌传递到Keycloak端点时 KEYCLOAK_ROOT/身份验证/领域/主/登录-操作/操作-令牌通过钥匙参数,它被验证和正确的操作 令牌处理程序被执行。 处理总是在身份验证会话的上下文中进行,要么是新鲜的 一个或操作令牌服务加入现有的身份验证会话 (详细信息如下所述)。 动作令牌 处理程序可以执行令牌规定的操作 (通常它会更改身份验证会话),并将结果转换为HTTP 响应 (例如,它可以继续进行身份验证或显示信息/错误页面)。 下面详细介绍了这些步骤。

  1. 基本操作令牌验证。 检查签名和时间有效性,并根据 上典型场。

  2. 确定身份验证会话。 如果操作令牌URL是在浏览器中打开的 现有的身份验证会话,并且令牌包含与身份验证会话匹配的身份验证会话ID 从浏览器中,操作令牌验证和处理将附加此正在进行的身份验证会话。 否则, 操作令牌处理程序创建一个新的身份验证会话,该会话替换存在于 那个时候在浏览器里。

  3. 特定于令牌类型的令牌验证。 操作令牌端点逻辑验证用户 (潜艇字段) 和 客户 (azp) 从令牌中存在,有效且未禁用。 然后它验证中定义的所有自定义验证 操作令牌处理程序。 此外,令牌处理程序可以请求此令牌是一次性使用的。 已经使用的令牌将是 被操作令牌端点逻辑拒绝。

  4. 执行操作。 在所有这些验证之后,调用执行实际 根据令牌中的参数进行操作。

  5. 一次性使用令牌的失效。 如果令牌设置为一次性使用,一旦身份验证流程完成, 动作令牌无效。

实现您自己的动作令牌及其处理程序

如何创建操作令牌

因为操作令牌只是一个签名的JWT,几乎没有强制性字段 (请参阅动作令牌的解剖 上面),它可以使用Keycloak的序列化和签名JWSBuilder类。 这种方式已经 实施于序列化 (session,realm,urifo)方法组织。密钥斗篷。身份验证。动作令牌。默认操作令牌 并且可以由实现者通过使用该类作为令牌而不是普通JsonWebToken

下面的示例显示了一个简单的操作令牌的实现。 请注意,该类必须具有不带任何参数的私有构造函数。 这是从JWT反序列化令牌类所必需的。

导入 组织。密钥斗篷。身份验证。动作令牌。默认操作令牌;

公众  DemoActionToken 延伸默认操作令牌 {

    公众 静态 决赛 字符串令牌类型 =我的演示令牌;

    公众DemoActionToken (字符串用户id,int绝对呼气量,字符串化合物认证会话id) {
        超级(userId,TOKEN_TYPE,absolutexpirationinsecs,null,comoundauthenticationsessionid);
    }

    私人DemoActionToken() {
        // 需要从JWT进行反序列化
        超级();
    }
}

如果您正在实现的操作令牌包含应序列化为JSON字段的任何自定义字段,则您 应该考虑实施组织。Keycloak。代表。JsonWebToken将实现的类 Org.Keycloak.mo dels.ActionTokenKeyModel接口。 在这种情况下,您可以利用现有的 组织。密钥斗篷。身份验证。动作令牌。默认操作令牌类,因为它已经满足这两个条件, 并直接使用它或实现它的子级,其字段可以用适当的Jackson注释 注释,例如com.fasterxml.jackson.注释.JsonProperty将它们序列化为JSON。

下面的示例扩展了DemoActionToken从上一个带有字段的示例中演示-id:

导入 com.fasterxml.jackson.注释.JsonProperty;
导入 组织。密钥斗篷。身份验证。动作令牌。默认操作令牌;

公众  DemoActionToken 延伸默认操作令牌 {

    公众 静态 决赛 字符串令牌类型 =我的演示令牌;

    私人 静态 决赛 字符串JSON_FIELD_DEMO_ID =演示-id;

    @ JsonProperty(值 = JSON_FIELD_DEMO_ID)
    私人 字符串演示;

    公众DemoActionToken (字符串用户id,int绝对呼气量,字符串化合物认证会话id,字符串演示id) {
        超级(userId,TOKEN_TYPE,absolutexpirationinsecs,null,comoundauthenticationsessionid);
        这个。demoId = demoId;
    }

    私人DemoActionToken() {
        // 您必须具有反序列化器的此私有构造函数
    }

    公众 字符串getDemoId() {
        返回演示;
    }
}

打包类和部署

要插入自己的操作令牌及其处理程序,您需要在服务器端实现几个接口:

  • 组织。密钥斗篷。身份验证。动作令牌处理程序-特定操作令牌的实际处理程序 动作 (即给定值为典型令牌字段)。

    该接口中的中心方法是handleToken (令牌,上下文)它定义了在以下情况下执行的实际操作 接收操作令牌。 通常是对身份验证会话注释的一些更改,但通常可以 武断。 仅当所有验证器 (包括在getVerifiers (上下文)) 有 成功了,而且保证了令牌会是由getTokenClass()方法。

    为了能够确定是否为当前身份验证会话发出了操作令牌,如中所述 上文第2项,提取身份验证会话ID的方法必须在 getAuthenticationSessionIdFromToken (令牌,上下文)方法。 中的实施默认操作令牌返回 的价值asid来自令牌的字段 (如果已定义)。 请注意,您可以覆盖该方法以返回当前 身份验证会话ID,而不考虑令牌-这样您就可以创建令牌,这些令牌将进入正在进行的 在启动任何身份验证流程之前的身份验证流程。

    如果令牌的身份验证会话与当前会话不匹配,则将要求操作令牌处理程序 打电话开始一个新的startFreshAuthenticationSession (令牌,上下文)。 它可以扔一个验证异常 (或者更好的是它更具描述性的变体解释tokenverificationexception) 发出禁止的信号。

    令牌处理程序还通过方法确定Canusetoken反复 (令牌,上下文)令牌是否会是 使用并完成身份验证后无效。 请注意,如果你有一个使用多个动作的流 令牌,只有最后一个令牌才会失效。 在这种情况下,你应该使用 Org.Keycloak.mo dels.ActionTokenStoreProvider在操作令牌处理程序中手动使使用的令牌无效。

    大多数的默认实现动作标记处理程序方法是 组织。Keycloak。身份验证。动作令牌。抽象动作中的抽象类Keycloak-服务模块。 的 唯一需要实现的方法是handleToken (令牌,上下文)执行实际操作。

  • 组织。密钥斗篷。身份验证。动作令牌-实例化操作令牌的工厂 处理程序。 实现必须覆盖getId()返回必须精确匹配的值典型 操作令牌中的字段。

    请注意,您必须注册自定义动作标记器工厂实施,如 服务提供商接口本指南的部分。

事件监听器SPI

编写事件侦听器提供程序首先实现事件列表提供程序事件列表提供工厂接口。 请参阅Javadoc 以及有关如何执行此操作的完整详细信息的示例。

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

SAML角色映射SPI

Keycloak定义了一个SPI,用于将SAML角色映射为SP环境中存在的角色。 返回的角色 第三方IDP可能并不总是与为SP应用程序定义的角色相对应,因此需要 允许将SAML角色映射到不同角色的机制。 SAML适配器在提取角色后使用它 从SAML断言来设置容器的安全上下文。

组织。Keycloak。适配器。saml。角色映射提供程序SPI不会对可以执行的映射施加任何限制。 实现不仅可以将角色映射到其他角色中,还可以添加或删除角色 (从而增加或减少 分配给SAML主体的角色) 取决于用例。

有关SAML适配器的角色映射提供程序配置的详细信息以及默认值的说明 可用的实现请参见保护应用程序和服务指南

实现自定义角色映射提供程序

要实现自定义角色映射提供程序,首先需要实现组织。Keycloak。适配器。saml。角色映射提供程序 接口。 然后,一个元信息/服务/组织。Keycloak。适配器。saml。角色映射提供程序包含完全限定名称的文件 的自定义实现必须添加到也包含实现类的存档中。 此存档可以是:

  • WEB-INF/classes中包含provider类的SP应用程序WAR文件;

  • 一个自定义的JAR文件,它将被添加到SP应用程序WAR的WEB-INF/lib中;

  • (仅适用于WildFly/JBoss EAP) 配置为jboss模块并在jboss-deployment-structure.xml SP应用战争的一部分。

部署SP应用程序时,将使用的角色映射提供程序将通过在 keycloak-saml.xml或者在Keycloak-saml子系统。 因此,要启用您的自定义提供商,只需确保其id为 在适配器配置中正确设置。

用户存储SPI

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

当Keycloak运行时需要查找用户时,例如当用户登录时,它会执行多个步骤来定位用户。 它首先查看用户是否在用户缓存中; 如果找到用户,它将使用该内存表示。 然后它在Keycloak本地数据库中查找用户。 如果找不到用户,则它会循环通过用户存储SPI提供程序实现来执行用户查询,直到其中一个返回运行时正在寻找的用户。 提供者向外部用户存储库查询用户,并将用户的外部数据表示形式映射到Keycloak的用户元模型。

用户存储SPI提供程序实现还可以执行复杂的标准查询,对用户执行CRUD操作,验证和管理凭据,或者一次执行许多用户的批量更新。 这取决于外部商店的功能。

用户存储SPI提供程序实现的打包和部署类似于 (并且通常是) Java EE组件。 默认情况下,它们不是启用的,而是必须在用户联合管理控制台中的选项卡。

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

提供程序接口

在构建用户存储SPI的实现时,您必须定义提供程序类和提供程序工厂。 提供者类实例由提供者工厂为每个事务创建。 提供者类完成用户查找和其他用户操作的所有繁重工作。 他们必须实施 组织。密钥斗篷。存储。用户存储提供程序接口。

包装 组织。Keycloak。存储;

公众 接口 用户存储提供程序 延伸 提供者{


    /**
     * 删除领域时的回调。  例如,如果你想做一些
     * 删除领域时清理用户存储
     *
     * @ param领域
     */
    默认
    虚空preRemove(RealmModel领域) {

    }

    /**
     * 删除组时的回调。  允许您执行诸如删除用户之类的操作
     * 如果合适,在外部商店中进行分组映射
     *
     * @ param领域
     * @ param组
     */
    默认
    虚空preRemove(RealmModel领域,GroupModel组) {

    }

    /**
     * 删除角色时的回调。  允许您执行诸如删除用户之类的操作
     * 外部商店中的角色映射 (如果合适)

     * @ param领域
     * @ param角色
     */
    默认
    虚空preRemove(RealmModel领域,RoleModel角色) {

    }

}

你可能会想用户存储提供程序界面很稀疏? 您将在本章后面看到,您的提供者类可能会实现其他混合接口,以支持用户集成。

用户存储提供程序每个事务创建一次实例。 当交易完成时,用户存储提供程序。关闭 ()方法被调用,然后实例被垃圾收集。 实例由提供商工厂创建。 提供商工厂实施组织。Keycloak。存储。用户存储提供工厂接口。

包装 组织。Keycloak。存储;

/**
 * @ 作者 <a href = "mailto:bill@burkecentral.com"> 比尔·伯克 </a>
 * @ 版本 $ 修订版: 1 $
 */
公众 接口 用户存储提供工厂<T延伸用户存储提供程序>延伸组件工厂 <T,用户存储提供程序> {

    /**
     * 这是提供程序的名称,将作为选项显示在管理控制台中。
     *
     * @ 返回
     */
    @ 覆盖
    字符串getId();

    /**
     * 按Keycloak交易调用。
     *
     * @ param会话
     * @ 参数模型
     * @ 返回
     */
    创建 (KeycloakSession会话,组件模型);
...
}

Provider factory类在实现时必须将具体的provider类指定为模板参数 用户存储提供工厂。 这是必须的,因为运行时会内省这个类来扫描它的功能 (它实现的其他接口)。 例如,如果您的提供者类被命名为文件提供程序,那么 工厂类应该是这样的:

公众  文件提供工厂 工具用户存储提供工厂 <文件提供程序> {

    公众 字符串getId() {返回 文件提供者;}

    公众文件提供程序创建 (KeycloakSession会话,组件模型) {
       ...
    }

getId()方法返回用户存储提供程序的名称。 此id将显示在管理控制台的 当您想要为特定领域启用提供程序时,用户联合联盟页面。

创建 ()方法负责分配提供者类的实例。 这需要一个Org.Keycloak.mo dels.KeycloakSession 参数。 此对象可用于查找其他信息和元数据,并提供对各种其他 运行时内的组件。 的组件模型参数表示提供程序是如何在其中启用和配置的 一个特定的领域。 它包含已启用的提供程序的实例id以及您可能已指定的任何配置 当您通过管理控制台启用它时。

用户存储提供工厂还有其他功能,我们将在本章后面介绍。

提供商能力接口

如果你检查过用户存储提供程序您可能会注意到它没有定义任何用于定位或管理用户的方法。 这些方法实际上是在其他能力接口取决于您的外部用户存储可以提供和执行的功能范围。 例如,一些外部存储是只读的,只能进行简单的查询和凭证验证。 您将只需要实现能力接口对于您能够提供的功能。 您可以实现以下接口:

SPI 描述

组织。密钥斗篷。存储。用户查找提供者

如果您希望能够从此外部商店与用户一起登录,则需要此界面。 大多数 (全部?) 提供程序实现此接口。

组织。密钥斗篷。存储。用户查询提供程序

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

组织。密钥斗篷。存储。用户注册提供程序

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

组织。密钥斗篷。存储。用户bulkupdateprovider

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

组织。密钥斗篷。凭证输入验证器

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

org.keycloak.凭据.CredentialInputUpdater

如果您的提供商支持更新一个或多个不同的凭据类型,则实现此接口。

模型接口

中定义的大多数方法能力 接口返回或以用户的表示形式传递。 这些表示由Org.Keycloak.mo dels.用户模型接口。 应用开发者需要实现这个接口。 它提供了外部用户存储和Keycloak使用的用户元模型之间的映射。

包装 Org.Keycloak.mo dels;

公众 接口 用户模型 延伸角色映射模型 {
    字符串getId();

    字符串getUsername();
    虚空设置用户名 (字符串用户名);

    字符串getFirstName();
    虚空设置名字 (字符串名字);

    字符串getLastName();
    虚空setLastName (字符串姓氏);

    字符串getEmail();
    虚空setEmail (字符串电子邮件);
...
}

用户模型实现提供了读取和更新有关用户的元数据的访问权限,包括用户名,名称,电子邮件,角色和组映射以及其他任意属性。

中还有其他模型类Org.Keycloak.mo dels代表Keycloak元模型其他部分的包:真实模型,角色模型,组模型,以及客户模型

存储id

一种重要的方法用户模型getId()方法。 实施时用户模型开发人员必须知道用户id格式。 格式必须是:

"f:" + 组件id + ":" + 外部id

Keycloak运行时通常必须根据用户的用户id查找用户。 用户id包含足够的信息,因此运行时不必查询每个用户存储提供程序在系统中查找用户。

组件id是从组件模型.getId()。 的组件模型在创建提供程序类时作为参数传入,这样你就可以从那里得到它。 外部id是您的提供者类需要在外部存储中查找用户的信息。 这通常是用户名或uid。 例如,它可能看起来像这样:

f:332a234e31234:wburke

当运行时按id进行查找时,会解析该id以获取组件id。 组件id用于定位用户存储提供程序最初用于加载用户。 然后将该提供者传递id。 提供者再次解析id以获取外部id,它将用于在外部用户存储中定位用户。

包装和部署

用户存储提供程序打包在JAR中,并以与在WildFly应用程序服务器中部署某些内容相同的方式部署或未部署到Keycloak运行时。 您可以将JAR直接复制到独立/部署/服务器的目录,或者使用JBoss CLI执行部署。

为了让Keycloak识别提供程序,您需要在JAR中添加一个文件:元信息/服务/组织。Keycloak。存储。用户存储提供工厂。 此文件必须包含一行分隔的完全限定的类名列表用户存储提供工厂实现方式:

org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory

Keycloak支持这些提供商jar的热部署。 您还将在本章后面看到,您可以将其打包在Java EE组件中并作为Java EE组件。

简单的只读,查找示例

为了说明实现用户存储SPI的基础知识,让我们来介绍一个简单的示例。 在本章中,您将看到一个简单的用户存储提供程序在一个简单的属性文件中查找用户。 属性文件包含用户名和密码定义,并硬编码到类路径上的特定位置。 提供商将能够通过ID和用户名查找用户,并且还能够验证密码。 来自此提供商的用户将是只读的。

提供者类

我们要经历的第一件事是用户存储提供程序类。

公众  属性文件用户存储提供程序 工具
        用户存储提供程序,
        UserLookupProvider,
        凭据输入验证器,
        证书
{
...
}

我们的提供商类,属性文件用户存储提供程序,实现了许多接口。 它实现了用户存储提供程序因为这是SPI的基本要求。 它实现了用户查找提供程序接口,因为我们希望能够与该提供商存储的用户一起登录。 它实现了凭据输入验证器界面,因为我们希望能够验证使用登录屏幕输入的密码。 我们的属性文件是只读的。 我们实施证书因为我们想在用户尝试更新密码时发布错误条件。

    受保护KeycloakSession会话;
    受保护 属性属性;
    受保护组件模型模型;
    // 此事务中已加载用户的映射
    受保护 地图<字符串,用户模型> loadedUsers =新的 HashMap<>();

    公众PropertyFileUserStorageProvider(KeycloakSession,ComponentModel model,属性属性) {
        这个。session = session;
        这个。模型 = 模型;
        这个。属性 = 属性;
    }

此提供程序类的构造函数将存储对KeycloakSession,组件模型,以及属性文件。 我们稍后会用到所有这些。 还要注意,有一个加载的用户地图。 每当我们找到用户时,我们都会将其存储在此地图中,以便避免在同一交易中再次创建它。 这是一个很好的做法,因为许多提供商需要这样做 (也就是说,任何与JPA集成的提供商)。 还请记住,每个事务创建一次提供者类实例,并在事务完成后关闭。

UserLookupProvider实现
    @ 覆盖
    公众用户模型获取用户用户名 (字符串用户名,真实模型领域) {
        UserModel adapter = loadedUsers.get (用户名);
        如果(适配器 = =null) {
            字符串password = properties.getProperty (用户名);
            如果(密码!=null) {
                adapter = createAdapter (领域,用户名);
                loadedUsers.Pue (用户名,适配器);
            }
        }
        返回适配器;
    }

    受保护用户模型创建适配器 (真实模型领域,字符串用户名) {
        返回 新的AbstractUserAdapter (会话、领域、模型) {
            @ 覆盖
            公众 字符串获取用户名 () {
                返回用户名;
            }
        };
    }

    @ 覆盖
    公众用户模型getUserById (字符串id,RealmModel领域) {
        存储id =新的存储id (id);
        字符串username = storageId.getExternalId();
        返回getUserByUsername (用户名,领域);
    }

    @ 覆盖
    公众用户模型getUserByEmail (字符串电子邮件,真实模型领域) {
        返回 null;
    }

获取用户用户名 ()当用户登录时,方法由Keycloak登录页面调用。 在我们的实现中,我们首先检查加载用户映射以查看用户是否已在此事务中加载。 如果尚未加载,我们将在属性文件中查找用户名。 如果它存在,我们创建一个实现用户模型,储存在加载用户供将来参考,并返回此实例。

创建适配器 ()方法使用helper类组织。Keycloak。存储。适配器。抽象用户适配器。 这为用户模型。 它使用用户的用户名作为外部id,根据所需的存储id格式自动生成用户id。

"f:" + 组件id + ":" + 用户名

每一种获取方法抽象用户适配器返回null或空集合。 但是,返回角色和组映射的方法将为每个用户返回为领域配置的默认角色和组。 每一套方法抽象用户适配器会扔一个组织。Keycloak。存储。ReadOnlyException。 因此,如果您尝试在管理控制台中修改用户,则会收到错误消息。

获取用户标识 ()方法解析id参数使用组织。Keycloak。存储id助手类。 的存储id.getExternalId()调用方法来获取嵌入在id参数。 然后,该方法委托给获取用户用户名 ()

电子邮件没有存储,所以获取用户邮件 ()方法返回null。

CredentialInputValidator实现

接下来,让我们看一下凭据输入验证器

    @ 覆盖
    公众 布尔isConfiguredFor (真实模型领域,用户模型用户,字符串凭据类型) {
        字符串password = properties.getProperty(user.getUsername());
        返回credentialType.equals(CredentialModel.PASSWORD) & & password!=null;
    }

    @ 覆盖
    公众 布尔支持scredentialtype (字符串凭据类型) {
        返回credentialType.equals(CredentialModel.PASSWORD);
    }

    @ 覆盖
    公众 布尔有效 (真实模型领域、用户模型用户、凭证输入) {
        如果(!支持scredentialtype (输入.getType()返回 ;

        字符串password = properties.getProperty(user.getUsername());
        如果(密码 = =null)返回 ;
        返回password.equals(input.getChallengeResponse());
    }

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

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

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

凭据inputupdater实现

如前所述,我们实现证书此示例中的接口禁止修改用户密码。 我们必须这样做的原因是因为否则运行时将允许密码在Keycloak本地存储中被覆盖。 我们将在本章后面详细讨论这一点。

    @ 覆盖
    公众 布尔updateCredential (真实模型领域、用户模型用户、凭据输入) {
        如果(输入.getType().equals (凭据模型.密码))投掷 新的ReadOnlyException (用户对于此更新是只读的);

        返回 ;
    }

    @ 覆盖
    公众 虚空禁用类型 (RealmModel领域,用户模型用户,字符串凭据类型) {

    }

    @ 覆盖
    公众 设置<字符串> getdisableecredentialtypes (真实模型领域,用户模型用户) {
        返回 收藏。EMPTY_SET;
    }

updateCredential()方法只是检查凭据类型是否为密码。 如果是的话,一个ReadOnlyException被扔了。

提供商工厂实施

现在提供者类已经完成,我们现在将注意力转向提供者工厂类。

公众  属性文件用户存储提供工厂
                 工具用户存储提供器工厂 <属性文件用户存储提供器> {

    公众 静态 决赛 字符串供应商名称 =只读-属性-文件;

    @ 覆盖
    公众 字符串getId() {
        返回供应商名称;
    }

首先要注意的是,在实现用户存储提供工厂类,您必须将具体提供程序类实现作为模板参数传递。 这里我们指定我们之前定义的提供者类:属性文件用户存储提供程序

如果未指定模板参数,则提供程序将不起作用。 运行时做类内省 确定能力接口提供者实现的。

getId()方法在运行时标识工厂,并且当您要为领域启用用户存储提供程序时,它也将是管理控制台中显示的字符串。

初始化
    私人 静态 决赛 记录器记录器 =记录器。getLogger(PropertyFileUserStorageProviderFactory.class);
    受保护 属性属性 =新的 属性();

    @ 覆盖
    公众 虚空init (配置。范围配置) {
        输入流is = getClass()。getClassLoader()。getResourceAsStream (/用户。属性);

        如果(is = =null) {
            记录器。警告 (找不到用户。类路径中的属性);
        }其他{
            尝试{
                属性。负载 (is);
            }抓住(IOExceptionex) {
                记录器。错误 (加载用户失败。属性文件,例如);
            }
        }
    }

    @ 覆盖
    公众PropertyFileUserStorageProvider创建 (KeycloakSession会话,组件模型) {
        返回 新的PropertyFileUserStorageProvider (会话、模型、属性);
    }

用户存储提供工厂接口有一个可选的init()方法你可以实现。 当Keycloak启动时,仅创建每个提供者工厂的一个实例。 同样在启动时,init()在这些工厂实例中的每一个上调用方法。 还有一个postInit()方法你也可以实现。 在每个工厂的init()方法被调用,他们的postInit()方法被调用。

在我们的init()方法实现时,我们从类路径中找到包含我们的用户声明的属性文件。 然后我们加载属性存储有用户名和密码组合的字段。

Config.Scope参数是可以在其中设置的工厂配置独立。xml,standalone-ha.xml,或者域。xml

例如,通过将以下内容添加到独立。xml:

<spi 名称=存储>
    <提供者 名称=只读-属性-文件 已启用=>
        <属性>
            <属性 名称=路径 =/other-users.properties/>
        </属性>
    </提供者>
</spi>

我们可以指定用户属性文件的类路径,而不是对其进行硬编码。 然后,您可以在PropertyFileUserStorageProviderFactory.init():

公众 虚空init (配置。范围配置) {
    字符串路径 = config.get (路径);
    输入流is = getClass()。getClassLoader()。getResourceAsStream (路径);

    ...
}
创建方法

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

    @ 覆盖
    公众PropertyFileUserStorageProvider创建 (KeycloakSession会话,组件模型) {
        返回 新的PropertyFileUserStorageProvider (会话、模型、属性);
    }

我们只需分配属性文件用户存储提供程序类。 这个create方法将在每个事务中调用一次。

包装和部署

我们的提供程序实现的类文件应该放在一个jar中。 您还必须在元信息/服务/组织。Keycloak。存储。用户存储提供工厂文件。

org.keycloak.examples.federation.properties.FilePropertiesStorageFactory

创建jar后,您可以使用常规WildFly进行部署,方法是: 将jar复制到独立/部署/目录或使用JBoss CLI。

在管理控制台中启用提供程序

您可以在每个领域启用用户存储提供商用户联合管理控制台中的页面。

用户联合

空用户联合页

从列表中选择我们刚刚创建的提供程序:只读-属性-文件。 它将您带到我们提供商的配置页面。 我们没有什么要配置的,所以点击保存

配置的提供程序

已创建存储提供程序

当你回到主要用户联合页面,您现在看到列出了您的提供商。

用户联合

用户联合页面

您现在可以使用在用户。属性文件。 该用户将只能在登录后查看帐户页面。

配置技术

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

用户存储提供工厂您可以实现处理提供程序配置的其他方法。 您描述了每个提供程序要配置的变量,并且管理控制台会自动呈现通用输入页面以收集此配置。 实现后,回调方法还会在保存配置之前、首次创建提供程序时以及更新配置时对其进行验证。 用户存储提供工厂org.keycloak.com组件工厂接口。

    列表<ProviderConfigProperty> getConfigProperties();

    默认
    虚空验证配置 (KeycloakSession会话,RealmModel领域,ComponentModel模型)
            投掷组件验证异常
    {

    }

    默认
    虚空onCreate(KeycloakSession会话,RealmModel领域,ComponentModel模型) {

    }

    默认
    虚空onUpdate(KeycloakSession会话,RealmModel领域,ComponentModel模型) {

    }

组件工厂。getConfigProperties()方法返回一个列表组织。密钥斗篷。提供程序。提供配置属性实例。 这些实例声明呈现和存储提供程序的每个配置变量所需的元数据。

配置示例

让我们扩大我们的属性文件用户存储提供工厂示例,以允许您将提供程序实例指向磁盘上的特定文件。

属性文件用户存储提供工厂
公众  属性文件用户存储提供工厂
                  工具用户存储提供器工厂 <属性文件用户存储提供器> {

    受保护 静态 决赛 列表<ProviderConfigProperty> configMetadata;

    静态{
        configMetadata = 提供配置生成器。创建 ()
                。属性 ()。名称 (路径)
                。类型 (提供配置属性。STRING_TYPE)
                。标签 (路径)
                。默认值 (${jboss.server.config.dir}/example-users.properties)
                。helpText (属性文件的文件路径)
                。添加 ()。构建 ();
    }

    @ 覆盖
    公众 列表<ProviderConfigProperty> getConfigProperties() {
        返回配置元数据;
    }

提供配置生成器类是创建配置属性列表的一个很好的帮助类。 这里我们指定一个名为路径这是一个字符串类型。 在此提供程序的 “管理控制台配置” 页面上,此配置变量标记为路径并且默认值为${jboss.server.config.dir}/example-users.properties。 当您将鼠标悬停在此配置选项的工具提示上时,它将显示帮助文本,属性文件的文件路径

接下来我们要做的是验证此文件是否存在于磁盘上。 我们不想在领域中启用此提供程序的实例,除非它指向有效的用户属性文件。 为此,我们实现了验证配置 ()方法。

    @ 覆盖
    公众 虚空验证配置 (KeycloakSession会话,RealmModel领域,ComponentModel config)
                   投掷组件验证异常 {
        字符串fp = config.getConfig().getFirst (路径);
        如果(fp = =null)投掷 新的ComponentValidationException (用户属性文件不存在);
        fp = EnvUtil。替换 (fp);
        文件文件 =新的 文件(fp);
        如果(!文件。存在 ()) {
            投掷 新的ComponentValidationException (用户属性文件不存在);
        }
    }

验证配置 ()方法我们从组件模型我们检查磁盘上是否存在该文件。 请注意,我们使用org.keycloak.com星期一.util.EnvUtil.replace()方法。 使用此方法,任何具有${}它将用系统属性值替换它。 的${jboss.server.config.dir}字符串对应于配置/我们服务器的目录,对于这个例子真的很有用。

接下来我们要做的是移除旧的init()方法。 我们这样做是因为用户属性文件将在每个提供程序实例中是唯一的。 我们把这个逻辑移到创建 ()方法。

    @ 覆盖
    公众PropertyFileUserStorageProvider创建 (KeycloakSession会话,组件模型) {
        字符串路径 = 模型。getConfig()。getFirst (路径);

        属性道具 =新的 属性();
        尝试{
            输入流is =新的 文件输入流(路径);
            道具。装载 (是);
            是。关闭 ();
        }抓住(IOExceptione) {
            投掷 新的 运行时间异常(e);
        }

        返回 新的PropertyFileUserStorageProvider (会话、模型、道具);
    }

当然,这种逻辑是低效的,因为每个事务从磁盘读取整个用户属性文件,但希望这以一种简单的方式说明了如何挂接配置变量。

在管理控制台中配置提供程序

现在配置已启用,您可以设置路径在管理控制台中配置提供程序时的变量。

配置的提供程序

具有配置的存储提供程序

添加/删除用户和查询能力接口

我们的示例没有做的一件事是允许它添加和删除用户或更改密码。 在我们的示例中定义的用户是 也不能在管理控制台中查询或查看。 要添加这些增强功能,我们的示例提供程序必须实现 的用户查询提供程序用户注册提供程序接口。

实现UserRegistrationProvider

要实现从该特定商店添加和删除用户,我们首先必须能够保存我们的属性 文件到磁盘。

属性文件用户存储提供程序
    公众 虚空保存 () {
        字符串路径 = 模型。getConfig()。getFirst (路径);
        path = EnvUtil.replace (路径);
        尝试{
            文件输出流fos =新的 文件输出流(路径);
            属性。存储 (fos,);
            fos.close();
        }抓住(IOExceptione) {
            投掷 新的 运行时间异常(e);
        }
    }

然后,实现了addUser()移除用户 ()方法变得简单。

属性文件用户存储提供程序
    公众 静态 决赛 字符串取消密码 =# $!-未设置-密码;

    @ 覆盖
    公众用户模型添加用户 (真实模型领域,字符串用户名) {
        同步(属性) {
            properties.setProperty (用户名,UNSET_PASSWORD);
            保存 ();
        }
        返回createAdapter (领域、用户名);
    }

    @ 覆盖
    公众 布尔移除用户 (RealmModel领域,用户模型用户) {
        同步(属性) {
            如果(属性。删除 (user.getUsername()) = =null)返回 ;
            保存 ();
            返回 ;
        }
    }

请注意,添加用户时,我们将属性映射的密码值设置为取消密码。 我们这样做是为了 我们不能在属性值中有一个属性的null值。 我们还必须修改凭据输入验证器 方法来反映这一点。

addUser()如果提供程序实现了用户注册提供程序接口。 如果您的提供商有 关闭添加用户的配置开关,返回null从该方法将跳过提供程序并调用 下一个。

属性文件用户存储提供程序
    @ 覆盖
    公众 布尔有效 (真实模型领域、用户模型用户、凭证输入) {
        如果(!支持scredentialtype (输入.getType()) | |!(输入实例用户凭证模型))返回 ;

        UserCredentialModel cred = (UserCredentialModel) 输入;
        字符串password = properties.getProperty(user.getUsername());
        如果(密码 = =null| | 取消密码。等于 (密码))返回 ;
        返回密码。等于 (cred.getValue());
    }

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

属性文件用户存储提供程序
    @ 覆盖
    公众 布尔updateCredential (真实模型领域、用户模型用户、凭据输入) {
        如果(!(输入实例用户凭证模型))返回 ;
        如果(!input.getType().equals(CredentialModel.PASSWORD))返回 ;
        UserCredentialModel cred = (UserCredentialModel) 输入;
        同步(属性) {
            properties.setProperty(user.getUsername(),cred.getValue());
            保存 ();
        }
        返回 ;
    }

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

属性文件用户存储提供程序
    @ 覆盖
    公众 虚空禁用类型 (RealmModel领域,用户模型用户,字符串凭据类型) {
        如果(!credentialType.equals(CredentialModel.PASSWORD))返回;
        同步(属性) {
            properties.setProperty(user.getUsername(),UNSET_PASSWORD);
            保存 ();
        }

    }

    私人 静态 决赛 设置<字符串> 禁用类型 =新的 哈希集<>();

    静态{
        Disabletypes.add(CredentialModel.PASSWORD);
    }

    @ 覆盖
    公众 设置<字符串> getdisableecredentialtypes (真实模型领域,用户模型用户) {

        返回禁用类型;
    }

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

实现UserQueryProvider

没有实施用户查询提供程序管理控制台将无法查看和管理已加载的用户 由我们的示例提供商提供。 让我们看看实现这个接口。

属性文件用户存储提供程序
    @ 覆盖
    公众 int获取用户计数 (真实模型领域) {
        返回属性.大小 ();
    }

    @ 覆盖
    公众 列表<用户模型> 获取用户 (真实模型领域) {
        返回getUsers (领域,0,整数。MAX_VALUE);
    }

    @ 覆盖
    公众 列表<用户模型> 获取用户 (真实模型领域,int第一个结果,int最大结果) {
        列表<用户模型> 用户 =新的 链接列表<>();
        inti =0;
        对于(对象obj: 属性。密钥集 ()) {
            如果(i ++ < firstResult)继续;
            字符串用户名 = (字符串) obj;
            UserModel user = getUserByUsername (用户名,领域);
            用户。添加 (用户);
            如果(用户大小 () >= 最大结果)休息;
        }
        返回用户;
    }

获取用户 ()方法迭代属性文件的键集,委派给获取用户用户名 ()加载用户。 请注意,我们正在根据第一个结果最大结果参数。 如果你的外部商店不支持分页,你将不得不做类似的逻辑。

属性文件用户存储提供程序
    @ 覆盖
    公众 列表<用户模型> 搜索用户 (字符串搜索,真实模型领域) {
        返回搜索用户 (搜索,领域,0,整数。MAX_VALUE);
    }

    @ 覆盖
    公众 列表<用户模型> 搜索用户 (字符串搜索,真实模型领域,int第一个结果,int最大结果) {
        列表<用户模型> 用户 =新的 链接列表<>();
        inti =0;
        对于(对象obj: 属性。密钥集 ()) {
            字符串用户名 = (字符串) obj;
            如果(!用户名。包含 (搜索)继续;
            如果(i ++ < firstResult)继续;
            UserModel user = getUserByUsername (用户名,领域);
            用户。添加 (用户);
            如果(用户大小 () >= 最大结果)休息;
        }
        返回用户;
    }

的第一份声明搜索用户 ()需要一个字符串参数。 这应该是一个字符串,你用 搜索用户名和电子邮件属性以查找用户。 这个字符串可以是一个子字符串,这就是为什么我们使用字符串。包含 () 方法在进行搜索时。

属性文件用户存储提供程序
    @ 覆盖
    公众 列表<用户模型> 搜索用户 (地图<字符串,字符串> 参数,真实模型领域) {
        返回搜索用户 (参数,领域,0,整数。MAX_VALUE);
    }

    @ 覆盖
    公众 列表<用户模型> 搜索用户 (地图<字符串,字符串> 参数,真实模型领域,int第一个结果,int最大结果) {
        // 仅支持按用户名搜索
        字符串用户名字符串 = 参数。获取 (用户名);
        如果(用户名字符串 = =null)返回 收藏。EMPTY_LIST;
        返回搜索用户 (usernameSearchString,领域,firstResult,maxresult);
    }

搜索用户 ()方法,该方法需要一个地图参数可以根据名字,姓氏,用户名和电子邮件搜索用户。 我们只存储用户名,所以我们只根据用户名进行搜索。 我们委托给搜索用户 ()为此。

属性文件用户存储提供程序
    @ 覆盖
    公众 列表<UserModel> getgroupmemers (RealmModel领域,GroupModel组,int第一个结果,int最大结果) {
        返回 收藏。EMPTY_LIST;
    }

    @ 覆盖
    公众 列表<UserModel> getgroupmemers (RealmModel领域,GroupModel组) {
        返回 收藏。EMPTY_LIST;
    }

    @ 覆盖
    公众 列表<用户模型> 搜索用户属性 (字符串attrName,字符串attrValue,RealmModel领域) {
        返回 收藏。EMPTY_LIST;
    }

我们不存储组或属性,因此其他方法返回一个空列表。

增加外部存储

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

对于这些类型的情况,Keycloak允许您通过存储额外的信息来增加外部存储 在Keycloak的数据库中。 这称为联合用户存储,封装在 组织。密钥斗篷。存储。联合。用户联合存储提供程序类。

用户联盟存储提供程序
包装 组织。Keycloak。存储。联合;

公众 接口 用户联盟存储提供程序 延伸 提供者{

    设置<GroupModel> getGroups(RealmModel领域,字符串用户id);
    虚空joinGroup(RealmModel领域,字符串用户id、GroupModel组);
    虚空离开组 (RealmModel领域,字符串用户id、GroupModel组);
    列表<字符串> getMembership(RealmModel领域,GroupModel组,int第一个结果,int最大);

...

用户联盟存储提供程序实例在KeycloakSession.userFederatedStorage()方法。 它具有用于存储属性,组和角色映射的所有不同类型的方法,不同的凭据类型, 和所需的行动。 如果您的外部商店的数据模型不能支持完整的Keycloak功能 设置,则此服务可以填补空白。

Keycloak带有一个助手类组织。Keycloak。存储。适配器。抽象用户适配器联合存储 这将委托每一个用户模型方法除了获取/设置用户名到用户联合存储。 覆盖 您需要重写以委托给外部存储表示的方法。 这是强烈的 建议您阅读此类的javadoc,因为它具有较小的受保护方法,您可能需要覆盖。 具体来说 周围的组成员和角色映射。

增强示例

在我们的属性文件用户存储提供程序例如,我们只需要对提供商进行简单的更改即可使用 摘要用户适配器联邦存储

属性文件用户存储提供程序
    受保护用户模型创建适配器 (真实模型领域,字符串用户名) {
        返回 新的Abstracuseradapterfederatedstorage (会话、领域、模型) {
            @ 覆盖
            公众 字符串获取用户名 () {
                返回用户名;
            }

            @ 覆盖
            公众 虚空设置用户名 (字符串用户名) {
                字符串pw = (字符串) 属性。删除 (用户名);
                如果(pw!=null) {
                    properties.put (用户名,pw);
                    保存 ();
                }
            }
        };
    }

我们改为定义一个匿名类实现摘要用户适配器联邦存储。 的设置用户名 () 方法对属性文件进行更改并保存。

进口实施策略

在实现用户存储提供商时,您可以采取另一种策略。 而不是使用用户联合存储, 您可以在Keycloak内置用户数据库中本地创建用户,并从您的外部复制属性 存储到本地副本中。 这种方法有很多优点。

  • Keycloak基本上成为您的外部存储的持久性用户缓存。 一旦导入用户 您将不再访问外部商店,从而减轻了负担。

  • 如果你要搬到Keycloak作为你的官方用户商店,并弃用旧的外部商店,你 可以慢慢迁移应用程序以使用Keycloak。 当所有应用程序都已迁移后,取消链接 导入用户,并退出旧的旧版外部商店。

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

  • 首次查找用户将需要对Keycloak数据库进行多次更新。 这可以 负载下的性能损失很大,并给Keycloak数据库带来很大压力。 用户联合 存储方法将仅根据需要存储额外的数据,并且可能永远不会使用,具体取决于外部存储的功能。

  • 使用导入方法,您必须保持本地Keycloak存储和外部存储同步。 用户存储SPI 您可以实现支持同步的功能接口,但这很快就会变得痛苦和混乱。

要实现导入策略,您只需首先检查用户是否已在本地导入。 如果是这样,请返回 本地用户,如果没有在本地创建用户并从外部存储中导入数据。 您也可以代理本地用户 这样大多数更改都会自动同步。

这将是一个人为的,但我们可以扩展我们的属性文件用户存储提供程序采取这种方法。 我们 首先从修改创建适配器 ()方法。

属性文件用户存储提供程序
    受保护用户模型创建适配器 (真实模型领域,字符串用户名) {
        UserModel local = session.userLocalStorage().getUserByUsername (用户名,领域);
        如果(本地 = =null) {
            local = session.userLocalStorage().addUser (领域,用户名);
            local.setFederationLink(model.getId());
        }
        返回 新的UserModelDelegate (本地) {
            @ 覆盖
            公众 虚空设置用户名 (字符串用户名) {
                字符串pw = (字符串) 属性。删除 (用户名);
                如果(pw!=null) {
                    properties.put (用户名,pw);
                    保存 ();
                }
                超级。setUsername (用户名);
            }
        };
    }

在这个方法中,我们调用KeycloakSession.userLocalStorage()获取对本地Keycloak的引用的方法 用户存储。 我们查看用户是否存储在本地,如果没有,我们将其添加到本地。 不要设置id本地用户的。 让Keycloak自动生成id。 还要注意,我们称之为 用户模型。设置联邦链接 ()并传入组件模型我们的供应商。 这在 提供者和导入的用户。

当删除用户存储提供程序时,它导入的任何用户也将被删除。 这是 呼叫的目的用户模型。设置联邦链接 ()

需要注意的另一件事是,如果本地用户已链接,则您的存储提供程序仍将委派给for methods 它从凭据输入验证器证书接口。 返回 从验证或更新只会导致Keycloak查看它是否可以使用验证或更新 本地存储。

另请注意,我们正在使用Org.Keycloak.mo dels.utils.UserModelDelegate类。 该类是用户模型。 每种方法都只委托给用户模型它是用实例化的。 我们覆盖了设置用户名 ()此委托类的方法,以自动与属性文件同步。 对于您的提供商,您可以使用它来拦截本地的其他方法用户模型执行同步 你的外部商店。 例如,get方法可以确保本地存储同步。 设置方法 保持外部商店与本地商店同步。 需要注意的一件事是getId()方法应始终返回 在本地创建用户时自动生成的id。 您不应该返回联邦id,如所示 其他非导入示例。

如果您的提供商正在实施用户注册提供程序界面,你的移除用户 ()方法不 需要从本地存储中删除用户。 运行时会自动执行此操作。 也 请注意,移除用户 ()将在从本地存储中删除之前调用。

ImportedUserValidation接口

如果您还记得本章前面的内容,我们讨论了查询用户的工作原理。 首先查询本地存储, 如果在那里找到用户,则查询结束。 这是我们想要的上述实现的问题 代理本地用户模型这样我们就可以保持用户名同步。 用户存储SPI在任何时候都有一个回调 从本地数据库加载链接的本地用户。

包装 组织。Keycloak。存储。用户;
公众 接口 输入用户验证{
    /**
     * 如果此方法返回null,则将删除本地存储中的用户
     *
     * @ param领域
     * @ param用户
     * @ 如果用户不再有效,则返回null
     */
    用户模型验证 (RealmModel领域,用户模型用户);
}

每当加载链接的本地用户时,如果用户存储提供程序类实现了此接口,则 验证 ()方法被调用。 在这里,您可以代理作为参数传入的本地用户并将其返回。 那 新的用户模型将被使用。 您还可以选择进行检查,以查看用户是否仍然存在于外部存储中。 如果验证 ()退货null,则本地用户将从数据库中删除。

导入同步接口

通过导入策略,您可以看到本地用户副本可能与 外部存储。 例如,可能用户已从外部商店中删除。 用户存储SPI具有 你可以实现一个额外的接口来处理这个问题,组织。密钥斗篷。存储。用户。导入同步:

包装 组织。Keycloak。存储。用户;

公众 接口 导入同步{
    同步结果同步 (KeycloakSessionFactory sessionFactory,字符串realmId,UserStorageProviderModel模型);
    同步结果同步 (日期最后同步,关键时钟会议工厂,字符串realmId,UserStorageProviderModel模型);
}

这个接口由提供者工厂实现。 一旦提供程序工厂实现了此界面,提供程序的管理控制台管理页面就会显示其他选项。 您可以通过单击按钮来手动强制同步。 这将调用导入同步。同步 ()方法。 此外,还会显示其他配置选项,使您可以自动安排同步。 自动同步调用同步 ()方法。

用户缓存

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

管理用户缓存

您可以通过调用访问用户缓存KeycloakSession。用户缓存 ()

/**
 * 所有这些方法都会影响整个Keycloak实例集群。
 *
 * @ 作者 <a href = "mailto:bill@burkecentral.com"> 比尔·伯克 </a>
 * @ 版本 $ 修订版: 1 $
 */
公众 接口 用户缓存 延伸用户提供者 {
    /**
     * 从缓存中驱逐用户。
     *
     * @ param用户
     */
    虚空逐出 (RealmModel领域,用户模型用户);

    /**
     * 驱逐特定领域的用户
     *
     * @ param领域
     */
    虚空驱逐 (现实模式领域);

    /**
     * 完全清除缓存。
     *
     */
    虚空清除 ();
}

存在用于驱逐特定用户,特定领域中包含的用户或整个缓存的方法。

OnUserCache回调接口

您可能希望缓存特定于提供程序实现的其他信息。 用户存储SPI 每当用户被缓存时都有一个回调:Org.Keycloak.mo dels.cache.OnUserCache

公众 接口 OnUserCache{
    虚空onCache(RealmModel领域,CachedUserModel用户,UserModel委托);
}

你的提供者类应该实现这个接口,如果它想要这个回调。 的用户模型委托参数 是用户模型您的提供商返回的实例。 的缓存用户模型是一个扩展的用户模型接口。 这是本地存储中本地缓存的实例。

公众 接口 缓存用户模型 延伸用户模型 {

    /**
     * 使该用户的缓存无效,并返回代表实际数据提供程序的委托
     *
     * @ 返回
     */
    用户模型getDelegateForUpdate();

    布尔是指驱逐 ();

    /**
     * 使此模型的缓存无效
     *
     */
    虚空无效 ();

    /**
     * 什么时候从数据库加载模型。
     *
     * @ 返回
     */
    getCacheTimestamp();

    /**
     * 返回一个包含与该模型一起缓存的自定义内容的地图。  你可以写这张地图。
     *
     * @ 返回
     */
    同意thashmapGetcached与 ();
}

这个缓存用户模型接口允许您从缓存中驱逐用户并获取提供商用户模型实例。 的Getcached与 ()方法返回一个映射,该映射允许您缓存与用户有关的其他信息。 例如,凭据不是用户模型接口。 如果要在内存中缓存凭据,则可以实现OnUserCache并使用Getcached与 ()方法。

缓存策略

在用户存储提供程序的 “管理控制台管理” 页面上,您可以指定唯一的缓存策略。

利用Java EE

用户存储提供程序可以打包在任何Java EE组件中,如果您设置了元信息/服务 正确归档以指向您的提供商。 例如,如果您的提供商需要使用第三方库,则您 可以将您的提供商打包在耳朵内,并将这些第三方库存储在lib/耳朵的目录。 另请注意,提供商罐子可以利用jboss-deployment-structure.xmlEjb、战争和耳朵的文件 可以在野蝇环境中使用。 有关此文件的更多详细信息,请参阅WildFly文档。 它 允许您在其他细粒度操作之间拉入外部依赖项。

提供程序实现必须是普通的java对象。 但是我们目前也支持 实施用户存储提供程序类作为有状态ejb。 如果要使用JPA,这尤其有用 连接到关系存储。 这是你会怎么做:

@ 有状态的
@ 本地(EjbExampleUserStorageProvider.class)
公众  EjbExampleUserStorageProvider 工具用户存储提供程序,
        UserLookupProvider,
        UserRegistrationProvider,
        用户查询提供程序,
        证书,
        凭据输入验证器,
        OnUserCache
{
    @ PersistenceContext
    受保护实体经理em;

    受保护组件模型模型;
    受保护KeycloakSession会话;

    公众 虚空setModel (组件模型) {
        这个。模型 = 模型;
    }

    公众 虚空setSession(KeycloakSession会话) {
        这个。session = session;
    }


    @ 移除
    @ 覆盖
    公众 虚空关闭 () {
    }
...
}

你必须定义@ 本地注释并在那里指定您的提供者类。 如果不这样做,EJB将 不能正确代理用户,您的提供商将无法工作。

你必须把@ 移除上的注释关闭 ()您的提供商的方法。 如果你不这样做,有状态的豆 永远不会被清理,您最终可能会看到错误消息。

的实现用户存储提供程序被要求是普通的Java对象。 你的工厂班会 在其create() 方法中执行有状态EJB的JNDI查找。

公众  EjbExampleUserStorageProviderFactory
        工具用户存储提供工厂 <EjbExampleUserStorageProvider> {

    @ 覆盖
    公众EjbExampleUserStorageProvider创建 (KeycloakSession会话,组件模型) {
        尝试{
            初始上下文ctx =新的 初始上下文();
            EjbExampleUserStorageProvider = (EjbExampleUserStorageProvider)ctx.lookup (
                     java: 全局/用户-存储-jpa-示例/+ EjbExampleUserStorageProvider.class.getSimpleName());
            provider.setModel (模型);
            provider.setSession (会话);
            返回提供者;
        }抓住(异常e) {
            投掷 新的 运行时间异常(e);
        }
    }

此示例还假设您已经在与提供程序相同的JAR中定义了JPA部署。 这意味着持久性。xml 文件以及任何JPA@ 实体类。

使用JPA时,任何其他数据源都必须是XA数据源。 Keycloak数据源 不是XA数据源。 如果您在同一事务中与两个或多个非XA数据源交互,服务器将返回 一条错误消息。 在单个事务中仅允许一个非XA资源。 有关部署XA数据源的更多详细信息,请参见WildFly手册。

CDI不支持。

REST管理API

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

REST组件API位于您的领域管理资源下。

/管理员/领域/{领域名称}/组件

我们将只展示这个REST API与Java客户端的交互。 希望你能从中提取如何做到这一点卷曲从这个API。

公众 接口 组件资源{
    @ GET
    @ 生产(MediaType.APPLICATION_JSON)
    公众 列表<组件表示> 查询 ();

    @ GET
    @ 生产(MediaType.APPLICATION_JSON)
    公众 列表<组件表示> 查询 (@ QueryParam(父母)字符串父母);

    @ GET
    @ 生产(MediaType.APPLICATION_JSON)
    公众 列表<组件表示> 查询 (@ QueryParam(父母)字符串父母,@ QueryParam(类型)字符串类型);

    @ GET
    @ 生产(MediaType.APPLICATION_JSON)
    公众 列表<组件表示> 查询 (@ QueryParam(父母)字符串父母,
                                               @ QueryParam(类型)字符串类型,
                                               @ QueryParam(名称)字符串姓名);

    @ 帖子
    @ 消费(MediaType.APPLICATION_JSON)
    响应添加 (组件表示代表);

    @ 路径({id})
    组件资源组件 (@ PathParam(id)字符串身份证);
}

公众 接口 组件资源{
    @ GET
    公众组件表示形式 ();

    @ 放
    @ 消费(MediaType.APPLICATION_JSON)
    公众 虚空更新 (组件表示代表);

    @ 删除
    公众 虚空删除 ();
}

要创建用户存储提供程序,必须指定提供程序id,即字符串的提供程序类型组织。密钥斗篷。存储。用户存储提供程序, 以及配置。

导入 组织。Keycloak。管理。客户端。Keycloak;
导入 组织。Keycloak。代表。idm。真实代表;
...

Keycloak keycloak = Keycloak.getInstance (
    http:// localhost:8080/auth,
    大师,
    管理员,
    密码,
    管理-cli);
RealmResource realmResource = Keycloak。领域 (大师);
RealmRepresentation领域 = realmResource。representation ();

组件表示组件 =新的组件表示 ();
组件。setName ();
组件。setProviderId (只读-属性-文件);
组件。setProviderType (组织。密钥斗篷。存储。用户存储提供程序);
组件。setParentId (领域。getId());
组件。setConfig (新的MultivaluedHashMap());
组件。getConfig()。putSingle (路径,~/用户。属性);

realmResource.com组件 ()。添加 (组件);

// 检索组件

列表<组件表示> 组件 = realmResource.com组件 ()。查询 (领域。getId(),
                                                                    组织。密钥斗篷。存储。用户存储提供程序,
                                                                    );
component = components。get (0);

// 更新一个组件

组件。getConfig()。putSingle (路径,~/my-users.properties);
realmResource.com组件 ()。组件 (组件。getId())。更新 (组件);

// 移除一个组件

realmREsource.com组件 ()。组件 (组件。getId())。删除 ();

从早期的用户联合SPI迁移

本章仅适用于使用较早版本 (现在已删除) 实现提供程序的情况 用户联盟SPI。

在Keycloak版本2.4.0和更早版本中,有一个用户联盟SPI。 红帽单点登录版本7.0,虽然不支持,有 这个早期的SPI也可用。 此较早的用户联盟SPI已从Keycloak版本2.5.0和Red Hat单点登录版本7.1中删除。 但是,如果您使用此较早的SPI编写了提供程序,则本章将讨论一些可以用来移植它的策略。

进口与非进口

早期的用户联合SPI要求您在Keycloak的数据库中创建用户的本地副本 并将信息从外部商店导入到本地副本。 然而,这不再是一项要求。 你仍然可以 将您以前的提供商按现在的方式移植,但是您应该考虑非导入策略是否可能是更好的方法。

进口策略的优势:

  • Keycloak基本上成为您的外部存储的持久性用户缓存。 一旦导入用户 您将不再使用外部商店,从而减轻了负担。

  • 如果您要将Keycloak作为您的官方用户商店并弃用较早的外部商店,则您 可以慢慢迁移应用程序以使用Keycloak。 当所有应用程序都已迁移后,取消链接 导入用户,并退出较早的旧版外部存储。

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

  • 首次查找用户将需要对Keycloak数据库进行多次更新。 这可以 负载下的性能损失很大,并给Keycloak数据库带来很大压力。 用户联合 存储方法将仅根据需要存储额外的数据,并且可能永远不会使用,具体取决于外部存储的功能。

  • 使用导入方法,您必须保持本地Keycloak存储和外部存储同步。 用户存储SPI 您可以实现支持同步的功能接口,但这很快就会变得痛苦和混乱。

用户联邦提供程序与用户存储提供程序

首先要注意的是用户联邦提供程序是一个完整的界面。 您在这个接口中实现了每个方法。 然而,用户存储提供程序取而代之的是将此接口分解为您根据需要实现的多个功能接口。

UserFederationProvider.getUserByUsername()获取用户邮件 ()在新的SPI中有完全等同的。 两者之间的区别在于您如何导入。 如果您要继续执行导入策略,则不再致电KeycloakSession.userStorage().addUser()在本地创建用户。 相反,你打电话给KeycloakSession.userLocalStorage().addUser()。 的用户存储 ()方法不再存在。

用户联邦提供程序。验证和代理 ()方法已移至可选能力接口,输入用户验证。 如果您要按现在的方式移植您的早期提供商,您希望实现此接口。 还要注意,在较早的SPI中,即使本地用户在缓存中,每次访问用户时都会调用此方法。 在后来的SPI中,只有当本地用户从本地存储加载时才调用此方法。 如果本地用户被缓存, 然后ImportedUserValidation.validate()方法根本不被调用。

UserFederationProvider.isValid()方法在后面的SPI中不再存在。

用户联邦提供程序方法同步注册 (),注册用户 (),以及移除用户 ()已经 移动到用户注册提供程序能力接口。 这个新接口是可选的,所以如果你的 提供程序不支持创建和删除用户,您不必实现它。 如果您之前的提供商有switch 要切换对注册新用户的支持,新的SPI支持此功能,返回null来自 用户注册提供程序。添加用户 ()如果提供商不支持添加用户。

较早的用户联邦提供程序现在,以凭据为中心的方法被封装在凭据输入验证器证书接口,根据您是否支持验证或 更新凭据。 用于存在于用户模型方法。 这些也已移至 凭据输入验证器证书接口。 需要注意的一点是,如果你不实现证书接口,然后 您的提供商提供的任何凭据都可以在Keycloak存储中本地覆盖。 所以如果你愿意 您的凭据是只读的,实现CredentialInputUpdater.updateCredential()方法和 返回aReadOnlyException

用户联邦提供程序查询方法,例如搜索属性 ()Getgroupmem员们 ()现在被封装了 在可选界面中用户查询提供程序。 如果您不实现此界面,则用户将无法查看 在管理控制台中。 不过,你仍然可以登录。

用户联邦提供工厂与用户存储提供工厂

早期SPI中的同步方法现在封装在一个可选的导入同步接口。 如果你已经实现了同步逻辑,那么你的新用户存储提供工厂实施 导入同步接口。

升级到新型号

用户存储SPI实例存储在一组不同的关系表中。 Keycloak 自动运行迁移脚本。 如果为某个领域部署了任何早期的用户联合提供程序,它们将被转换 到以后的存储模型,包括id的数据。 只有当用户存储提供商存在时,才会发生这种迁移 具有与较早的用户联合提供者相同的提供者ID (即 “ldap” 、 “kerberos”)。

所以,知道这一点,你可以采取不同的方法。

  1. 您可以在较早的Keycloak部署中删除较早的提供程序。 这将删除本地链接的副本 在您导入的所有用户中。 然后,当您升级Keycloak时,只需为您的领域部署和配置新的提供商。

  2. 第二种选择是编写您的新提供程序,确保它具有相同的提供程序ID:用户存储提供工厂。getId()。 确保此提供程序在独立/部署/新Keycloak安装的目录。 启动服务器,并具有 内置的迁移脚本从早期的数据模型转换为后期的数据模型。 在这种情况下,所有您之前链接的导入 用户将工作并保持不变。

如果您决定摆脱导入策略并重写您的用户存储提供商,我们建议您删除较早的提供商 升级Keycloak之前。 这将删除您导入的任何用户的链接本地导入副本。

基于流的接口

Keycloak中的许多用户存储界面都包含查询方法,这些方法可以返回潜在的大型对象集, 这可能会导致内存消耗和处理时间方面的重大影响。 当只有 在查询方法的逻辑中使用对象内部状态的一小部分。

为开发人员提供一种更有效的替代方案,以在这些查询方法中处理大型数据集,溪流 子接口已添加到用户存储接口中。 这些溪流子接口取代了原来的基于集合的 超级接口中的方法与基于流的变体,使基于集合的方法成为默认值。 默认实现 基于集合的查询方法调用其对应并将结果收集到适当的收集类型中。

溪流子接口允许实现专注于基于流的方法来处理数据集和 受益于该方法的潜在内存和性能优化。 提供溪流 要实现的子接口包括几个能力接口,中的所有接口组织。Keycloak。存储。联合 包和其他一些可能根据自定义存储实现的范围而实现的包。

请参阅提供溪流给开发人员的子接口。

包装

组织。Keycloak。证书

证书(*),用户凭证存储

Org.Keycloak.mo dels

组模型,角色映射模型,用户凭证管理器,用户模型,用户提供者

Org.Keycloak.mo dels.缓存

缓存用户模型,用户缓存

组织。Keycloak。存储。联合

所有接口

组织。凯奇洛克。存储。用户

用户查询提供程序(*)

(*) 表示接口是能力接口

想要从流方法中受益的自定义用户存储实现应该简单地实现溪流 子接口而不是原始接口。 例如,下面的代码使用溪流的变体用户查询提供程序 接口:

公众  定制提供者 延伸用户查询提供程序。流 {
...
    @ 覆盖
    流 <用户模型> 获取用户流 (真实模型领域,整数第一个结果,整数最大结果) {
        // 这里的自定义逻辑
    }

    @ 覆盖
    流 <用户模型> 搜索用户流 (字符串搜索,真实模型领域) {
        // 这里的自定义逻辑
    }
...
}

保险库SPI

保险库提供程序

您可以从以下位置使用vault SPI组织。Keycloak。保险库程序包为Keycloak编写自定义扩展,以连接到任意vault实现。

内置的文件-明文provider是这个SPI实现的一个例子。 一般来说,以下规则适用:

  • 为了防止秘密跨领域泄漏,您可能希望隔离或限制可以由领域检索的秘密。 在这种情况下,您的提供者在查找秘密时应考虑领域名称,例如通过前缀 具有领域名称的条目。 例如,一个表达式${vault.key}然后会对不同的条目进行一般的评估 名称,取决于它是否在一个领域使用A或领域B。为了区分领域,领域需要 传递给创建的VaultProvider实例来自VaultProviderFactory.create()方法,其中可从 KeycloakSession参数。

  • vault提供程序需要实现一个单一的方法获取秘密返回一个VaultRawSecret对于给定的秘密名称。 该类持有秘密的表示形式字节 []或者字节缓冲并有望根据需要在两者之间转换。 请注意,该缓冲区将在使用后被丢弃,如下所述。

关于领域分离,所有内置的vault提供商工厂都允许配置一个或多个关键解析器。 代表 由VaultKeyResolver接口,一个键解析器本质上实现了用于组合领域名称的算法或策略 使用密钥 (从${vault.key}表达式) 转换为最终的条目名称,该条目名称将用于检索 金库的秘密。 处理此配置的代码已提取到抽象vault提供程序和vault中 提供程序工厂类,因此想要为密钥解析器提供支持的自定义实现可以扩展这些抽象类 而不是实现SPI接口来继承配置检索秘密时应尝试的密钥解析器的功能。

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

消耗vault中的值

保险库包含敏感数据,Keycloak会相应地处理这些秘密。 访问秘密时,从vault中获取秘密,并仅在必要的时间内保留在JVM内存中。 然后,所有可能的从JVM内存中丢弃其内容的尝试都完成了。 这是通过仅在以下位置使用保险库秘密来实现的尝试-资源声明概述如下:

    char[]c;
    尝试(VaultCharSecret cSecret = session.vault().getCharSecret(SECRET_NAME)) {
        //...使用cSecret
        c = cSecret.getAsArray().orElse (null);
        // 如果c!= null,它现在包含密码
    }

    // 如果c!= null,它现在包含垃圾

该示例使用KeycloakSession.vault()作为访问的入口点 秘密。 使用VaultProvider。获得秘密方法直接确实是 也有可能。 然而保险库 ()方法具有能力的好处 解释原始秘密 (通常是字节数组) 作为字符数组 (通过保险库 ()。getCharSecret()) 或字符串 (通过vault()。getStringSecret()) 除了获得原件 未解释的值 (通过保险库 ()。getRawSecret()方法)。

请注意,由于字符串对象是不可变的,其内容不能丢弃 通过覆盖随机垃圾。 即使在默认情况下已经采取了措施 VaultStringSecret防止内部化的实施字符串s,秘密 存储在字符串对象至少会存活到下一轮GC。 因此使用 纯字节和字符数组和缓冲区是优选的。