核心服务
现在,我们对Spring Security的架构和核心类有了高层次的了解了, 让我们近距离看看这些核心接口和他们的实现, 特别是AuthenticationManager
, UserDetailsService
和 AccessDecisionManager
。 它们的信息都在这个文档的后面,所以重要的是我们要知道如何配置,如何操作。
AuthenticationManager
只是一个接口,所以呢,它的实现 可以让我们随便选择,但是实际上它是如何工作的呢? 如果我们需要检查多个授权数据库或者将不同的授权服务结合起来,比如数据库和lDAP服务器?
在Spring Security中的默认实现是ProviderManager
不只是处理授权请求自己,它委派了一系列配置好的 AuthenticationProvider
, 每个按照顺序查看它是否可以执行验证。每个供应器会跑出一个异常, 或者返回一个完整的 Authentication
对象。 要记得我们的好朋友,UserDetails
和、 UserDetailsService
。 如果不记得了,返回到前面的章节刷新一下你的记忆。 最常用的方式是验证一个授权请求读取 对应的UserDetails
,并检查用户录入的密码。 这是通过DaoAuthenticationProvider
实现的(见下面), 加载的UserDetails
对象 - 特别是包含的 GrantedAuthority
- 会在建立Authentication
时使用,这回返回一个成功验证,保存到SecurityContext
中。
如果你使用了命名空间,一个ProviderMananger
的实例会被创建 并在内部进行维护,你可以使用命名空间验证元素,或给一个bean添加一个 <custom-authentication-provider>
元素。(参考 命名空间章节)。在这里,你不应该在你的application context中声明一个 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>
在上面的例子中,我们有三个供应器。 它们按照顺序显示(使用List
实现), 每个供应器能够尝试进行授权,或通过返回null
跳过授权。 如果所有的实现都返回null。ProviderManager
会跑出一个ProviderNotFoundException
异常。 如果你对链状供应器感兴趣, 请参考ProviderManager
的javadoc。
验证机制,比如表单登陆处理过滤器被注入一个ProviderManager
, 会被用来处理它们的认证请求。 你需要的供应器有时需要被认证机制内部改变的,当在其他时候, 他们会以来一个特定的认证机制, 比如DaoAuthenticationProvider
和LdapAuthenticationProvider
可疑对应任何一个提交简单username/password 的认证请求,所以可以和基于表单登陆和HTTP基本认证一起工作。 其他时候,一些认证机制创建了 一个认证请求对象,只可以被单个类型的AuthenticationProvider
拦截。 一个例子就是JA-SIG CAS, 它使用一个提醒的服务票据,所以只可以被 CasAuthenticationProvider
认证。 你不需要很了解这些, 因为如果你忘记了注册合适的供应器, 你会得到一个ProviderNotFoundException
当这个验证尝试起作用的时候。
默认情况下,(从Spring Security 3.1开始),ProviderManager
会尝试,从成功的验证请求返回的 Authentication
对象中, 清除所有敏感的凭证信息。 这可以防止密码这类的信息不必要的保存过程的时间。
当你使用了一个用户缓存时,就可以产生问题,比如, 为了提高一个无状态应用的性能。如果Authentication
保存了缓存里的对象引用(比如UserDetails
实例) 这就会把它的凭证删除掉,然后就不能在使用缓存的数据来进行验证了。 你需要考虑这个问题,如果你使用了缓存。 一个常见的解决方法是提前复制对象,或者在缓存实现里 或在创建返回Authentication
对象的 AuthenticationProvider
中。或者, 你可以禁用ProviderManager
的 eraseCredentialsAfterAuthentication
属性。 参考javadoc获得更多信息。
spring security中最简单的AuthenticationProvider
实现 是DaoAuthenticationProvider
,这也是框架中最早支持的功能之一。 它是UserDetailsService
的杠杆(作为DAO), 为了获得username, password和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
和SaltSource
都是可选的,一个PasswordEncoder
提供了编码和解码密码, 在UserDetails
对象中,被返回自配置好的 UserDetailsService
。更多的细节会在 下面进行讨论。
像在前面提及的一样,大多数认证供应器都是用了UserDetails
和UserDetailsService
接口。 调用UserDetailsService
中的单独的方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的UserDetails
是一个接口,它提供了获得保证 非空的认证信息,比如用户名,密码,授予的权限和用户账号是可用还是禁用。 大多数认证供应器会使用UserDetailsService
, 即使username和password没有实际用在这个认证决策中。 它们可以使用返回的UserDetails
对象,获得它的 GrantedAuthority
信息,因为一些其他系统(比如LDAP或者X.509或CAS等等) 了解真实验证证书的作用。
这里的UserDetailsService
也很简单实现, 它应该为用户简单的获得认证信息,使用它们选择的持久化策略。 这样说,Spring Security包含了很多有用的基本实现,下面我们会看到。
创建一个自定义的UserDetailsService
的实现是很容易的, 可以从选择的持久化引擎中获得信息,但是许多应用没有那么复杂。尤其是如果你建立一个原型应用 或只是开始集成Spring Security的时候,当我们不是真的需要耗费时间配置数据库或者写 UserDetailsService
实现。为了这些情况, 一个简单的选择是使用安全命名空间中的 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
Spring Security也包含了一个UserDetailsService
, 它包含从一个JDBC数据源中获得认证信息。内部使用了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
会假设用户的权限都保存在authorities表中。 (参考数据库结构附录). 还有一种选择是把权限分组,然后让用户加入这些用户组。一些人更喜欢使用这种方法来管理用户的权限。 参考JdbcDaoImpl
的Javadoc以获得更多的信息,了ijeruhe启用权限分组。 用户组使用的数据库结构也包含在附录中。
Spring Security的PasswordEncoder
接口用来支持 对密码通过一些方式进行加密,并保存到媒介中。 这通常意味着密码被“散列加密”,使用一个加密算法,比如MD5或者SHA。 Spring Security 3.1的crypto
包提供了一个更简单的API,可以实现最佳的密码哈希实现。 我们将推荐你使用这些API来进行新的开发,将org.springframework.security.authentication.encoding
作为遗留的实现。DaoAuthenticationProvider
可以注入新的或遗留的 PasswordEncoder
类型。
密码加密不是Spring Security唯一的,但这对一个不了解这个概念的用户来说 还是一个很容易搞混的来源。一个散列(或摘要)算法是一个单向方法提供了一小段固定长度 的输出数据(散列)从一些输入数据中,比如一个密码。作为一个例子, 字符串“password”的MD5散列(16进制)是
5f4dcc3b5aa765d61d8327deb882cf99
散列是 “单向的” 在这种情况下,很难(基本上不可能)根据给出的散列值获得原始输入, 或是找出任何可能的输入将生成散列值。这个特点让散列值对权限方面很有用。 它们可以保存在你的用户数据库中作为原始明文密码的替换,假设这些值被泄露了 也无法立即盗取登录的密码。注意这也意味着你没有办法把编码后的密码还原。
使用密码加密的一个潜在的问题是,因为散列是单向的,如果输入是一个常用的单词的话 找到输入值就相对容易很多了。比如,如果我们查找散列值 5f4dcc3b5aa765d61d8327deb882cf99
通过google。我们会很快找到 原始词是“password”。简单的方法,一个攻击者可以建立一个散列值的字典 把标准单词排列,使用它来查找原始密码。一个方法来帮助防止这种问题是使用高强度的密码 策略来防止使用常用单词。另一个是在计算散列时使用“盐值”。 这是一个对每个用户都知道的附加字符串,它会结合到密码中,在计算散列之前。 注意这个数值应该是尽可能的随机数,但是实际中任何盐值通常都是不可取的。 使用盐值,意味着攻击者必须创建单独的散列字典,为不同的盐值, 这让攻击更难了(但不是不可能)。
StandardPasswordEncoder
在 crypto
包中使用了随机的8位盐值, 它被保存在密码相同的地方。
Note
遗留的处理盐值的方法是把一个SaltSource
注入到DaoAuthenticationProvider
中, 这将为特定的用户获得一个盐值,并传递到PasswordEncoder
中。 使用一个随机盐值,把它和密码数据集合意味着你不需要担心处理盐值的细节( 比如值保存在哪里),它是在内部实现的。所以我们强烈推荐你使用这种方式, 除非你已经在系统中有一个地方用来单独保存盐值。
当一个认证供应器(比如Spring Security的DaoAuthenticationProvider
) 需要检验密码,在提交认证请求中,与用户知道的数据进行比较,保存的密码通过一些方式进行了加密, 然后提交的数据必须也使用相同的算法进行加密。这要求你去检查兼容性,因为Spring Security 对持久化的没有任何控制。如果你在Spring Security的认证配置中添加了密码散列功能, 你的数据库包含原始明文密码,那么认证就绝对不可能成功。 如果你在数据库中使用MD5对密码加密,比如,你的应用配置为使用Spring Security的 Md5PasswordEncoder
,这也有其他可能的问题。 数据库可能用Base 64进行了加密,比如当加密器使用16进制的字符串(默认)。 可以选择,你的数据库可能使用了大写,当编码器输出的是小写。 确定你编写了一个测试来检测从你的密码编码器的输出,使用一个知道的密码和盐值结合 检测它是否与数据库值匹配,在更深入之前,尝试通过你的系统认证。
如果你希望直接通过java生成密码, 为你的用户数据库保存,然后你可以使用PasswordEncoder
的 encode
方法。