用户身份验证
应用程序的安全机制需要在授权用户访问资源之前确定用户的身份,即用户是谁。大多数应用都会弹出一个登陆界面供用户输入用户名密码。在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。