10. Core Services

现在,我们已经对 Spring Security 体系结构及其核心类进行了高级概述,让我们仔细研究一下一个或两个核心接口及其实现,尤其是AuthenticationManagerUserDetailsServiceAccessDecisionManager。这些在本文档的其余部分中会定期出现,因此了解它们的配置方式和操作方式非常重要。

10.1 AuthenticationManager,ProviderManager 和 AuthenticationProvider

AuthenticationManager只是一个接口,因此实现可以是我们选择的任何东西,但是实际上它如何工作?如果我们需要检查多个身份验证数据库或不同身份验证服务(例如数据库和 LDAP 服务器)的组合,该怎么办?

Spring Security 中的默认实现称为ProviderManager,而不是处理身份验证请求本身,它委派给已配置的AuthenticationProvider s 列表,依次查询每个AuthenticationProvider s 以查看其是否可以执行认证。每个提供程序都将引发异常或返回完全填充的Authentication对象。还记得我们的好朋友UserDetailsUserDetailsService吗?如果没有,请返回上一章并刷新您的 Memory。验证身份验证请求的最常见方法是加载相应的UserDetails并对照用户 Importing 的密码检查加载的密码。这是DaoAuthenticationProvider使用的方法(请参见下文)。构建完整填充的Authentication对象(从成功的身份验证返回并存储在SecurityContext中)时,将使用已加载的UserDetails对象-尤其是其中包含的GrantedAuthority

如果使用名称空间,则在内部创建并维护ProviderManager的实例,然后使用名称空间身份验证提供程序元素将提供程序添加到该名称(请参见命名空间一章)。在这种情况下,您不应在应用程序上下文中声明ProviderManager bean。但是,如果您不使用名称空间,则可以这样声明:

<bean id="authenticationManager"
		class="org.springframework.security.authentication.ProviderManager">
	<constructor-arg>
		<list>
			<ref local="daoAuthenticationProvider"/>
			<ref local="anonymousAuthenticationProvider"/>
			<ref local="ldapAuthenticationProvider"/>
		</list>
	</constructor-arg>
</bean>

在上面的示例中,我们有三个提供程序。按照显示的 Sequences 尝试使用它们(使用List暗示),每个提供程序都可以尝试进行身份验证,或者仅返回null即可跳过身份验证。如果所有实现都返回 null,则ProviderManager将抛出ProviderNotFoundException。如果您想了解有关链接提供程序的更多信息,请参阅ProviderManager Javadoc。

诸如 Web 表单登录处理过滤器之类的身份验证机制将注入对ProviderManager的引用,并将调用该ProviderManager以处理其身份验证请求。您所需的提供程序有时可以与身份验证机制互换,而在其他时候,它们将取决于特定的身份验证机制。例如,DaoAuthenticationProviderLdapAuthenticationProvider与提交简单的用户名/密码身份验证请求的任何机制兼容,因此将与基于表单的登录名或 HTTP Basic 身份验证一起使用。另一方面,某些身份验证机制会创建只能由AuthenticationProvider的单一类型解释的身份验证请求对象。一个示例就是 JA-SIG CAS,它使用服务票证的概念,因此只能由CasAuthenticationProvider进行身份验证。您不必太担心这一点,因为如果您忘记注册合适的提供程序,则在尝试进行身份验证时,您只会收到ProviderNotFoundException

10.1.1 清除成功认证时的凭据

默认情况下(从 Spring Security 3.1 开始),ProviderManager将尝试从Authentication对象中清除所有敏感的凭据信息,该信息由成功的身份验证请求返回。这样可以防止将密码之类的信息保留的时间过长。

例如,在使用用户对象的缓存来提高 Stateless 应用程序的性能时,这可能会导致问题。如果Authentication包含对缓存中某个对象(例如UserDetails实例)的引用,并且其凭据已删除,则将无法再针对缓存的值进行身份验证。如果使用缓存,则需要考虑到这一点。一个明显的解决方案是首先在高速缓存实现中或在AuthenticationProvider中创建对象的副本,后者创建返回的Authentication对象。或者,您可以禁用ProviderManagereraseCredentialsAfterAuthentication属性。有关更多信息,请参见 Javadoc。

10.1.2 DaoAuthenticationProvider

