现在的位置: 首页 > 综合 > 正文

Spring 用户身份验证

2013年06月28日 ⁄ 综合 ⁄ 共 11600字 ⁄ 字号 评论关闭

 A example

用户身份验证

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

抱歉!评论已关闭.