12. Additional Topics

在本部分中,我们介绍了一些功能,这些功能需要了解前几章,以及一些框架的更高级和更不常用的功能。

12.1 域对象安全性(ACL)

12.1.1 Overview

复杂的应用程序经常会发现需要定义访问权限,而不仅仅是在 Web 请求或方法调用级别。取而代之的是,安全决策需要同时包括谁(Authentication),何处(MethodInvocation)和什么(SomeDomainObject)。换句话说,授权决策还需要考虑方法调用的实际域对象实例主题。

假设您正在设计宠物诊所的应用程序。基于 Spring 的应用程序将有两个主要的用户组:宠物诊所的工作人员以及宠物诊所的 Client。工作人员将有权访问所有数据,而您的 Client 只能看到他们自己的 Client 记录。为了使其更有趣一点,您的 Client 可以允许其他用户查看其 Client 记录,例如其“幼稚园幼教”导师或本地“小马俱乐部”总裁。以 Spring Security 为基础,您可以使用几种方法:

  • 编写您的业务方法以增强安全性。您可以在Customer域对象实例中查询集合,以确定哪些用户有权访问。通过使用SecurityContextHolder.getContext().getAuthentication(),您将可以访问Authentication对象。

  • 编写AccessDecisionVoter以增强Authentication对象中存储的GrantedAuthority[]的安全性。这意味着您的AuthenticationManager需要用代表用户可以访问的Customer域对象实例的自定义GrantedAuthority[]填充Authentication

  • 编写AccessDecisionVoter以增强安全性,然后直接打开目标Customer域对象。这意味着您的投票者需要访问 DAO,以使其能够检索Customer对象。然后它将访问Customer对象的已批准用户的集合并做出适当的决定。

这些方法中的每一种都是完全合法的。但是,第一个将您的授权检查与您的业务代码结合在一起。这样做的主要问题包括单元测试的难度增加以及在其他地方重用Customer授权逻辑会更加困难。从Authentication对象获得GrantedAuthority[] s 也是可以的,但不会扩展为大量的Customer s。如果用户能够访问 5,000_s(在这种情况下不太可能,但是可以想象如果它是大型 Pony Club 的受欢迎的兽医!),那么消耗内存量和构造Authentication对象所需的时间将是不希望的。最后一种方法,直接从外部代码打开Customer,可能是这三种方法中最好的。它可以实现关注点分离,并且不会滥用内存或 CPU 周期,但是它仍然效率低下,因为AccessDecisionVoter和最终的业务方法本身都将执行对负责检索Customer对象的 DAO 的调用。每个方法调用两次访问显然是不可取的。此外,列出每种方法时,您都需要从头开始编写自己的访问控制列表(ACL)持久性和业务逻辑。

幸运的是,还有另一种选择,我们将在下面讨论。

12.1.2 关键概念

Spring Security 的 ACL 服务位于spring-security-acl-xxx.jar中。您将需要将此 JAR 添加到 Classpath 中,以使用 Spring Security 的域对象实例安全性功能。

Spring Security 的域对象实例安全性功能以访问控制列表(ACL)的概念为中心。系统中的每个域对象实例都有其自己的 ACL,并且 ACL 记录了谁可以使用该域对象以及不能使用该域对象的详细信息。考虑到这一点,Spring Security 为您的应用程序提供了三个与 ACL 相关的主要功能:

  • 一种有效检索所有域对象的 ACL 条目(并修改这些 ACL)的方法

  • 确保在调用方法之前允许给定的主体使用对象的方法

  • 在调用方法之后,一种确保给定的委托人可以使用您的对象(或它们返回的对象)的方法

如第一个项目要点所示,Spring Security ACL 模块的主要功能之一是提供一种高性能的 ACL 检索方法。这个 ACLRepositories 功能非常重要,因为系统中的每个域对象实例都可能有多个访问控制项,并且每个 ACL 都可能以树状结构从其他 ACL 继承(Spring 对此提供了开箱即用的支持)安全性,并且非常常用)。 Spring Security 的 ACL 功能经过精心设计,可提供高性能的 ACL 检索,以及可插入的缓存,最小化死锁的数据库更新,与 ORM 框架的独立性(我们直接使用 JDBC),适当的封装以及透明的数据库更新。

给定数据库对于 ACL 模块的操作至关重要,让我们研究实现中默认使用的四个主表。以下是典型的 Spring Security ACL 部署中按大小 Sequences 显示的表,最后列出的行数最多:

  • ACL_SID 允许我们唯一地标识系统中的任何主体或权限(“ SID”代表“安全身份”)。唯一的列是 ID,SID 的文本表示形式以及用于指示文本表示形式是引用主体名称还是GrantedAuthority的标志。因此,每个唯一的主体或GrantedAuthority都有一行。当在接收许可的上下文中使用 SID 时,通常将其称为“收件人”。

  • ACL_CLASS 允许我们唯一地标识系统中的任何域对象类。唯一的列是 ID 和 Java 类名称。因此,对于每个我们希望存储其 ACL 权限的唯一类,都有一行。

  • ACL_OBJECT_IDENTITY 存储系统中每个唯一域对象实例的信息。列包括 ID,ACL_CLASS 表的外键,唯一标识符,因此我们知道我们要为其提供信息的 ACL_CLASS 实例,父级,ACL_SID 表的外键以表示域对象实例的所有者,以及是否允许 ACL 条目从任何父 ACL 继承。对于要为其存储 ACL 权限的每个域对象实例,我们只有一行。

  • 最后,ACL_ENTRY 存储分配给每个收件人的个人权限。列包括 ACL_OBJECT_IDENTITY 的外键,接收者(即 ACL_SID 的外键),是否进行审计以及代表实际权限被授予或拒绝的整数位掩码。对于每个接收到使用域对象的权限的收件人,我们只有一行。