Spring Security 实现的最简单的AuthenticationProviderDaoAuthenticationProvider,也是框架最早支持的之一。它利用UserDetailsService(作为 DAO)来查找用户名,密码和GrantedAuthority。只需将UsernamePasswordAuthenticationToken中提交的密码与UserDetailsService加载的密码进行比较,即可对用户进行身份验证。配置提供程序非常简单:

<bean id="daoAuthenticationProvider"
	class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

PasswordEncoder是可选的。 PasswordEncoder提供对从已配置UserDetailsService返回的UserDetails对象中提供的密码的编码和解码。 below将对此进行更详细的讨论。

10.2 UserDetailsService 的实现

如本参考指南前面所述,大多数身份验证提供程序都使用UserDetailsUserDetailsService接口。回想一下UserDetailsService的 Contract 是一种方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

返回的UserDetails是提供获取器的接口,该获取器保证以非空方式提供身份验证信息,例如用户名,密码,授予的权限以及用户帐户是启用还是禁用。即使用户名和密码实际上并未用作身份验证决策的一部分,大多数身份验证提供程序也会使用UserDetailsService。他们可能仅将返回的UserDetails对象用于GrantedAuthority信息,因为某些其他系统(例如 LDAP 或 X.509 或 CAS 等)承担了实际验证凭据的责任。

鉴于UserDetailsService实施起来非常简单,因此用户应该使用自己选择的持久性策略轻松检索身份验证信息。话虽如此,Spring Security 确实包含一些有用的基本实现,我们将在下面进行介绍。

10.2.1 内存中身份验证

创建自定义UserDetailsService实现易于使用,该实现从所选的持久性引擎中提取信息,但是许多应用程序不需要这种复杂性。如果您正在构建原型应用程序或刚开始集成 Spring Security,而又不想 true 花费时间配置数据库或编写UserDetailsService实现,则尤其如此。对于这种情况,一个简单的选择是使用安全性namespace中的user-service元素:

<user-service id="userDetailsService">
<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="bobspassword" authorities="ROLE_USER" />
</user-service>

这也支持使用外部属性文件:

<user-service id="userDetailsService" properties="users.properties"/>

属性文件应包含以下形式的条目

username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]

For example

jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled

10.2.2 JdbcDaoImpl

Spring Security 还包括一个UserDetailsService,该UserDetailsService可以从 JDBC 数据源获取身份验证信息。在内部使用 Spring JDBC,因此避免了仅用于存储用户详细信息的功能齐全的对象关系 Map 器(ORM)的复杂性。如果您的应用程序确实使用了 ORM 工具,则您可能更愿意编写自定义UserDetailsService来重用您可能已经创建的 Map 文件。返回JdbcDaoImpl,示例配置如下所示:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<bean id="userDetailsService"
	class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>

您可以通过修改上面显示的DriverManagerDataSource来使用不同的关系数据库 Management 系统。您还可以使用从 JNDI 获得的全局数据源,就像其他任何 Spring 配置一样。

Authority Groups

默认情况下,JdbcDaoImpl假定单个权限直接 Map 到用户(请参见数据库架构附录),从而为单个用户加载权限。另一种方法是将权限划分为组,然后将组分配给用户。有些人喜欢使用这种方法来 Management 用户权限。有关如何启用组权限的更多信息,请参见JdbcDaoImpl Javadoc。组模式也包含在附录中。

10.3 密码编码

Spring Security 的PasswordEncoder接口用于支持以永久存储方式以某种方式编码的密码。永远不要以纯文本形式存储密码。始终使用诸如 bcrypt 之类的单向密码哈希算法,该算法使用的内置盐值对于每个存储的密码都不同。不要使用普通的哈希函数,例如 MD5 或 SHA,甚至不要使用盐版。 Bcrypt 故意设计得很慢,并且会阻止脱机密码破解,而标准的哈希算法速度很快,可以轻松地用于在自定义硬件上并行测试数千个密码。您可能会认为这不适用于您,因为您的密码数据库是安全的,并且脱机攻击也没有风险。如果是这样,请对所有以这种方式受到破坏并且因不安全地存储密码而受到 Mock 的知名网站进行一些研究和阅读。最好是安全起见。使用org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"是安全的好选择。其他通用编程语言中也有兼容的实现,因此它也是互操作性的不错选择。

