用户身份验证
应用程序的安全机制需要在授权用户访问资源之前确定用户的身份,即用户是谁。大多数应用都会弹出一个登陆界面供用户输入用户名密码。在 Spring 安全机制中, authentication manager 由org.acegisecurity.AuthenticationManager 接口定义。
public insterface AuthenticationManager {
public Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
上面出现的 acegi 就是前面提到的 Spring 安全机制的早期的名字。也许现在的版本已经将此接口转移到 org.springframework.security 包下。接口中的 authenticate 方法会尝试着通过 Authentication 对象 (Authentication 对象包含了用户的登陆信息 ) 对用户身份进行验证。如果成功,该方法返回一个完整的 Authentication 对象,包含用户授权信息;如果失败,该方法会抛出 AuthenticationException异常。 Spring 提供了 ProviderManager 类实现 AuthenticationManager 接口。下面让我们来看看如何使用 ProviderManager
·配置 provider manager
ProviderManager 实现了 AuthenticationManager 接口,但它也不会直接对用户进行身份验证,它会将该工作交给其他多个 authentication provider ,如图:
下面的 XML 片段展示了如何配置 ProviderManager :
<bean id=”authenticationManager”
class=”org.acegisecurity.providers.ProviderManager”>
<property name=”providers”>
<list>
<ref bean=”daoAuthenticationProvider” />
<ref bean=”ldapAuthenticationProvider” />
</list>
</property>
</bean>
上面 XML 代码提供了一组 authentication provider 给 ProviderManager 。一般情况下,你只需要一个 provider 即可,但是在有些时候,提供一组 provider 可能回事非常有用的。 Spring 提供了很多authentication provider ,例如: AuthByAdapterProvider 、 AnomymousAuthenticationProvider等。如果你认为 Spring 提供的 provider 不能满足你的需求,你可以创建自己的 authentication provider ,只需实现 AuthenticationProvider 接口即可:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class authentication);
}
下面介绍比较常用的 provider : DaoAuthenticationProvider ——支持面向数据库的身份验证。DaoAuthenticationProvider 使用 DAO 从数据库中获得用户信息 ( 包括用户密码 ) ,然后和从Authentication 对象传递过来的信息进行比较。如果用户名密码完全匹配,则会返回一个完整的Authentication 对象;如果失败则抛出 AuthenticationException 异常。
配置 DaoAuthenticationProvider 更简单。下面的 XML 代码即展示了如何声明DaoAuthenticationProvider bean :
<bean id=”authenticationProvider”
class=”org.acegisecurity.providers.dao. DaoAuthenticationProvider”>
<property name=”userDetailsService” ref=”userDetailsService” />
</bean>
userDetailService 属性用于确定从数据库读取用户信息的 bean 。这个属性指定了org.acegisecurity.userdetails.UserDetailService 的一个实例。下面的问题就在于 userDetailsService是如何被配置的。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException;
}
loadUsername 方法从名字看就知道是做什么的。但在你开始编写自己的 UserDetailsService 实现时,你应该了解 Spring 提供了两个现成的 AuthenticationDao 实现: InMemoryImpl 和 JdbcDaoImpl。
· In-memory DAO
实际上, AuthenticationDao 并不是一定要去数据库查询用户信息。如果你的应用身份验证需求不强烈或是你想简化开发过程,你可以直接在配置文件中配置你的用户信息。 Spring 提供了InMemoryImpl 类实现 UserDetailsService 接口,它可以从 Spring 配置文件中抽取出用户信息。用法如下:
<bean id=”authenticationDao”
class=”org.acegisecurity.userdetails.memory.InMemoryDaoImpl”>
<property name=”userMap”>
<value>
palmerd=4moreyears, disabled, ROLE_PRESIDENT
bauer=ineedsleep, ROLE_FILED_OPS
obrianc=nosmile, ROLE_SR_ANALYST, ROLE_OPS
myersn=traitor, disabled, ROLE_CENTRAL_OPS
</value>
</property>
</bean>
userMap 属性是 org.acegisecurity.userdetails.memory.UserMap 对象。它定义了一组用户名,密码和权限。如 palmerd 是用户名,密码是 4moreyears 。后面的那个是权限, disabled 表明该用户的状态为不可用,因此不能进行身份验证。在使用 InMemoryDaoImpl 时你不需要实例化 UserMap 对象,因为存在一个属性编辑器可以将字符串转化成一个 UserMap 对象。
In-memory DAO 虽然简单易用,但有很明显的缺陷: 1. 需要修改配置文件并重新部署应用; 2.不适用于生产环境下使用。因此可以考虑使用 JdbcDaoImpl 。
· JdbcDaoImpl
JdbcDaoImpl 从数据库中获取用户信息,用法如下:
<bean id =”authenticationDao”
class=”org.acegisecurity.userdetails.dbc.JdbcDaoImpl”>
<property name=”dataSource” ref=”dataSource” />
</bean>
对于用户信息在数据库中的保存, JdbcDaoImpl 做了一些基本的假设。它认为在数据库中存在两站表: Users 表和 Authorities 表:
这样当查询用户信息时, JdbcDaoImpl 使用
SELECT username, password, enable FROM users WHERE username=?
而查询用户权限时,使用下面 SQL 语句:
SELECT username, authority FROM authorities WHERE username=?
JdbcDaoImpl 做这样的假设太过简单,对于其他的应用来说可能并不匹配。例如 RoadRantz 应用来说, Motorist 表保存了用户的用户名密码。那么如何使用 JdbcDaoImpl 来对 motorist 进行身份验证呢?方法就是设置 usersByUsernameQuery 属性。例如:
<bean id=”authenticationDao”
class=”org.acegisecurity.userdetails.jdbc.JdbcDaoImpl”>
<property name=”dataSource” ref bean=”dataSource” />
<property name=”userByUsernameQuery” >
<value>
SELECT email as username, password, enabled FROM Motorist
WHERE email=?
</value>
</property>
</bean>
另外,我们还需要告诉 JdbcDaoImpl 如何查询用户的权限
<bean id=”authenticationDao”
class=”org.acegisecurity.userdeatils.jdbc.JdbcDaoImpl”>
<property name=”dataSource” ref=”dataSource” />
…
<property name=”authoritiesByUsernameQuery”>
<value>
SELECT email as username, privilege as authority
FROM Motorist_Privileges mp, Motorist m
WHERE mp.motorist_id=m.id AND m.email=?
</value>
</property>
</bean>
上面的 SQL 语句从 Motorist_Privileges 表中查询用户权限。
·使用加密的密码
DaoAuthenticationProvider 在验证用户密码的时候总是认为密码是没有加密的。因此如果要加密密码, DaoAuthenticationProvider 需要使用一个密码编码器, Spring 提供了一些密码编码器,包括 Md5PasswordEncoder , PlaintextPasswordEncoder , ShaPasswordEncoder 和LdapShaPasswordEncoder 。默认情况下, DaoAuthenticationProvider 使用PlaintextPasswordEncoder ,这表示密码是未经过编码的。下面的 XML 代码展示了如何指定密码编码器:
<bean id=”daoAuthenticationProvider”
class=”org.acegisecurity.providers.dao.DaoAuthenticationProvider”>
<property name=”userDetailsService” ref=”authenticationDao” />
<property name=”passwordEncoder”>
<bean
class=”org.acegisecurity.providers.encoding.Md5PasswordEncoder” />
</property>
</bean>
你还需要为一个编码器指定一个 salt source 。 Spring 提供了两类 salt source :
· SystemWideSaltSource ——为所有用户提供相同的 salt
· ReflectionSaltSource ——在 User 对象的指定属性上应用反射来创建 salt
ReflectionSaltSource 更安全一些,因为每个用户的密码都是用不同 salt 值进行加密的。使用方法如下:
<bean id=”daoAuthenticationProvider”
class=”org.acegisecurity.providers.dao.DaoAuthenticationProvier” >
<property name=”userDetailsService” ref=”authenticationDao” />
<property name=”passwordEncoder”>
<bean class=”org.acegisecurity.providers.encoding.Md5PasswordEncoder” />
</property>
<property name=”saltSource”>
<bean class=”org.acegisecurity.providers.dao.salt.ReflectionSaltSource”>
<property name=”userPropertyToUse” value=”userName” />
</bean>
</property>
</bean>
Salt 就像一个 key 一样用来加密密码,它必须保持值不变。
尽管 ReflectionSaltSource 更加安全, SystemWideSaltSource 更加常用一些,使用方法如下:
<bean id=”daoAuthenticationProvider”
class=”org.acegisecurity.providers.dao.DaoAuthenticationProvider”>
<property name=”userDetailsService” ref=”authenticationDao” />
<property name=”passwordEncoder”>
<bean class=”….Md5PasswordEncoder” />
</property>
<property name=”saltSource”>
<bean class=”org.acegisecurity.providers.dao.salt.SystemWideSaltSource”>
<property name=”systemWideSalt” value=”ABC123XYZ789” />
</bean>
</property>
</bean>
上述代码中的 ABC123XYZ789 用于加密所有的密码。
·缓存用户信息
如果用户信息不经常改变,那么缓存用户信息可以提高性能。我们需要向DaoAuthenticationProvider 提供 org.acegisecurity.providers.dao.UserCache 的一个实现,该接口定义了三个方法:
public UserDetails getUserFromCache(String username);
public void putUserInCache(UserDetails user);
public void removeUserFromCache(String username);
当然你也可以编写你自己的 UserCache 实现。但在这之前你需要注意 Spring 提供了两个方便的实现:
org.acegisecurity.providers.dao.cache.NullUserCache
org.acegisecutiry.providers.dao.cache.EhCacheBasedUserCache
NullUserCache 实际上并不执行任何的 Cache 操作。相反,它总是从 getUserFromCache 方法中返回 NULL ,迫使 DaoAuthenticationProvider 强行执行查询操作。
EhCacheBasedUserCache 更常用一些,它是基于 EHCache 的。例如:
<bean id=”daoAuthenticationProvider”
class=”org.acegisecurity.providers.dao.DaoAuthenticationProvider”>
<property name=”userDetailsService” ref=”authenticationDao” />
…
<property name=”userCache”>
<bean class=”org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache”>
<property name=”cache” ref=”ehcache” />
</bean>
</property>
</bean>
Cache 属性引用了一个 ehcache bean ,即一个 EHCache 对象。若要获得 Cache 对象可以使用 Spring 的 cache 模块,例如:
<bean id=”ehcache”
class=”org.springframework.cache.ehcache.EhCacheFactoryBean”>
<property name=”cacheManager” ref=”cacheManager” />
<property name=”cacheName” value=”userCache” />
</bean>
<bean id=”cacheManager”
class=” org.springframework.cache.ehcache.EhCacheManagerFactoryBean”>
<property name=”configLocation” value=”classpath:ehcache.xml” />
</bean>
EhCacheFactoryBean 是一个工厂 bean 用来产生一个 EHCache 对象。 ehcache.xml 文件配置了真正的缓存配置。
当你的安全信息保存在关系数据库中时, DaoAuthenticationProvider 非常有用。但如果信息保存在 LDAP 服务器上时,你就需要使用 LdapAuthenticationProvider 了。
· LdapAuthenticationProvider
Spring 提供了对于 LDAP 的支持。用法如下:
<bean id=”ldapAuthProvider”
class=”org.acegisecurity.providers.ldap.LdapAuthenticationProvider”>
<constructor-arg ref=”authenticator” />
<constructor-arg ref=”populator” />
</bean>
值得注意的是, LdapAuthenticationProvider 有两个参数 authenticator 和 populator 。
· authenticator :负责对 LDAP repository 进行验证。 authenticator 可以是实现了org.acegisecurity.providers.ldap.LdapAuthenticator 接口的任意对象。
· populator :负责从 LDAP repository 中获取授权用户集。 Populator 可以是实现了org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator 接口的任意对象。
下面看看 authenticator 是如何被定义的。
· LDAP 绑定认证
LDAP 认证有两种方法: 1. 使用 LDAP 用户名及密码; 2. 获取 LDAP 用户信息与 LDAP 记录中的信息做比较。
对于前者, Spring 提供了 LdapAuthenticator 的实现 BindAuthenticator 。 BindAuthenticator 使用LDAP 绑定操作符绑定 LDAP 服务器的用户。这依赖于 LDAP server 对用户进行验证。例如:
<bean id=”authenticator”
class=”org.acegisecurity.providers.ldap.authenticator.BindAuthenticator”>
<constructor-arg ref=”initialDirContextFactory” />
<property name=”userDnPatterns”>
<list>
<value>uid={0}, ou=motorists</value>
</list>
</property>
</bean>
首先我们来看 userDnPatterns 属性。该属性表示如何在 LDAP 中寻找一个用户。它包含了一组模式列表, BindAuthentication 以它们作为关键字 (DN) 来确定用户。 {0} 表示一个占位符用以接受一个用户名。现在来看那个构造参数。 BindAuthenticator 需要知道如何访问 LDAP repository ,它的构造函数需要一个 initialDirContextFactory ,它的 bean 写法如下:
<bean id=” initialDirContextFactory”
class=”org.acegisecurity.ldap.DefaultInitialDirContextFactory”>
<constructor-arg value=”ldap://ldap.roadrantz.com:389/dc=roadrantz,dc=com” />
</bean>
DefaultInitialDirContextFactory 将会捕获连接 LDAP 服务器所需的所有信息并产生一个 JNDI DirContext 对象。 BindAuthenticator 会使用 DefaultInitialDirContextFactory 来连接 LDAP repository。
·密码匹配验证
通过使用 PasswordComparisonAuthenticator , Spring 提供了基于密码匹配的验证。使用方法如下:
<bean id=”authenticator”
class=”org.acegisecurity.providers.ldap.authenticator.PasswordComparisonAuthenticator”>
<constructor-arg ref=”initialDirContextFactory” />
<property name=”userDnPatterns”>
<list>
<value>uid={0}, ou=motorists</value>
</list>
</property>
</bean>
可以发现,除了类名不同之外其他的都与 BindAuthenticator 完全相同。当然还是可以有一些不同的:
<bean id=”authenticator”
class=”org.acegisecurity.providers.ldap.authenticator.PasswordComparisonAuthenticator”>
<constructor-arg ref=”initialDirContextFactory” />
<property name=”userDnPatterns”>
<list>
<value>uid={0}, ou=motorists</value>
</list>
</property>
<property name=”passwordAttributeName” value=”userCredentials” />
</bean>
还有就是密码加密方式的不同。默认情况下使用 LdapShaPasswordEncoder 来加密,但你可以编写自己的加密算法,只需要实现 PasswordEncoder 接口就可以了,例如:
<bean id=”authenticator”
class=”org.acegisecurity.providers.ldap.authenticator.PasswordComparisonAuthenticator”>
<constructor-arg ref=”initialDirContextFactory” />
<property name=”userDnPatterns”>
<list>
<value>uid={0}, ou=motorists</value>
</list>
</property>
<property name=”passwordEncoder”>
<bean class=”org.acegisecurity.providers.encoding.PlaintextPasswordEncoder” />
</property>
</bean>
最后需要说明的是, PasswordComparisonAuthenticator 不会通过用户名来绑定 LDAP 。虽然大部分 LDAP provider 不允许匿名绑定,但还是有些 LDAP provider 是允许这样做的。所以我们需要提供为 DefaultInitialDirContextFacotry 一个管理器关键字和密码。
<bean id=”initialDirContextFactory”
class=”org.acegisecurity.ldap.DefaultInitialDirContextFactory”>
<constructor-arg value=”ldap://….” />
<property name=”managerDn” value=”cn=manager,dc=roadrantz,dc=com”/>
<property name=”managerPassword” value=”letmein” />
</bean>
下面讨论 LDAP 认证中的另一个参数: populator
·声明 populator bean
用户身份验证只是第一步,下一步需要获取用户的权限列表。 Spring 创建了DefaultLdapAuthoritiesPopulator 类实现了 LdapAuthoritiesPopulator 接口,配置文件如下:
<bean id=”populator”
class=”org.acegisecurity.providers.ldap.populator. DefaultLdapAuthoritiesPopulator”>
<constructor-arg ref=”initialDirContextFactory” />
<constructor-arg value=”ou=groups” />
<property name=”groupRoleAttribute” value=”ou” />
</bean>
DefaultLdapAuthoritiesPopulator 有两个构造参数。第一个参数前面说过不再赘述,第二个参数用来查找 LDAP repository 中的组信息。最后的 groupRoleAttribute 属性指定了包含用户角色信息的属性名,默认值是 cn ,我们这里设置为 ou 。