如上一段所述,ACL 系统使用整数位掩码。不用担心,您不需要了解使用 ACL 系统的位转换的优点,但是只要说我们有 32 位可以打开或关闭就可以了。这些位中的每一个都代表一个权限,默认情况下,权限是读(位 0),写(位 1),创建(位 2),删除(位 3)和 Management(位 4)。如果您希望使用其他权限,则很容易实现自己的Permission实例,并且 ACL 框架的其余部分将在不了解扩展的情况下运行。

重要的是要了解,系统中域对象的数量与我们选择使用整数位掩码这一事实完全无关。虽然您有 32 位可用的权限,但您可能有数十亿个域对象实例(这意味着 ACL_OBJECT_IDENTITY 中的数十亿行,很可能是 ACL_ENTRY)。之所以说出这一点,是因为我们发现有时人们会误以为每个潜在的域对象都需要一点,事实并非如此。

现在,我们已经提供了有关 ACL 系统功能以及它在表结构中的外观的基本概述,让我们探索关键接口。关键接口是:

  • Acl:每个域对象只有一个Acl对象,该对象在内部保存AccessControlEntry,并且知道Acl的所有者。 Acl 不直接引用域对象,而是ObjectIdentityAcl存储在 ACL_OBJECT_IDENTITY 表中。

  • AccessControlEntryAcl包含多个AccessControlEntry,在框架中通常缩写为 ACE。每个 ACE 都引用PermissionSidAcl的特定 Tuples。 ACE 也可以是授予或不授予的,并且包含审核设置。 ACE 存储在 ACL_ENTRY 表中。

  • Permission:权限代表特定的不可变位掩码,并提供用于位掩码和输出信息的便捷功能。上面显示的基本权限(位 0 至 4)包含在BasePermission类中。

  • Sid:ACL 模块需要引用主体和GrantedAuthority[]Sid接口提供了间接级别,它是“安全身份”的缩写。常见的类包括PrincipalSid(以表示Authentication对象内的主体)和GrantedAuthoritySid。安全标识信息存储在 ACL_SID 表中。

  • ObjectIdentity:每个域对象在 ACL 模块内部由ObjectIdentity表示。默认实现称为ObjectIdentityImpl

  • AclService:检索适用于给定ObjectIdentityAcl。在包含的实现(JdbcAclService)中,检索操作委托给LookupStrategyLookupStrategy提供了一种高度优化的策略,用于使用批量检索((BasicLookupStrategy)来检索 ACL 信息,并支持利用实例化视图,分层查询和类似的以性能为中心的非 ANSI SQL 功能的自定义实现。

  • MutableAclService:允许显示经过修改的Acl以保持持久性。如果您不愿意,则不必使用此界面。

请注意,我们现成的 AclService 和相关数据库类均使用 ANSI SQL。因此,这应该适用于所有主要数据库。在撰写本文时,已使用 Hypersonic SQL,PostgreSQL,Microsoft SQL Server 和 Oracle 成功测试了该系统。

Spring Security 附带了两个示例,它们演示了 ACL 模块。第一个是联系人 samples,另一个是文档 Management 系统(DMS)samples。我们建议您看一下这些示例。

12.1.3 使用入门

要开始使用 Spring Security 的 ACL 功能,您需要将 ACL 信息存储在某个地方。这需要使用 Spring 实例化DataSource。然后将DataSource注入到JdbcMutableAclServiceBasicLookupStrategy实例中。后者提供高性能的 ACL 检索功能,而前者提供了 mutator 功能。有关示例配置,请参阅 Spring Security 附带的示例之一。您还需要使用上一节中列出的四个特定于 ACL 的表填充数据库(有关适当的 SQL 语句,请参阅 ACL 示例)。

创建所需的架构并实例化JdbcMutableAclService之后,接下来需要确保您的域模型支持与 Spring Security ACL 软件包的互操作性。希望ObjectIdentityImpl足够了,因为它提供了多种使用方式。大多数人将拥有包含public Serializable getId()方法的域对象。如果返回类型为 long 或与 long 兼容(例如 int),您将发现无需进一步考虑ObjectIdentity问题。 ACL 模块的许 Multipart 都依赖长标识符。如果您不使用 long(或 int,byte 等),则很有可能需要重新实现许多类。我们不打算在 Spring Security 的 ACL 模块中支持非长标识符,因为长已经与所有数据库序列(最常见的标识符数据类型)兼容,并且长度足以容纳所有常见的使用场景。

以下代码片段显示了如何创建Acl或修改现有的Acl

// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;

// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}

// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);

在上面的示例中,我们检索与标识号为 44 的“ Foo”域对象相关联的 ACL。然后,我们添加 ACE,以便名为“ Samantha”的主体可以“Management”该对象。除了 insertAce 方法外,代码片段是相对不言自明的。 insertAce 方法的第一个参数是确定新条目将在 Acl 中的哪个位置插入。在上面的示例中,我们只是将新 ACE 放在现有 ACE 的末尾。最后一个参数是布尔值,指示 ACE 是授予还是拒绝。在大多数情况下,它将被授予(true),但如果拒绝(false),则实际上将阻止该权限。

Spring Security 没有提供任何特殊的集成来自动创建,更新或删除 ACL,这是 DAO 或存储库操作的一部分。相反,您将需要为单个域对象编写如上所示的代码。值得考虑在服务层上使用 AOP 来自动将 ACL 信息与服务层操作集成在一起。过去,我们发现这是一种非常有效的方法。

一旦使用了上述技术在数据库中存储了一些 ACL 信息,下一步就是实际将 ACL 信息用作授权决策逻辑的一部分。您在这里有很多选择。您可以编写自己的AccessDecisionVoterAfterInvocationProvider,分别在方法调用之前或之后触发。这样的类将使用AclService检索相关的 ACL,然后调用Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)来确定是授予许可还是拒绝许可。或者,您可以使用AclEntryVoterAclEntryAfterInvocationProviderAclEntryAfterInvocationCollectionFilteringProvider类。所有这些类都提供了一种基于声明的方法来在运行时评估 ACL 信息,从而使您无需编写任何代码。请参考示例应用程序以学习如何使用这些类。

