核心服务

 

现在,我们对Spring Security的架构和核心类有了高层次的了解了, 让我们近距离看看这些核心接口和他们的实现, 特别是AuthenticationManager, UserDetailsService和 AccessDecisionManager。 它们的信息都在这个文档的后面,所以重要的是我们要知道如何配置,如何操作。

6.1. The AuthenticationManagerProviderManager 和 AuthenticationProviders

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 当这个验证尝试起作用的时候。

6.1.1. 认证成功后清除凭证(密码)

默认情况下,(从Spring Security 3.1开始),ProviderManager 会尝试,从成功的验证请求返回的 Authentication对象中, 清除所有敏感的凭证信息。 这可以防止密码这类的信息不必要的保存过程的时间。

当你使用了一个用户缓存时,就可以产生问题,比如, 为了提高一个无状态应用的性能。如果Authentication 保存了缓存里的对象引用(比如UserDetails实例) 这就会把它的凭证删除掉,然后就不能在使用缓存的数据来进行验证了。 你需要考虑这个问题,如果你使用了缓存。 一个常见的解决方法是提前复制对象,或者在缓存实现里 或在创建返回Authentication对象的 AuthenticationProvider中。或者, 你可以禁用ProviderManager的 eraseCredentialsAfterAuthentication属性。 参考javadoc获得更多信息。

6.1.2. DaoAuthenticationProvider

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>

PasswordEncoderSaltSource 都是可选的,一个PasswordEncoder提供了编码和解码密码, 在UserDetails对象中,被返回自配置好的 UserDetailsService。更多的细节会在 下面进行讨论。

6.2. UserDetailsService实现

像在前面提及的一样,大多数认证供应器都是用了UserDetails 和UserDetailsService接口。 调用UserDetailsService中的单独的方法:

 

  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
            

 

返回的UserDetails是一个接口,它提供了获得保证 非空的认证信息,比如用户名,密码,授予的权限和用户账号是可用还是禁用。 大多数认证供应器会使用UserDetailsService, 即使username和password没有实际用在这个认证决策中。 它们可以使用返回的UserDetails对象,获得它的 GrantedAuthority信息,因为一些其他系统(比如LDAP或者X.509或CAS等等) 了解真实验证证书的作用。

这里的UserDetailsService也很简单实现, 它应该为用户简单的获得认证信息,使用它们选择的持久化策略。 这样说,Spring Security包含了很多有用的基本实现,下面我们会看到。

6.2.1. 内存认证

创建一个自定义的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

6.2.2. JdbcDaoImpl

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配置。

6.2.2.1. 权限分组

默认情况下,JdbcDaoImpl会假设用户的权限都保存在authorities表中。 (参考数据库结构附录). 还有一种选择是把权限分组,然后让用户加入这些用户组。一些人更喜欢使用这种方法来管理用户的权限。 参考JdbcDaoImpl的Javadoc以获得更多的信息,了ijeruhe启用权限分组。 用户组使用的数据库结构也包含在附录中。

6.3. 密码加密

Spring Security的PasswordEncoder 接口用来支持 对密码通过一些方式进行加密,并保存到媒介中。 这通常意味着密码被散列加密”,使用一个加密算法,比如MD5或者SHA。 Spring Security 3.1的crypto包提供了一个更简单的API,可以实现最佳的密码哈希实现。 我们将推荐你使用这些API来进行新的开发,将org.springframework.security.authentication.encoding 作为遗留的实现。DaoAuthenticationProvider可以注入新的或遗留的 PasswordEncoder类型。

6.3.1. 什么是散列加密?

密码加密不是Spring Security唯一的,但这对一个不了解这个概念的用户来说 还是一个很容易搞混的来源。一个散列(或摘要)算法是一个单向方法提供了一小段固定长度 的输出数据(散列)从一些输入数据中,比如一个密码。作为一个例子, 字符串password”的MD5散列(16进制)是

    5f4dcc3b5aa765d61d8327deb882cf99

散列是 单向的” 在这种情况下,很难(基本上不可能)根据给出的散列值获得原始输入, 或是找出任何可能的输入将生成散列值。这个特点让散列值对权限方面很有用。 它们可以保存在你的用户数据库中作为原始明文密码的替换,假设这些值被泄露了 也无法立即盗取登录的密码。注意这也意味着你没有办法把编码后的密码还原。

6.3.2. 为散列加点儿盐

使用密码加密的一个潜在的问题是,因为散列是单向的,如果输入是一个常用的单词的话 找到输入值就相对容易很多了。比如,如果我们查找散列值 5f4dcc3b5aa765d61d8327deb882cf99通过google。我们会很快找到 原始词是password”。简单的方法,一个攻击者可以建立一个散列值的字典 把标准单词排列,使用它来查找原始密码。一个方法来帮助防止这种问题是使用高强度的密码 策略来防止使用常用单词。另一个是在计算散列时使用盐值”。 这是一个对每个用户都知道的附加字符串,它会结合到密码中,在计算散列之前。 注意这个数值应该是尽可能的随机数,但是实际中任何盐值通常都是不可取的。 使用盐值,意味着攻击者必须创建单独的散列字典,为不同的盐值, 这让攻击更难了(但不是不可能)。

StandardPasswordEncoder 在 crypto 包中使用了随机的8位盐值, 它被保存在密码相同的地方。

Note

遗留的处理盐值的方法是把一个SaltSource 注入到DaoAuthenticationProvider中, 这将为特定的用户获得一个盐值,并传递到PasswordEncoder中。 使用一个随机盐值,把它和密码数据集合意味着你不需要担心处理盐值的细节( 比如值保存在哪里),它是在内部实现的。所以我们强烈推荐你使用这种方式, 除非你已经在系统中有一个地方用来单独保存盐值。

6.3.3. 散列和认证

当一个认证供应器(比如Spring Security的DaoAuthenticationProvider) 需要检验密码,在提交认证请求中,与用户知道的数据进行比较,保存的密码通过一些方式进行了加密, 然后提交的数据必须也使用相同的算法进行加密。这要求你去检查兼容性,因为Spring Security 对持久化的没有任何控制。如果你在Spring Security的认证配置中添加了密码散列功能, 你的数据库包含原始明文密码,那么认证就绝对不可能成功。 如果你在数据库中使用MD5对密码加密,比如,你的应用配置为使用Spring Security的 Md5PasswordEncoder,这也有其他可能的问题。 数据库可能用Base 64进行了加密,比如当加密器使用16进制的字符串(默认)。 可以选择,你的数据库可能使用了大写,当编码器输出的是小写。 确定你编写了一个测试来检测从你的密码编码器的输出,使用一个知道的密码和盐值结合 检测它是否与数据库值匹配,在更深入之前,尝试通过你的系统认证。

如果你希望直接通过java生成密码, 为你的用户数据库保存,然后你可以使用PasswordEncoder的 encode方法。

posted @ 2019-11-27 17:29  DarJeely  阅读(463)  评论(0编辑  收藏  举报