10. Core Services(核心服务)
既然我们已经对Spring安全体系结构及其核心类有了一个高层次的概述,那么让我们仔细看看一两个核心接口及其实现,特别是AuthenticationManager、UserDetailsService和AccessDecisionManager。这些在本文档的其余部分中经常出现,所以您知道它们是如何配置和如何操作的是很重要的。
10.1 The AuthenticationManager, ProviderManager and AuthenticationProvider(授权管理器、提供者管理器和授权提供者)
AuthenticationManager只是一个接口,所以实现可以是我们选择的任何东西,但是它在实践中是如何工作的呢?如果我们需要检查多个身份验证数据库或不同身份验证服务的组合(如数据库和LDAP服务器),该怎么办?
Spring Security中的默认实现称为ProviderManager,而非处理身份验证请求本身,它委托给一个列表去配置AuthenticationProvider,按照列表顺序执行,看它是否能进行认证。每个提供者要么抛出一个异常,要么返回一个完全填充的身份验证对象。还记得我们的好朋友,UserDetails 和UserDetailsService吗?如果没有,回到前一章,并刷新你的记忆。验证身份验证请求的最常见方法是加载相应的UserDetails,并根据用户输入的密码检查加载的密码。这是DaoAuthenticationProvider使用的方法(见下文)。当构建完全填充的Authentication 时,将使用加载的UserDetails 对象(尤其是它所包含的GrantedAuthority ),该Authentication 对象从成功的身份验证中返回并存储在SecurityContext中。
如果您使用的是命名空间,则会在内部创建并维护一个ProviderManager实例,并通过使用命名空间身份验证提供程序元素向其中添加提供程序(请参见命名空间一章)。在这种情况下,您不应该在应用程序上下文中声明一个ProviderManager bean。但是,如果您不使用命名空间,那么您应该这样声明它:
在上面的例子中,我们有三个提供者。它们试图在顺序显示(List的使用暗示了这一点),每个提供者都能尝试验证,或者通过简单的返回null跳过认证。如果所有的实现都返回null,则ProviderManager将抛出一个ProviderNotFoundException。如果你有兴趣了解更多的有关提供者,请参考ProviderManager的JavaDocs。
身份验证机制(如网页表单登录处理过滤器)被注入对提供者管理器的引用,并将调用它来处理他们的身份验证请求。您需要的提供者有时可以与身份验证机制互换,而在其他时候,它们将依赖于特定的身份验证机制。例如,DaoAuthenticationProvider和LdapAuthenticationProvider与任何提交简单用户名/密码身份验证请求的机制都兼容,因此可以使用基于表单的登录或HTTP基本身份验证。另一方面,一些身份验证机制创建一个只能由单一类型的身份验证提供程序解释的身份验证请求对象。这方面的一个例子是JA-SIG CAS,它使用服务票据的概念,因此只能由一个CasAuthenticationProvider进行身份验证。您不必过于担心这一点,因为如果您忘记注册合适的提供程序,当尝试进行身份验证时,您只会收到一个ProviderNotFoundException。
10.1.1Erasing Credentials on Successful Authentication成功验证时擦除凭据
默认情况下(从Spring Security 3.1开始),ProviderManager将尝试从身份验证对象中清除由成功的身份验证请求返回的任何敏感凭据信息。这可以防止密码等信息保留的时间过长。
例如,当您使用用户对象的缓存来提高无状态应用程序的性能时,这可能会导致问题。如果Authentication包含对缓存中对象(如UserDetails实例)的引用,并且该对象的凭据已被删除,则无法再根据缓存值进行身份验证。如果使用缓存,您需要考虑这一点。一个显而易见的解决方案是首先在缓存实现中或者在创建返回的Authentication 对象的AuthenticationProvider 中创建对象的副本。或者,您可以禁用ProviderManager上的erasecredentialsafterathentication属性。有关更多信息,请参见Javadoc。
10.1.2 DaoAuthenticaationProvider(Dao认证提供者)
由Spring Security实现的最简单的AuthenticationProvider是DaoAuthenticationProvider,它也是框架最早支持的之一。它利用一个UserDetailsService (作为一个DAO)来查找用户名、密码和授权.它只是通过将UsernamePasswordAuthenticationToken (用户名密码身份验证令牌)中提交的密码与UserDetailsService(用户详细信息服务)中加载的密码进行比较来验证用户。配置提供程序非常简单:
密码编码器是可选的。密码编码器对从配置的UserDetailsService(用户详细信息服务)返回的UserDetails (用户详细信息)对象中显示的密码进行编码和解码。这将在下面详细讨论。
10.2 UserDetailsService Implementations(用户详细服务实现)
如本参考指南前面所述,大多数身份验证提供者都利用了UserDetails (用户详细信息)和UserDetailsService (用户详细信息服务)接口。回想一下,UserDetailsService的契约是一个单一的方法:
返回的UserDetails是提供给getters的一个接口,以保证非空的认证信息,例如,用户名,密码,授权和用户帐户是否被启用或禁用。大多数身份验证提供者将使用UserDetailsService,即使用户名和密码实际上并未用作身份验证决策的一部分。他们可能只将返回的UserDetails (用户详细信息)对象用于其GrantedAuthority (授权)信息,因为其他一些系统(如LDAP或X.509或CAS等)已经承担了实际验证凭据的责任。
鉴于UserDetailsService的实现非常简单,用户应该很容易使用自己选择的持久性策略来检索身份验证信息。话虽如此,Spring Security确实包含了一些有用的基础实现,我们将在下面讨论。
10.2.1 In-Memory Authentication(内存认证)
简单的使用去创建一个自定义的UserDetailsService实现选择从一个持久性引擎中提取信息,但许多应用程序不需要这么复杂。如果您正在构建一个原型应用程序,或者刚刚开始集成Spring Security,而又不想花时间配置数据库或编写UserDetailsService实现,那么这一点尤其重要。对于这种情况,一个简单的选择是使用安全命名空间中的用户服务元素:
这也支持一个外部属性文件的使用:
属性文件应包含在表单条目
例如
10.2.2 JdbcDaoImpl
Spring Security还包括一个UserDetailsService,它可以从JDBC数据源获取身份验证信息。在内部使用了Spring JDBC,因此它避免了功能齐全的对象关系映射器(ORM)的复杂性,而只是为了存储用户细节。如果您的应用程序确实使用了ORM工具,您可能更愿意编写一个自定义的UserDetailsService (用户详细信息服务)来重用您可能已经创建的映射文件。回到JdbcDaoImpl,下面显示了一个配置示例:
您可以通过修改上面显示的DriverManagerDataSource (驱动程序管理器数据源)来使用不同的关系数据库管理系统。您也可以使用从JNDI获得的全局数据源,就像使用任何其他Spring配置一样。
Authority Groups(权限组)
默认情况下,JdbcDaoImpl为单个用户加载权限,假设权限直接映射到用户(参见数据库模式附录 database schema appendix)。另一种方法是将权限划分成组,并将组分配给用户。有些人更喜欢用这种方法来管理用户权限。
10.3 Password Encoding (密码加密)
Spring Security的 PasswordEncoder接口用于支持密码以某种方式在持久存储中进行编码。你不应该在纯文本中存储密码。始终使用单向密码哈希算法,如bcrypt,它使用一个内置的salt值,该值对于每个存储的密码都不同。不要使用普通的散列函数,如MD5或SHA,甚至是加盐版本。Bcrypt被故意设计得很慢,以阻止离线密码破解,而标准哈希算法速度很快,可以很容易地用于在定制硬件上并行测试数千个密码。你可能认为这不适用于你,因为你的密码数据库是安全的,离线攻击没有风险。如果是这样的话,做一些调查,阅读所有高调的网站,这些网站已经以这种方式被破坏了,并且因为不安全地存储密码而被嘲笑。最好是安全起见。使用org . spring framework . security . crypto . bcrypt . bcryptPasswordencode "是一个很好的安全选择。在其他通用编程语言中也有兼容的实现,因此它也是互操作性的好选择。
如果您使用的是一个已经有哈希密码的遗留系统,那么您将需要使用一个与您当前算法相匹配的编码器,至少直到您可以将您的用户迁移到一个更安全的方案(通常这将涉及要求用户设置一个新密码,因为哈希是不可逆的)。Spring Security具有包含传统的密码编码功能的实现,即org.springframework.security.authentication.encoding包。该DaoAuthenticationProvider可与新的或旧的PasswordEncoder类型注入。
10.3.1 What is a hash?(什么是散列?)
密码哈希不是Spring Security独有的,但是对于不熟悉这一概念的用户来说,它是一个常见的混淆源。哈希(或摘要)算法是一个单向函数,它从一些输入数据(如密码)中产生一段固定长度的输出数据(哈希)。例如,字符串“密码”(十六进制)的MD5哈希为5f4dcc3b5aa765d61d8327deb882cf99散列是“单向的”,因为给定散列值,很难(实际上不可能)获得原始输入,或者任何可能产生该散列值的输入。该属性使得哈希值对于身份验证非常有用。它们可以存储在您的用户数据库中,作为明文密码的替代,即使这些值被泄露,它们也不会立即显示可用于登录的密码。请注意,这也意味着一旦密码被编码,您将无法恢复密码。
10.3.2 Adding Salt to a Hash(hash中添加盐)
使用密码哈希的一个潜在问题是,如果输入使用一个普通单词,绕过哈希的单向属性相对容易。人们倾向于选择相似的密码,而且从以前被黑客攻击的网站上可以找到类似的巨大字典。例如,如果您使用google搜索哈希值5 F4 DCC 3 b5 aa 765d 61d 8327 deb 882 cf 99,您将很快找到原始单词“password”。以类似的方式,攻击者可以从标准单词列表中构建一个哈希字典,并使用它来查找原始密码。有助于防止这种情况的一种方法是使用适当的强密码策略来防止常用词被使用。另一种是在计算哈希值时使用“盐”。这是每个用户的已知数据的附加字符串,在计算哈希之前与密码结合在一起。理想情况下,数据应该尽可能随机,但实际上任何盐值通常都比没有盐值好。使用salt意味着攻击者必须为每个salt值构建单独的哈希字典,这使得攻击更加复杂(但并非不可能)。
Bcrypt在编码时会自动为每个密码生成一个随机的盐值,并以标准格式存储在bcrypt字符串中。
处理salt的传统方法是将SaltSource注入到DaoAuthenticationProvider中,它将获得特定用户的salt值并将其传递给PasswordEncoder。使用bcrypt意味着您不必担心盐处理的细节(例如值存储在哪里),因为这都是在内部完成的。所以我们强烈建议你使用bcrypt,除非你已经有一个单独储存盐的系统。
10.3.3 Hashing and Authentication (哈希和身份验证)
当身份验证提供者(如Spring Security的DaoAuthenticationProvider)需要根据用户的已知值检查提交的身份验证请求中的密码,并且存储的密码以某种方式编码时,提交的值必须使用完全相同的算法进行编码。这取决于您来检查这些是否兼容,因为Spring Security无法控制持久性值。如果在Spring Security中将密码哈希添加到您的身份验证配置中,并且您的数据库包含明文密码,那么身份验证就不可能成功。例如,即使您知道您的数据库正在使用MD5对密码进行编码,并且您的应用程序配置为使用Spring Security的MD5密码编码,仍然有可能出错。例如,当编码器使用十六进制字符串(默认)时,数据库可以用Base 64编码密码。或者,您的数据库可能使用大写,而编码器的输出是小写。请确保您编写了一个测试,以使用已知的密码和salt组合检查配置的密码编码器的输出,并在进一步尝试通过您的应用程序进行身份验证之前,检查它是否与数据库值匹配。使用像bcrypt这样的标准可以避免这些问题。
如果您想直接在Java中生成编码的密码以存储在您的用户数据库中,那么您可以使用密码编码器上的编码方法。
10.4 Jackson Support(json支持)
Spring Security增加了Jackson支持持久Spring Security相关的课程。当使用分布式会话(即会话复制、Spring会话等)时,这可以提高序列化Spring安全相关类的性能。
要使用它,请将JacksonJacksonModules.getModules(ClassLoader)模块注册为 Jackson Modules。