12.2 预身份验证方案

在某些情况下,您想使用 Spring Security 进行授权,但是在访问应用程序之前,某些外部系统已经对用户进行了可靠的身份验证。我们将这些情况称为“预身份验证”方案。示例包括 X.509,Siteminder 以及通过运行应用程序的 Java EE 容器进行的身份验证。使用预认证时,Spring Security 必须

  • 标识发出请求的用户。

  • 获取用户的权限。

详细信息将取决于外部身份验证机制。在 X.509 中,可以通过其证书信息来标识用户;在 Siteminder 中,可以通过 HTTP 请求 Headers 来标识用户。如果依赖于容器身份验证,将通过对传入的 HTTP 请求调用getUserPrincipal()方法来标识用户。在某些情况下,外部机制可能会为用户提供角色/权限信息,但在其他情况下,必须从单独的来源(例如UserDetailsService)获得权限。

12.2.1 预身份验证框架类

因为大多数预认证机制遵循相同的模式,所以 Spring Security 具有一组类,这些类提供了用于实现预认证的认证提供程序的内部框架。这消除了重复,并允许以结构化的方式添加新的实现,而不必从头开始编写所有内容。如果您想使用X.509 authentication之类的内容,则无需了解这些类,因为它已经具有一个名称空间配置选项,该选项更易于使用和入门。如果您需要使用显式的 Bean 配置或计划编写自己的实现,那么对所提供的实现如何工作的理解将非常有用。您将在org.springframework.security.web.authentication.preauth下找到类。我们仅在此处提供概述,因此您应该在适当的地方查阅 Javadoc 和源代码。

AbstractPreAuthenticatedProcessingFilter

此类将检查安全性上下文的当前内容,如果为空,它将尝试从 HTTP 请求中提取用户信息并将其提交给AuthenticationManager。子类重写以下方法来获取此信息:

protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);

protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);

调用完这些之后,过滤器将创建一个包含返回数据的PreAuthenticatedAuthenticationToken并将其提交以进行身份验证。这里的“身份验证”实际上只是意味着进一步处理以加载用户的权限,但是遵循标准的 Spring Security 身份验证体系结构。

像其他 Spring Security 身份验证过滤器一样,预身份验证过滤器具有authenticationDetailsSource属性,默认情况下会创建WebAuthenticationDetails对象,以在Authentication对象的details属性中存储其他信息,例如会话标识符和始发 IP 地址。如果可以从预身份验证机制获取用户角色信息,则数据也存储在此属性中,详细信息实现GrantedAuthoritiesContainer接口。这使身份验证提供程序可以读取从外部分配给用户的权限。接下来,我们将看一个具体示例。

J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource

如果过滤器配置有authenticationDetailsSource作为此类的实例,则通过为“Map 角色”的每个 sched 集合调用isUserInRole(String role)方法来获取权限信息。该类从已配置的MappableAttributesRetriever中获取它们。可能的实现包括在应用程序上下文中对列表进行硬编码,以及从web.xml文件中的<security-role>信息中读取角色信息。预认证示例应用程序使用后一种方法。

在另一个阶段,使用已配置的Attributes2GrantedAuthoritiesMapper将角色(或属性)Map 到 Spring Security GrantedAuthority对象。默认值只是在名称中添加通常的ROLE_前缀,但它使您可以完全控制行为。

PreAuthenticatedAuthenticationProvider

经过预身份验证的提供程序除了为用户加载UserDetails对象外,无所要做。它通过委派AuthenticationUserDetailsService来实现。后者与标准UserDetailsService类似,但采用Authentication对象,而不仅仅是用户名:

public interface AuthenticationUserDetailsService {
    UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}

该接口可能还有其他用途,但是具有预身份验证功能,它允许访问打包在Authentication对象中的授权机构,如上一节所述。 PreAuthenticatedGrantedAuthoritiesUserDetailsService类可以执行此操作。或者,它可以通过UserDetailsByNameServiceWrapper实现委派给标准UserDetailsService

Http403ForbiddenEntryPoint

technical overview章节讨论了AuthenticationEntryPoint。通常,它负责启动未经身份验证的用户的身份验证过程(当他们尝试访问受保护的资源时),但是在经过预先身份验证的情况下,这并不适用。如果您未将预身份验证与其他身份验证机制结合使用,则只能使用此类的实例配置ExceptionTranslationFilter。如果用户被AbstractPreAuthenticatedProcessingFilter拒绝而导致身份验证为空,则将调用此方法。如果被调用,它将始终返回403禁止响应代码。

12.2.2 具体实施

X.509 身份验证包含在其own chapter中。在这里,我们将看一些为其他预身份验证的场景提供支持的类。

请求 Headers 身份验证(Siteminder)

外部身份验证系统可以通过在 HTTP 请求上设置特定的 Headers 来向应用程序提供信息。一个著名的例子是 Siteminder,它在名为SM_USER的 Headers 中传递用户名。 RequestHeaderAuthenticationFilter类支持此机制,该类仅从 Headers 中提取用户名。默认情况下,使用名称SM_USER作为标题名称。有关更多详细信息,请参见 Javadoc。

