2. 核心服务
既然你对Spring Security的架构以及它的核心类有了更高的认识,让我们更近一步来看看其中的一两个核心接口和实现,尤其是"AuthenticationManager " ,"UserDetailsService " 和“AccessDecisionManager ”。这些经常出现在本文档的其余部分所以知道它们是如何配置及如何操作的是很重要的。
2.1. AuthenticationManager,ProviderManager 和 AuthenticationProvider
AuthenticationManager 只是一个接口,因此我们可以自由选择如何实现,但是实际中它是如何工作的?如果我们需要查询多个认证数据库或者组合多个认证服务比如一个数据库和一个LDAP服务器,这时该如何?
Spring Security 中默认的实现是“ProviderManager” ,它自己不处理认证请求,而是委托一组配置的“AuthenticationProvider”,这些会按顺序查询能否处理认证请求。每个provider 可以抛出异常或者返回一个完整填充的“Authentication”对象。记得“UserDetails” 和“UserDetailsService”吗?如果不记得,回到前一章再看看。验证认证请求最常规的方式是加载对应的"UserDetails",检查用户输入的密码。“DaoAuthenticationProvider”用的就是这种方式。加载的“UserDetails”对象- 特别是它包含的“GrantedAuthority”- 将会被用来创建完整填充地的“Authentication”对象,这个对象将会在认证成功时返回并且存储在“SecurityContext”中。
如果使用的是命名空间,程序内部会创建和维护一个“ProviderManager”的实例,并且通过配置认证提供者元素来给它添加providers 。在这种情况下,你不应该定义“ProviderManager” bean。然而,如果你不使用命名空间,你需要像下面这样声明它:
<bean id="authenticationManager"
class="org.springframework.security.authentication.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
<ref local="ldapAuthenticationProvider"/>
</list>
</property>
</bean>
在上面的例子中,我们有三个provider。它们将会按照声明的顺序调用,每个provider 将会尝试进行认证,或者通过返回null 来跳过认证。如果所有的provider 都返回null,“ProviderManager ”将会抛出“ProviderNotFoundException” 异常。如果你对provider 链有兴趣,可以阅读“ProviderManager” 的javadoc。
身份验证机制,比如web表单登录处理过滤器注入ProviderManager的引用,并将调用它来处理身份验证请求。你需要的provider有时会互换认证机制,然而有时候它们依赖于特定的认证机制。例如,“DaoAuthenticationProvider”和“LdapAuthenticationProvider”对于使用用户名和密码的认证机制是兼容的,因此都可以在基于表单登录和HTTP 基本认证的情况下工作。换句话说,有些认证机制创建一个只能被单个类型“AuthenticationProvider”理解的认证请求对象。比如JA-SIG CAS,它使用了服务单的概念,因此只能被“CasAuthenticationProvider”认证。你不必太担心这个,因为如果你忘记注册适合的provider,在你尝试认证时你会收到一个“ProviderNotFoundException” 异常。
2.1.1. 擦除成功的身份验证凭证
从Spring Security 3.1 开始,“ProviderManager” 默认会尝试清除成功认证时返回的“Authentication”对象中任何敏感的凭证信息。这可以防止访问非必要的信息了,比如密码。
当你使用缓存的用户对象,这可能会导致问题,比如在无状态应用中提高性能。如果“Authentication” 包含缓存对象的引用(比如“UserDetails” 实例)而且凭证被删除,那么不可能再对缓存进行认证了。如果你正在使用缓存,你需要考虑到这个。一个明显的解决方案是先复制这个对象,既可以是缓存中的,也可以是“AuthenticationProvider”中的。或者你可以禁用“ProviderManager” 的“eraseCredentialsAfterAuthentication” 属性。
2.2.2. DaoAuthenticationProvider
Spring Security 中最简单的“AuthenticationProvider” 实现类是“DaoAuthenticationProvider” ,这也是框架最早支持的。它利用“UserDetailsService ”来查找用户名,密码和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”中的密码。下面将会讨论更多细节。
2.2. UserDetailsService 实现
在这篇指南的前面提到,大多数的认证providers 利用“UserDetails ”和“UserDetailsService ”接口。回忆一下,“UserDetailsService ”的规范只一个简单的方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的“UserDetails ”是一个接口中,它能提供一些get方法,这些get方法能保证提供非空的认证信息,比如用户名,密码,授权的权限和用户是否可用。大多数的认证provider将使用‘UserDetailsService’,即使用户名和密码实际上不会用作认证决策的一部分。它们有可能仅仅使用“UserDetails ”对象的“GrantedAuthority ” 信息,因为有一些其他的系统(比如 LDAP,X.509,CAS等)已经覆行了实际验证凭证的责任。
鉴于“UserDetailsService”如此简单的实现,它应该对用户使用持久性策略检索身份验证信息来说很简单。之前说过,Spring Security 确实包含了一些有用的基础实现,下面我们来看看。
2.2.1. 内存中的认证
很容易创建一个自定义的“UserDetailsService ”实现,用它来从持久引擎中提取信息,但是许多应用不需要这么复杂。 ,如果你正在创建一个原型应用或者仅仅是开始集成Spring Security,当你不是真正地需要花时间来配置数据或者实现“UserDetailsService”时,尤其是这样。对于这种情况一个简单的选择是使用security 命名空间的元素“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]
例如:
jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled
2.2.2. JdbcDaoImpl
Spring Security 也包含一下可以从JDBC 数据源获取认证信息的“UserDetailsService ”。它内部使用了Spring JDBC,因此避免了仅仅为了存储用户信息而使用全功能的ORM。如果你的应用使用了ORM框架,你可以更喜欢写一个自定义的“UserDetailsService ”来重复利用已经创建的映射文件。让我们回到“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 ”来使用不同的关系型数据库。你也可以使用了全局的JNDI数据源,或者其他任何Spring 配置的数据源。
权限组
默认地,“JdbcDaoImpl ” 假设权限直接映射到用户,在这种情况下加载单个用户的权限。一个可选的方式用户的权限划分为组然后将权限组分配到用户。有些人喜欢这种管理用户权限的方式。可以查看“JdbcDaoImpl ”来获取更多如何使用权限组的信息。组的schema也包含在附录中。
2.3. 密码编码
Spring Security 的“PasswordEncoder ”接口用来支持以某方式持久化存储编码的密码。永远也不要用明文存储密码。总是使用单向密码散列算法比如Bcrypt,它内部使用了每个密码都不同的混淆值。不要使用一个简单的散列函数比如MD5和SHA,甚至是过时的版本。Bcrypt 专门设计成缓慢的和阻碍离线的密码破解,反之标准的散列算法是快速的,并且能被用来在特制的硬件上并行测试成千上万的密码。你可能认为这不会发生在你的身上,因为你认为数据库是安全的,离线攻击没有威胁。如果是这样的话,搜索一些受此方式牵连和安全存储密码被嘲笑的知名网站。最好是安全可靠的。使用“org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder” 是一个很好的保证安全的方式。也有一些其它语言兼容的实现,它们也是一个实现互通的很好方式。
如果你正在使用包含散列密码的遗留系统,那么你将需要一个匹配当前的算法的编码器,至少直到你可以将你的用户迁移到更安全的计划(通常这将需要用户设置一个新的密码,因为散列是不可逆的)。Spring Security有一个包包含了遗留密码编码的实现,也就是“org.springframework.security.authentication.encoding”。“DaoAuthenticationProvider ” 可以注入新的或遗留的PasswordEncoder类型。
2.3.1. 什么是hash?
密码散列不是Spring Security 独有的,而是对那些不熟悉这个概念的用户来说,都是容易混淆的。散列(或摘要)算法是一种单向函数,它们为输入的数据生成一种固定长度的数据,比如密码。例如字符串“password”(16进制) 的MD5散列码是:
5f4dcc3b5aa765d61d8327deb882cf99
散列是单向的是指它很难(实际上不可能)从散列值中获取源输入的值,或者实际上没有可能输入的值会产生相同的散列值。这个特性对认证来说散列值是很有用的。它们可以作为可能的纯文本密码存储在数据库,即使散列值被盗用,它们也不会立即暴露真实的密码。注意这同时意味着密码一旦被编码它就不能恢复了。
2.3.2. 加盐的散列(Adding Salt to a Hash?)
个人觉得这里的意思应该是给hash的过程加点儿额外的东西。
在使用密码散列的过程中有一个潜在的问题,就是如果使用了一个很常见的词作为密码时,它相对很容易避开散列的单向特性。人们倾向于选择同样的密码,预先被攻破的网站上有大量的简单密码词典可以在线访问。例如,如果你用谷歌搜索散列值“5f4dcc3b5aa765d61d8327deb882cf99 ”,你将很快找到它的源词“password”。按同样的方式,黑客可以创建一个标准单词列表的散列词典,然后使用这个来查找源密码。有一种方式来阻止这种情况,那就是使用一种适当地强密码策略来避免使用常见单词。另外一种是在计算散列值的时候加点其他的值。对每个用户来说这个是一个附加的已知数据的字符串,在计算散列值之前将它跟密码组合起来。理想情况下这个数据尽量随机,但是实际上任何值通常是可取的。加额外值意味着黑客不得不为每个额外值建立一个独立的散列词典,这使得攻击变得更复杂(但不是不可能的)。
Bcrypt 在编码每个密码时自动生成一个随机值,然后以一种标准的格式存储在bcrypt 字符串中。
注:处理额外值的传统方法是给“DaoAuthenticationProvider”注入一个“SaltSource ”,“DaoAuthenticationProvider” 将会为特定的用户获取一个额外值,然后传入到“PasswordEncoder”。使用bcrypt 意味着你不需要去关心处理额外值的细节(比如值为存储在什么位置),因为它内部都做了。因此我们强烈建议你使用bcrypt 除非你已经使用了一个独立存储额外值的系统。
2.3.3. 散列和认证
当一个认证provider (比如Spring Security的“DaoAuthenticationProvider”)需要检查用户提交认证请求中的密码并与用户已知的密码进行比较,并且存储的密码是以某种方式编码的,然后提交的值比须用正确的相同算法进行编码。这由你来检查那些与Spring Security没有控制的持久化的兼容的值。如果你在Spring Security中为认证配置添加密码散列,而且你的数据库包含明文密码,那么没有一种方式是可行的。即使你知道你的数据库使用了MD5来加密密码,例如,你的应用使用了Spring Security 的“Md5PasswordEncoder”,仍然有可能会出错。数据库有可能以base64的方式编码,例如当编码器使用了16进制的字符串(默认的)。或者你的数据库有可能使用了大写字母,但是编码器输出的是小写。写一个测试用一个已知的密码和额外值组合来检查你配置的密码编码器的输出,并且在你进行下一步和尝试通过你的应用来认证之前检查它是不是与数据库的值匹配。使用像bcrypt 这样的标准将会避免这些问题。
如果你想在Java中直接生成编码的密码存储在你的数据库中,那么你可以用“PasswordEncoder”的encode方法。