如果您使用的是已经具有哈希密码的旧系统,那么您将需要使用与您的当前算法匹配的编码器,至少直到您可以将用户迁移到更安全的方案之前(通常这将涉及要求用户进行设置)新密码,因为哈希是不可逆的)。 Spring Security 有一个包含旧密码编码实现的软件包,即org.springframework.security.authentication.encodingDaoAuthenticationProvider可以注入新的或旧的PasswordEncoder类型。

10.3.1 什么是哈希?

密码哈希不是 Spring Security 所独有的,但对于不熟悉该概念的用户来说,是常见的混淆源。哈希(或摘要)算法是一种单向函数,可从某些 Importing 数据(例如密码)中生成一段固定长度的输出数据(哈希)。例如,字符串“ password”(以十六进制表示)的 MD5 哈希为

5f4dcc3b5aa765d61d8327deb882cf99

哈希是“单向的”,即很难(实际上是不可能)在给定哈希值的情况下获取原始 Importing,或者实际上会产生该哈希值的任何可能的 Importing。此属性使哈希值对于身份验证非常有用。可以将它们存储在用户数据库中,以替代纯文本密码,即使这些值被破坏,它们也不会立即显示可用于登录的密码。请注意,这也意味着密码一旦编码就无法恢复。

10.3.2 向哈希添加盐

使用密码散列的一个潜在问题是,如果将通用词用于 Importing,则相对容易解决散列的单向属性。人们倾向于选择类似的密码,而这些密码从以前被黑客入侵的网站中都有大量词典可在线获得。例如,如果您使用 google 搜索哈希值5f4dcc3b5aa765d61d8327deb882cf99,您将很快找到原始单词“ password”。以类似的方式,攻击者可以从标准单词列表构建哈希字典,然后使用该字典来查找原始密码。防止这种情况发生的一种方法是拥有适当的密码策略,以尝试防止使用常用词。另一种是在计算哈希值时使用“盐”。这是每个用户的已知数据的附加字符串,在计算哈希值之前将其与密码结合在一起。理想情况下,数据应尽可能地随机,但实际上,任何盐值通常比没有盐更好。使用盐意味着攻击者必须针对每个盐值构建单独的哈希字典,从而使攻击更加复杂(但并非不可能)。

Bcrypt 在对每个密码进行编码时会自动为它生成一个随机的盐值,并将其以标准格式存储在 bcrypt 字符串中。

Note

处理盐的传统方法是将SaltSource注入DaoAuthenticationProvider,这将为特定用户获取盐值并将其传递给PasswordEncoder。使用 bcrypt 意味着您不必担心盐处理的详细信息(例如值的存储位置),因为所有这些操作都是在内部完成的。因此,我们强烈建议您使用 bcrypt,除非您已经拥有一个单独存储盐的系统。

10.3.3 哈希和身份验证

当身份验证提供者(例如 Spring Security 的DaoAuthenticationProvider)需要对照用户的已知值检查提交的身份验证请求中的密码,并且以某种方式对存储的密码进行编码时,则必须使用完全相同的密码对提交的值进行编码算法。由于 Spring Security 无法控制持久值,因此您需要检查它们是否兼容。如果您在 Spring Security 中将密码哈希添加到身份验证配置中,并且数据库包含纯文本密码,则身份验证无法成功。例如,即使您知道您的数据库正在使用 MD5 对密码进行编码,并且您的应用程序配置为使用 Spring Security 的Md5PasswordEncoder,仍然有些地方会出错。例如,当编码器使用十六进制字符串(默认值)时,数据库可能具有在 Base 64 中编码的密码。另外,您的数据库可能使用大写字母,而编码器的输出是小写字母。在 continue 尝试通过应用程序进行身份验证之前,请确保编写测试以检查配置的密码编码器的输出是否具有已知的密码和盐组合,并检查其是否与数据库值匹配。使用像 bcrypt 这样的标准可以避免这些问题。

如果要直接使用 Java 生成编码后的密码以存储在用户数据库中,则可以使用PasswordEncoder上的encode方法。

10.4 Jackson 支持

Spring Security 已添加了 Jackson 支持来持久化与 Spring Security 相关的类。使用分布式会话(即会话复制,Spring Session 等)时,这可以提高序列化与 Spring Security 相关的类的性能。

要使用它,请将JacksonJacksonModules.getModules(ClassLoader)注册为Jackson Modules

ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);

// ... use ObjectMapper as normally ...
SecurityContext context = new SecurityContextImpl();
// ...
String json = mapper.writeValueAsString(context);