Tip

请注意,当使用这样的系统时,框架完全不执行身份验证检查,因此,正确配置外部系统并保护对应用程序的所有访问非常重要。如果攻击者能够在未检测到原始请求的情况下伪造 Headers,则他们可能会选择所需的任何用户名。

Siteminder 示例配置

使用此过滤器的典型配置如下所示:

<security:http>
<!-- Additional http configuration omitted -->
<security:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
</security:http>

<bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="SM_USER"/>
<property name="authenticationManager" ref="authenticationManager" />
</bean>

<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
    <bean id="userDetailsServiceWrapper"
        class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <property name="userDetailsService" ref="userDetailsService"/>
    </bean>
</property>
</bean>

<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>

我们在这里假设security namespace用于配置。还假定您已在配置中添加了UserDetailsService(称为“ userDetailsService”)以加载用户的角色。

Java EE 容器认证

J2eePreAuthenticatedProcessingFilter类将从HttpServletRequestuserPrincipal属性中提取用户名。通常将此过滤器与名为“ J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource”的部分中所述的 Java EE 角色结合使用。

代码库中有一个使用此方法的示例应用程序,因此,如果有兴趣,可以从 github 上获取代码,并查看应用程序上下文文件。该代码位于samples/xml/preauth目录中。

12.3 LDAP 认证

12.3.1 Overview

LDAP 通常被组织用作用户信息的中央存储库和身份验证服务。它还可以用于存储应用程序用户的角色信息。

关于如何配置 LDAP 服务器,有许多不同的方案,因此 Spring Security 的 LDAP 提供程序是完全可配置的。它使用单独的策略接口进行身份验证和角色检索,并提供可以配置为处理各种情况的默认实现。

在尝试将其与 Spring Security 结合使用之前,您应该熟悉 LDAP。以下链接很好地介绍了相关概念,并提供了使用免费 LDAP 服务器 OpenLDAP 设置目录的指南:http://www.zytrax.com/books/ldap/。熟悉用于从 Java 访问 LDAP 的 JNDI API 可能也很有用。我们在 LDAP 提供程序中不使用任何第三方 LDAP 库(Mozilla,JLDAP 等),但是 Spring LDAP 被广泛使用,因此如果您计划添加自己的自定义项,则对该项目有些熟悉可能会很有用。

使用 LDAP 身份验证时,重要的是要确保正确配置 LDAP 连接池。如果您不熟悉此操作,可以参考Java LDAP 文档

12.3.2 将 LDAP 与 Spring Security 结合使用

Spring Security 中的 LDAP 认证可以大致分为以下几个阶段。

  • 从登录名获取唯一的 LDAP“专有名称”或 DN。除非事先知道用户名到 DN 的确切 Map,否则这通常意味着在目录中执行搜索。因此,用户在登录时可能会 Importing 名称“ joe”,但是用于验证 LDAP 的实际名称将是完整 DN,例如uid=joe,ou=users,dc=spring,dc=io

  • 通过“绑定”该用户或通过对该用户的密码与 DN 目录条目中的 password 属性进行远程“比较”操作来对用户进行身份验证。

  • 加载用户的权限列表。

exception 是仅使用 LDAP 目录检索用户信息并在本地对其进行身份验证。这可能是不可能的,因为目录经常被设置为具有诸如用户密码之类的属性的有限读取权限。

我们将在下面查看一些配置方案。有关可用配置选项的完整信息,请查阅安全名称空间架构(XML 编辑器中应提供该信息)。

12.3.3 配置 LDAP 服务器

您需要做的第一件事是配置要对其进行身份验证的服务器。这是使用安全名称空间中的<ldap-server>元素完成的。可以使用url属性将其配置为指向外部 LDAP 服务器:

<ldap-server url="ldap://springframework.org:389/dc=springframework,dc=org" />

使用嵌入式测试服务器

<ldap-server>元素还可用于创建嵌入式服务器,这对于测试和演示非常有用。在这种情况下,您可以不使用url属性来使用它:

<ldap-server root="dc=springframework,dc=org"/>

在这里,我们指定目录的根 DIT 应该为“ dc = springframework,dc = org”,这是默认设置。通过这种方式,名称空间解析器将创建一个嵌入式 Apache Directory 服务器并扫描 Classpath 中是否有任何 LDIF 文件,它将尝试将其加载到服务器中。您可以使用ldif属性来自定义此行为,该属性定义了要加载的 LDIF 资源:

<ldap-server ldif="classpath:users.ldif" />

这使使用 LDAP 进行安装和运行变得容易得多,因为在任何时候都无法与外部服务器一起工作。它还使用户免受连接 Apache Directory 服务器所需的复杂 bean 配置的影响。使用普通的 Spring Beans,配置将更加混乱。您必须具有必要的 Apache Directory 依赖项 jar,供应用程序使用。这些可以从 LDAP 示例应用程序获得。

使用绑定身份验证

这是最常见的 LDAP 身份验证方案。

<ldap-authentication-provider user-dn-pattern="uid={0},ou=people"/>

这个简单的示例将通过使用提供的模式替换用户登录名并尝试使用该登录密码将该用户绑定来获取该用户的 DN。如果所有用户都存储在目录中的单个节点下,则可以。相反,如果您希望配置 LDAP 搜索过滤器来定位用户,则可以使用以下方法:

<ldap-authentication-provider user-search-filter="(uid={0})"
    user-search-base="ou=people"/>

