现在,我们对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中最简单的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="saltSource" ref bean="saltSource"/> <property name="passwordEncoder" ref="passwordEncoder"/> </bean>
PasswordEncoder
和SaltSource
都是可选的,一个PasswordEncoder
提供了编码和解码密码,
在UserDetails
对象中,被返回自配置好的
UserDetailsService
。一个SaltSource
可以让密码使用"盐值"生成,这可以提高授权仓库中密码的安全性。更多的细节会在
下面进行讨论。
像在前面提及的一样,大多数认证供应器都是用了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唯一的,但这对一个不了解这个概念的用户来说 还是一个很容易搞混的来源。一个散列(或摘要)算法是一个单向方法提供了一小段固定长度 的输出数据(散列)从一些输入数据中,比如一个密码。作为一个例子, 字符串“password”的MD5散列(16进制)是
5f4dcc3b5aa765d61d8327deb882cf99
散列是 “单向的” 在这种情况下,很难(基本上不可能)根据给出的散列值获得原始输入, 或是找出任何可能的输入将生成散列值。这个特点让散列值对权限方面很有用。 它们可以保存在你的用户数据库中作为原始明文密码的替换,假设这些值被泄露了 也无法立即盗取登录的密码。注意这也意味着你没有办法把编码后的密码还原。
使用密码加密的一个潜在的问题是,因为散列是单向的,如果输入是一个常用的单词的话
找到输入值就相对容易很多了。比如,如果我们查找散列值
5f4dcc3b5aa765d61d8327deb882cf99
通过google。我们会很快找到
原始词是“password”。简单的方法,一个攻击者可以建立一个散列值的字典
把标准单词排列,使用它来查找原始密码。一个方法来帮助防止这种问题是使用高强度的密码
策略来防止使用常用单词。另一个是在计算散列时使用“盐值”。
这是一个对每个用户都知道的附加字符串,它会结合到密码中,在计算散列之前。
注意这个数值应该是尽可能的随机数,但是实际中任何盐值通常都是不可取的。
Spring Security有一个SaltSource
接口,
可以被验证供应器用来为特定的用户生成一个盐值。
使用盐值,意味着攻击者必须创建单独的散列字典,为不同的盐值,
这让攻击更难了(但不是不可能)。
当一个认证供应器(比如Spring Security的DaoAuthenticationProvider
)
需要检验密码,在提交认证请求中,与用户知道的数据进行比较,保存的密码通过一些方式进行了加密,
然后提交的数据必须也使用相同的算法进行加密。这要求你去检查兼容性,因为Spring Security
对持久化的没有任何控制。如果你在Spring Security的认证配置中添加了密码散列功能,
你的数据库包含原始明文密码,那么认证就绝对不可能成功。
如果你在数据库中使用MD5对密码加密,比如,你的应用配置为使用Spring Security的
Md5PasswordEncoder
,这也有其他可能的问题。
数据库可能用Base 64进行了加密,比如当加密器使用16进制的字符串(默认)
[5]。
可以选择,你的数据库可能使用了大写,当编码器输出的是小写。
确定你编写了一个测试来检测从你的密码编码器的输出,使用一个知道的密码和盐值结合
检测它是否与数据库值匹配,在更深入之前,尝试通过你的系统认证。
要想获得更多信息,在默认的方法从结合盐值和密码,查看
BasePasswordEncoder
的Javadoc。如果你希望直接通过java生成密码,
为你的用户数据库保存,然后你可以使用PasswordEncoder
的
encodePassword
方法。
[5] 你可以配置编码器使用Base 64替换16进制,通过设置
encodeHashAsBase64
为true。参考
MessageDigestPasswordEncoder
的Javadoc和它的
超类获得更多信息。