如果与上面的服务器定义一起使用,这将使用user-search-filter属性的值作为过滤器在 DN ou=people,dc=springframework,dc=org下执行搜索。再次用用户登录名代替过滤器名称中的参数,因此它将搜索uid属性等于用户名的条目。如果未提供user-search-base,则将从根目录执行搜索。

Loading Authorities

通过以下属性控制如何从 LDAP 目录中的组加载权限。

  • group-search-base。定义目录树的一部分,应在该部分下执行组搜索。

  • group-role-attribute。该属性包含组条目定义的权限名称。默认为cn

  • group-search-filter。用于搜索组成员身份的过滤器。默认值为uniqueMember={0},对应于groupOfUniqueNames LDAP 类[19]。在这种情况下,替换参数是用户的完整专有名称。如果要过滤登录名,可以使用参数{1}

因此,如果我们使用以下配置

<ldap-authentication-provider user-dn-pattern="uid={0},ou=people"
        group-search-base="ou=groups" />

并成功验证为用户“ ben”,随后的权限加载将在目录条目ou=groups,dc=springframework,dc=org下执行搜索,以查找包含值为uid=ben,ou=people,dc=springframework,dc=org的属性uniqueMember的条目。默认情况下,权限名称将带有前缀ROLE_。您可以使用role-prefix属性更改此设置。如果您不需要任何前缀,请使用role-prefix="none"。有关加载权限的更多信息,请参见DefaultLdapAuthoritiesPopulator类的 Javadoc。

12.3.4 实现类

与显式使用 Spring Bean 相比,我们上面使用的名称空间配置选项易于使用并且更加简洁。在某些情况下,您可能需要了解如何在应用程序上下文中直接配置 Spring Security LDAP。例如,您可能希望自定义某些类的行为。如果您对使用名称空间配置感到满意,则可以跳过本节和下一节。

主要的 LDAP 提供程序类LdapAuthenticationProvider实际上并不会做很多事情,而是将工作委托给另外两个 bean,分别是LdapAuthenticatorLdapAuthoritiesPopulator,这两个 bean 分别负责认证用户和检索用户的GrantedAuthority集合。

LdapAuthenticator Implementations

验证者还负责检索任何必需的用户属性。这是因为对属性的权限可能取决于所使用的身份验证类型。例如,如果以用户身份进行绑定,则可能有必要使用用户自己的权限来读取它们。

Spring Security 提供了两种身份验证策略:

  • 直接对 LDAP 服务器进行身份验证(“绑定”身份验证)。

  • 密码比较,将用户提供的密码与存储库中存储的密码进行比较。可以通过检索 password 属性的值并在本地对其进行检查来完成此操作,也可以通过执行 LDAP“比较”操作来完成,在该操作中,将提供的密码传递给服务器进行比较,并且永远不会检索到真实的密码值。

Common Functionality

在可能(通过任何一种策略)对用户进行身份验证之前,必须从提供给应用程序的登录名中获得专有名称(DN)。这可以通过简单的模式匹配(通过设置setUserDnPatterns数组属性)或通过设置userSearch属性来完成。对于 DN 模式匹配方法,使用标准 Java 模式格式,并且登录名将替换{0}参数。该模式应相对于已配置的SpringSecurityContextSource绑定到的 DN(有关此信息,请参阅连接到 LDAP 服务器的部分)。例如,如果您使用具有ldap://monkeymachine.co.uk/dc=springframework,dc=org URL 的 LDAP 服务器,并且具有uid={0},ou=greatapes模式,则登录名“大猩猩”将 Map 到 DN uid=gorilla,ou=greatapes,dc=springframework,dc=org。依次尝试每个已配置的 DN 模式,直到找到匹配项。有关使用搜索的信息,请参见下面的search objects部分。也可以使用两种方法的组合-首先检查模式,如果找不到匹配的 DN,将使用搜索。

BindAuthenticator

org.springframework.security.ldap.authentication中的类BindAuthenticator实现了绑定身份验证策略。它只是尝试以用户身份进行绑定。

PasswordComparisonAuthenticator

PasswordComparisonAuthenticator类实现密码比较身份验证策略。

连接到 LDAP 服务器

上面讨论的 Bean 必须能够连接到服务器。它们都必须带有SpringSecurityContextSource,这是 Spring LDAP ContextSource的扩展。除非有特殊要求,否则通常将配置一个DefaultSpringSecurityContextSource bean,该 bean 可配置为使用 LDAP 服务器的 URL,也可以配置为“Management 者”用户的用户名和密码,默认情况下,绑定到服务器时将使用该用户名和密码(而不是匿名绑定)。有关更多信息,请阅读此类的 Javadoc 和 Spring LDAP 的AbstractContextSource

LDAP 搜索对象

在目录中定位用户条目时,通常需要比简单的 DN 匹配更复杂的策略。可以将其封装在LdapUserSearch实例中,该实例可以提供给身份验证器实现,例如,以允许他们找到用户。提供的实现是FilterBasedLdapUserSearch

FilterBasedLdapUserSearch

该 bean 使用 LDAP 过滤器来匹配目录中的用户对象。 Javadoc 中针对JDK DirContext 类上的相应搜索方法说明了该过程。如此处所述,可以为搜索过滤器提供参数。对于此类,唯一有效的参数是{0},它将替换为用户的登录名。

LdapAuthoritiesPopulator

成功验证用户身份后,LdapAuthenticationProvider将通过调用已配置的LdapAuthoritiesPopulator bean 尝试为用户加载一组权限。 DefaultLdapAuthoritiesPopulator是一种实现,它将通过在目录中搜索用户所属的组来加载权限(通常,这些组将是目录中的groupOfNamesgroupOfUniqueNames条目)。有关此类的更多详细信息,请查阅 Javadoc。

如果您只想使用 LDAP 进行身份验证,但从其他来源(例如数据库)加载授权,则可以提供自己的接口实现,然后注入该接口。

Spring Bean 配置

使用我们在这里讨论过的一些 bean 的典型配置可能如下所示:

<bean id="contextSource"
        class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<constructor-arg value="ldap://monkeymachine:389/dc=springframework,dc=org"/>
<property name="userDn" value="cn=manager,dc=springframework,dc=org"/>
<property name="password" value="password"/>
</bean>

<bean id="ldapAuthProvider"
    class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
<constructor-arg>
<bean class="org.springframework.security.ldap.authentication.BindAuthenticator">
    <constructor-arg ref="contextSource"/>
    <property name="userDnPatterns">
    <list><value>uid={0},ou=people</value></list>
    </property>
</bean>
</constructor-arg>
<constructor-arg>
<bean
    class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
    <constructor-arg ref="contextSource"/>
    <constructor-arg value="ou=groups"/>
    <property name="groupRoleAttribute" value="ou"/>
</bean>
</constructor-arg>
</bean>

这将设置提供程序以使用 URL ldap://monkeymachine:389/dc=springframework,dc=org访问 LDAP 服务器。将通过尝试与 DN uid=<user-login-name>,ou=people,dc=springframework,dc=org绑定来执行身份验证。成功通过身份验证后,将通过使用默认过滤器(member=<user's-DN>)在 DN ou=groups,dc=springframework,dc=org下搜索来将角色分配给用户。角色名称将从每个匹配项的“ ou”属性中获取。

要配置使用过滤器(uid=<user-login-name>)代替 DN 模式(或除此之外)的用户搜索对象,您需要配置以下 bean

<bean id="userSearch"
    class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg index="0" value=""/>
<constructor-arg index="1" value="(uid={0})"/>
<constructor-arg index="2" ref="contextSource" />
</bean>

并通过设置BindAuthenticator bean 的userSearch属性来使用它。然后,在尝试以该用户身份进行绑定之前,验证者将调用搜索对象以获得正确的用户 DN。

LDAP 属性和自定义的用户详细信息

使用LdapAuthenticationProvider进行身份验证的最终结果与使用标准UserDetailsService接口的常规 Spring Security 身份验证相同。创建一个UserDetails对象并将其存储在返回的Authentication对象中。与使用UserDetailsService一样,一个共同的要求是能够自定义此实现并添加其他属性。使用 LDAP 时,这些通常是用户条目中的属性。 UserDetails对象的创建由提供者的UserDetailsContextMapper策略控制,该策略负责将用户对象与 LDAP 上下文数据进行 Map:

public interface UserDetailsContextMapper {

UserDetails mapUserFromContext(DirContextOperations ctx, String username,
        Collection<GrantedAuthority> authorities);

void mapUserToContext(UserDetails user, DirContextAdapter ctx);
}

仅第一种方法与身份验证有关。如果提供此接口的实现并将其注入LdapAuthenticationProvider,则可以完全控制如何创建 UserDetails 对象。第一个参数是 Spring LDAP DirContextOperations的实例,该实例使您可以访问在身份验证期间加载的 LDAP 属性。 username参数是用于认证的名称,最后一个参数是配置的LdapAuthoritiesPopulator为用户加载的权限的集合。

上下文数据的加载方式根据您所使用的身份验证类型而略有不同。使用BindAuthenticator时,将从绑定操作返回的上下文用于读取属性,否则将使用从配置的ContextSource获得的标准上下文读取数据(当配置搜索以定位用户时,这将是数据由搜索对象返回)。

12.3.5 Active Directory 身份验证

Active Directory 支持其自己的非标准身份验证选项,并且正常使用模式不太适合标准LdapAuthenticationProvider。通常,身份验证是使用域用户名(格式为[email protected])而不是使用 LDAP 可分辨名称来执行的。为了简化此操作,Spring Security 3.1 具有一个身份验证提供程序,该身份验证提供程序是针对典型的 Active Directory 设置而定制的。

ActiveDirectoryLdapAuthenticationProvider

配置ActiveDirectoryLdapAuthenticationProvider非常简单。您只需要提供域名和 LDAP URL,即可提供服务器[20]的地址。然后,示例配置如下所示:

<bean id="adAuthenticationProvider"
class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <constructor-arg value="mydomain.com" />
    <constructor-arg value="ldap://adserver.mydomain.com/" />
</bean>
}

请注意,无需指定单独的ContextSource即可定义服务器位置-Bean 是完全独立的。例如,名为“ Sharon”的用户将能够通过 Importing 用户名sharon或完整的 Active Directory userPrincipalName(即[email protected])进行身份验证。然后将定位用户的目录条目,并返回属性以供可能使用以自定义创建的UserDetails对象(可以为此目的注入UserDetailsContextMapper)。与目录的所有交互都以用户本身的身份进行。没有“Manager”用户的概念。

默认情况下,从用户条目的memberOf属性值获得用户权限。可以再次使用UserDetailsContextMapper定制分配给用户的权限。您还可以将GrantedAuthoritiesMapper注入提供程序实例以控制最终在Authentication对象中的权限。

Active Directory 错误代码

默认情况下,失败的结果将导致标准的 Spring Security BadCredentialsException。如果将属性convertSubErrorCodesToExceptions设置为true,则将解析异常消息,以尝试提取特定于 Active Directory 的错误代码并引发更特定的异常。检查类 Javadoc 以获取更多信息。

12.4 OAuth 2.0 登录—高级配置

HttpSecurity.oauth2Login()提供了许多用于自定义 OAuth 2.0 登录名的配置选项。主要配置选项分为它们的协议端点对应项。

例如,oauth2Login().authorizationEndpoint()允许配置* Authorization Endpoint ,而oauth2Login().tokenEndpoint()允许配置 Token Endpoint *。

以下代码显示了一个示例:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .authorizationEndpoint()
                    ...
                .redirectionEndpoint()
                    ...
                .tokenEndpoint()
                    ...
                .userInfoEndpoint()
                    ...
    }
}

oauth2Login() DSL 的主要目标是与规范中定义的命名保持一致。

OAuth 2.0 授权框架如下定义Protocol Endpoints

授权过程利用两个授权服务器端点(HTTP 资源):

  • 授权端点:Client 端用于通过用户代理重定向从资源所有者获取授权。

  • 令牌端点:Client 端用于交换访问令牌的授权授权,通常与 Client 端身份验证交换。

以及一个 Client 端端点:

  • 重定向端点:由授权服务器用于通过资源所有者用户代理将包含授权证书的响应返回给 Client 端。

OpenID Connect Core 1.0 规范对UserInfo Endpoint的定义如下:

UserInfo 端点是 OAuth 2.0 保护的资源,可返回有关经过身份验证的最终用户的声明。为了获得有关最终用户的请求的声明,Client 端使用通过 OpenID Connect Authentication 获得的访问令牌向 UserInfo 端点发出请求。这些声明通常由 JSON 对象表示,该对象包含声明的名称/值对的集合。

以下代码显示了适用于oauth2Login() DSL 的完整配置选项:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .clientRegistrationRepository(this.clientRegistrationRepository())
                .authorizedClientRepository(this.authorizedClientRepository())
                .authorizedClientService(this.authorizedClientService())
                .loginPage("/login")
                .authorizationEndpoint()
                    .baseUri(this.authorizationRequestBaseUri())
                    .authorizationRequestRepository(this.authorizationRequestRepository())
                    .authorizationRequestResolver(this.authorizationRequestResolver())
                    .and()
                .redirectionEndpoint()
                    .baseUri(this.authorizationResponseBaseUri())
                    .and()
                .tokenEndpoint()
                    .accessTokenResponseClient(this.accessTokenResponseClient())
                    .and()
                .userInfoEndpoint()
                    .userAuthoritiesMapper(this.userAuthoritiesMapper())
                    .userService(this.oauth2UserService())
                    .oidcUserService(this.oidcUserService())
                    .customUserType(GitHubOAuth2User.class, "github");
    }
}

以下各节详细介绍了每个可用的配置选项:

12.4.1 OAuth 2.0 登录页面

默认情况下,DefaultLoginPageGeneratingFilter自动生成 OAuth 2.0 登录页面。默认登录页面显示每个已配置的 OAuthClient 端及其ClientRegistration.clientName作为链接,该 Client 端能够启动授权请求(或 OAuth 2.0 登录)。

每个 OAuthClient 端的链接目的地默认为以下位置:

OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI“ // {}”

下面的行显示了一个示例:

<a href="/oauth2/authorization/google">Google</a>

要覆盖默认的登录页面,请配置oauth2Login().loginPage()和(可选)oauth2Login().authorizationEndpoint().baseUri()

以下清单显示了一个示例:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .loginPage("/login/oauth2")
                ...
                .authorizationEndpoint()
                    .baseUri("/login/oauth2/authorization")
                    ....
    }
}

Tip

您需要为@Controller提供@RequestMapping("/login/oauth2"),以便能够呈现自定义登录页面。

Tip

如前所述,配置oauth2Login().authorizationEndpoint().baseUri()是可选的。但是,如果您选择自定义它,请确保每个 OAuthClient 端的链接都与authorizationEndpoint().baseUri()匹配。

下面的行显示了一个示例:

<a href="/login/oauth2/authorization/google">Google</a>

12.4.2 重定向端点

重定向端点由授权服务器用于通过资源所有者用户代理将授权响应(包含授权凭证)返回给 Client 端。

Tip

OAuth 2.0 登录利用授权码授予。因此,授权凭证是授权代码。

默认的授权响应baseUri(重定向端点)为/login/oauth2/code/*,在OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI中定义。

如果要自定义“授权响应” baseUri,请按以下示例所示进行配置:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .redirectionEndpoint()
                    .baseUri("/login/oauth2/callback/*")
                    ....
    }
}

Tip

您还需要确保ClientRegistration.redirectUriTemplate与自定义的授权响应baseUri匹配。

以下清单显示了一个示例:

return CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.redirectUriTemplate("{baseUrl}/login/oauth2/callback/{registrationId}")
.build();

12.4.3 UserInfo 端点

UserInfo 端点包括许多配置选项,如以下小节所述:

Map 用户权限

在用户成功通过 OAuth 2.0 提供程序进行身份验证之后,OAuth2User.getAuthorities()(或OidcUser.getAuthorities())可以 Map 到一组新的GrantedAuthority实例,在完成身份验证后将提供给OAuth2AuthenticationToken

Tip

OAuth2AuthenticationToken.getAuthorities()用于授权请求,例如hasRole('USER')hasRole('ADMIN')中的请求。

Map 用户权限时,有两个选项可供选择:

使用 GrantedAuthoritiesMapper

提供GrantedAuthoritiesMapper的实现并按以下示例所示进行配置:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .userInfoEndpoint()
                    .userAuthoritiesMapper(this.userAuthoritiesMapper())
                    ...
    }

    private GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;

                    OidcIdToken idToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();

                    // Map the claims found in idToken and/or userInfo
                    // to one or more GrantedAuthority's and add it to mappedAuthorities

                } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                    OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;

                    Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                    // Map the attributes found in userAttributes
                    // to one or more GrantedAuthority's and add it to mappedAuthorities

                }
            });

            return mappedAuthorities;
        };
    }
}

或者,您可以注册GrantedAuthoritiesMapper @Bean使其自动应用于配置,如以下示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.oauth2Login();
    }

    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        ...
    }
}
使用 OAuth2UserService 的基于委派的策略

与使用GrantedAuthoritiesMapper相比,此策略是高级的,但是,它也更灵活,因为它使您可以访问OAuth2UserRequestOAuth2User(使用 OAuth 2.0 UserService 时)或OidcUserRequestOidcUser(使用 OpenID Connect 1.0 UserService 时)。

OAuth2UserRequest(和OidcUserRequest)使您可以访问关联的OAuth2AccessToken,这在* delegator *需要从受保护资源中获取权限信息才能为其 Map 用户的自定义权限时非常有用。

以下示例显示如何使用 OpenID Connect 1.0 UserService 实施和配置基于委派的策略:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .userInfoEndpoint()
                    .oidcUserService(this.oidcUserService())
                    ...
    }

    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        final OidcUserService delegate = new OidcUserService();

        return (userRequest) -> {
            // Delegate to the default implementation for loading a user
            OidcUser oidcUser = delegate.loadUser(userRequest);

            OAuth2AccessToken accessToken = userRequest.getAccessToken();
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            // TODO
            // 1) Fetch the authority information from the protected resource using accessToken
            // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

            // 3) Create a copy of oidcUser but use the mappedAuthorities instead
            oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

            return oidcUser;
        };
    }
}

配置自定义 OAuth2 用户

CustomUserTypesOAuth2UserServiceOAuth2UserService的实现,为自定义OAuth2User类型提供支持。

如果默认实现(DefaultOAuth2User)不适合您的需要,则可以定义自己的OAuth2User实现。

以下代码演示了如何为 GitHub 注册自定义OAuth2User类型:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .userInfoEndpoint()
                    .customUserType(GitHubOAuth2User.class, "github")
                    ...
    }
}

以下代码显示了 GitHub 的自定义OAuth2User类型的示例:

public class GitHubOAuth2User implements OAuth2User {
    private List<GrantedAuthority> authorities =
        AuthorityUtils.createAuthorityList("ROLE_USER");
    private Map<String, Object> attributes;
    private String id;
    private String name;
    private String login;
    private String email;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public Map<String, Object> getAttributes() {
        if (this.attributes == null) {
            this.attributes = new HashMap<>();
            this.attributes.put("id", this.getId());
            this.attributes.put("name", this.getName());
            this.attributes.put("login", this.getLogin());
            this.attributes.put("email", this.getEmail());
        }
        return attributes;
    }

    public String getId() {
        return this.id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLogin() {
        return this.login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getEmail() {
        return this.email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Tip

idnameloginemail是 GitHub 的 UserInfo Response 中返回的属性。有关从 UserInfo 端点返回的详细信息,请参阅“获取经过身份验证的用户”的 API 文档。

OAuth 2.0 UserService

DefaultOAuth2UserServiceOAuth2UserService的实现,支持标准的 OAuth 2.0 提供程序。

Note

OAuth2UserService从 UserInfo 端点获取最终用户(资源所有者)的用户属性(通过使用在授权流程中授予 Client 端的访问令牌),并以OAuth2User的形式返回AuthenticatedPrincipal

在 UserInfo 端点上请求用户属性时,DefaultOAuth2UserService使用RestOperations

如果需要自定义 UserInfo Request 的预处理,则可以为DefaultOAuth2UserService.setRequestEntityConverter()提供自定义Converter<OAuth2UserRequest, RequestEntity<?>>。默认实现OAuth2UserRequestEntityConverter构建 UserInfo 请求的RequestEntity表示形式,默认情况下在AuthorizationHeaders 中设置OAuth2AccessToken

另一方面,如果您需要自定义 UserInfo Response 的后处理,则需要为DefaultOAuth2UserService.setRestOperations()提供自定义配置的RestOperations。默认RestOperations配置如下:

RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

OAuth2ErrorResponseErrorHandlerResponseErrorHandler,可以处理 OAuth 2.0 错误(400 错误请求)。它使用OAuth2ErrorHttpMessageConverter将 OAuth 2.0 错误参数转换为OAuth2Error

无论您是自定义DefaultOAuth2UserService还是提供自己的OAuth2UserService的实现,都需要按以下示例所示进行配置:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .userInfoEndpoint()
                    .userService(this.oauth2UserService())
                    ...
    }

    private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
        ...
    }
}

OpenID Connect 1.0 UserService

OidcUserService是支持 OpenID Connect 1.0 提供程序的OAuth2UserService的实现。

在 UserInfo 端点上请求用户属性时,OidcUserService利用DefaultOAuth2UserService

如果您需要自定义 UserInfo 请求的预处理和/或 UserInfo Response 的后处理,则需要为OidcUserService.setOauth2UserService()提供自定义配置的DefaultOAuth2UserService

无论您是为 OpenID Connect 1.0 提供程序自定义OidcUserService还是提供自己的OAuth2UserService的实现,都需要对其进行配置,如以下示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login()
                .userInfoEndpoint()
                    .oidcUserService(this.oidcUserService())
                    ...
    }

    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        ...
    }
}

[19]请注意,这与使用member={0}的基础DefaultLdapAuthoritiesPopulator的默认配置不同。

[20]也可以使用 DNS 查找来获取服务器的 IP 地址。目前尚不支持此功能,但希望会在将来的版本中提供。