技术概述
1.1。运行环境
Spring Security 3.0须要Java 5.0运行时环境或更高版本。因为Spring Security旨在以独立的方式运行,所以无需将任何特殊配置文件放入Java运行时环境中。特别地,不须要配置特殊的Java认证和受权服务(JAAS)策略文件或将Spring Security放置到常见的类路径位置中。html
一样,若是您正在使用EJB容器或Servlet容器,则无需在任何位置放置任何特殊的配置文件,也不须要在服务器类加载器中包含Spring Security。全部必需的文件将包含在您的应用程序中。java
此设计提供最大的部署时间灵活性,由于您能够简单地将目标工件(不管是JAR,WAR仍是EAR)从一个系统复制到另外一个系统,并将当即起做用。web
1.2。核心组件
在Spring Security 3.0中,弹簧安全核心罐的内容最小化。它再也不包含与Web应用程序安全性,LDAP或命名空间配置相关的任何代码。咱们将在这里查看您在核心模块中找到的一些Java类型。它们表明了框架的构建块,因此若是你须要超越一个简单的命名空间配置,那么重要的是你明白它们是什么,即便你不须要直接与它们进行交互。算法
1.2.1。 SecurityContextHolder,SecurityContext和Authentication Objects
最基本的对象是SecurityContextHolder。这是咱们存储应用程序当前安全上下文的详细信息,其中包括当前正在使用应用程序的主体的详细信息。默认状况下,SecurityContextHolder使用ThreadLocal来存储这些详细信息,这意味着安全上下文老是可用于同一执行线程中的方法,即便安全上下文未做为参数显式传递给这些方法。这样使用ThreadLocal是很是安全的,若是在处理当前主体的请求以后注意清除线程。固然,Spring Security会自动处理这个,因此没有必要担忧。spring
某些应用程序不彻底适用于使用ThreadLocal,由于它们与线程配合使用的具体方式。例如,Swing客户端可能但愿Java虚拟机中的全部线程都使用相同的安全上下文。 SecurityContextHolder能够经过启动策略进行配置,以指定如何存储上下文。对于独立应用程序,您将使用SecurityContextHolder.MODE_GLOBAL策略。其余应用程序可能但愿由安全线程产生的线程也承担相同的安全身份。这是经过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL实现的。您能够经过两种方式从默认SecurityContextHolder.MODE_THREADLOCAL更改模式。一是设置系统属性,二是调用SecurityContextHolder上的静态方法。大多数应用程序不须要从默认更改,但若是这样作,请查看SecurityContextHolder的JavaDocs了解更多信息。sql
获取有关当前用户的信息数据库
在SecurityContextHolder中,咱们存储当前正在与应用程序交互的主体的详细信息。 Spring Security使用Authentication对象来表示此信息。您一般不须要本身建立一个Authentication对象,但用户查询Authentication对象是至关广泛的。您可使用如下代码块(从应用程序的任何位置)获取当前验证的用户的名称,例如:编程
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
调用getContext()返回的对象是SecurityContext接口的一个实例。这是保存在线程本地存储中的对象。正如咱们将在下面看到的,使用Spring Security的大多数身份验证机制将返回UserDetails的实例做为主体。数组
1.2.2。 UserDetailsService
从上述代码片断中要注意的另外一个项目是能够从Authentication对象获取主体。校长只是一个对象。大部分时间能够转换成UserDetails对象。 UserDetails是Spring Security中的核心界面。它表明一个委托人,可是在可扩展和应用程序特定的方式。将UserDetails视为您本身的用户数据库之间的适配器,以及SecurityContextHolder中须要的Spring Security。做为一个表明你本身的用户数据库的东西,不少时候你会将UserDetails转换成你应用程序提供的原始对象,因此你能够调用业务特定的方法(如`getEmail(),'getEmployeeNumber()等等) 。浏览器
如今你可能想知道,因此我何时提供一个UserDetails对象?我怎么作?我觉得你说这个东西是声明性的,我不须要编写任何Java代码 - 给出了什么?简单的答案是有一个名为UserDetailsService的特殊界面。此接口上惟一的方法接受基于String的用户名参数,并返回UserDetails:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
成功认证后,UserDetails用于构建存储在SecurityContextHolder中的Authentication对象(更多的内容)。 好消息是,咱们提供了一些UserDetailsService实现,包括使用内存映射(InMemoryDaoImpl)和另外一个使用JDBC(JdbcDaoImpl)的实现。 大多数用户倾向于本身编写,可是实现方式每每只是位于现有的数据访问对象(DAO)之上,它表明了他们的员工,客户或其余应用程序的用户。 记住,不管您的UserDetailsService返回的老是可使用上述代码片断从SecurityContextHolder获取的优势。
这每每是对UserDetailsService有些混淆。 它纯粹是用户数据的DAO,不执行其余功能,而不是将数据提供给框架内的其余组件。 特别地,它不认证用户,这是由AuthenticationManager完成的。 在许多状况下,若是须要自定义身份验证过程,则直接 implement AuthenticationProvider更有意义。
1.2.3。一个GrantedAuthority
除了主体外,Authentication提供的另外一个重要方法是getAuthorities()。此方法提供了一个GrantedAuthority对象的数组。授予的受权是绝不奇怪的,授予委托人的受权。这些权限一般是“角色”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。这些角色后来配置为Web受权,方法受权和域对象受权。 Spring Security的其余部分可以解释这些权力机构,并指望他们可以出席。 GrantedAuthority对象一般由UserDetailsService加载。
一般GrantedAuthority对象是应用程序范围的权限。它们不是特定于给定的域对象。所以,您不可能有GrantedAuthority表示对Employee对象编号54的权限,由于若是有成千上万的此类权限,您将很快用尽内存(或至少致使应用程序花费很长时间验证用户的时间)。固然,Spring Security也是为了处理这个共同的要求而设计的,可是你也可使用项目的域对象安全功能。
1.2.4。概要
只是总结一下,到目前为止咱们看到的Spring Security的主要组成部分是:
- SecurityContextHolder,提供对SecurityContext的访问。
- SecurityContext,用于保存身份验证和可能的特定于请求的安全信息。
- Authentication,以Spring Security特定的方式表示主体。
- GrantedAuthority,以反映授予主体的应用程序范围的权限。
- UserDetails,提供从应用程序的DAO或其余安全数据源构建Authentication对象所需的信息。
- UserDetailsService,在基于字符串的用户名(或证书ID等)中传递时建立UserDetails。
如今,您已经了解了这些重复使用的组件,咱们来仔细看看认证过程。
1.3。认证
Spring Security能够参与许多不一样的认证环境。虽然咱们建议人们使用Spring Security进行身份验证,而不是与现有的容器管理身份验证集成,但它仍然受到支持 - 与您本身的专有身份验证系统集成在一块儿。
1.3.1。什么是Spring Security中的身份验证?
让咱们考虑每一个人都熟悉的标准认证场景。
- 提示用户使用用户名和密码登陆。
- 系统(成功)验证用户名的密码是否正确。
- 得到该用户的上下文信息(其角色列表等)。
- 为用户创建安全上下文
- 用户继续进行,潜在地执行可能由访问控制机制保护的一些操做,该访问控制机制根据当前的安全上下文信息检查针对该操做的所需权限。
前三个项目构成了身份验证过程,所以咱们将看看Spring Security中如何发生这种状况。
- 获取用户名和密码并将其组合为UsernamePasswordAuthenticationToken(咱们前面看到的Authentication接口的一个实例)的一个实例。
- 令牌被传递给AuthenticationManager的实例以进行验证。
- AuthenticationManager在成功认证时返回彻底填充的Authentication实例。
- 安全上下文经过调用SecurityContextHolder.getContext()。setAuthentication(...),传入返回的认证对象来创建。
从那时起,用户被认为是认证的。咱们来看一些代码做为例子。
import org.springframework.security.authentication.*; import org.springframework.security.core.*; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; public class AuthenticationExample { private static AuthenticationManager am = new SampleAuthenticationManager(); public static void main(String[] args) throws Exception { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while(true) { System.out.println("Please enter your username:"); String name = in.readLine(); System.out.println("Please enter your password:"); String password = in.readLine(); try { Authentication request = new UsernamePasswordAuthenticationToken(name, password); Authentication result = am.authenticate(request); SecurityContextHolder.getContext().setAuthentication(result); break; } catch(AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } } System.out.println("Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication()); } } class SampleAuthenticationManager implements AuthenticationManager { static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>(); static { AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER")); } public Authentication authenticate(Authentication auth) throws AuthenticationException { if (auth.getName().equals(auth.getCredentials())) { return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES); } throw new BadCredentialsException("Bad Credentials"); } }
这里咱们写了一个程序,要求用户输入用户名和密码,并执行上述顺序。 咱们在这里实现的AuthenticationManager将验证其用户名和密码相同的用户。 它为每一个用户分配一个角色。 上面的输出将是:
Please enter your username: bob Please enter your password: password Authentication failed: Bad Credentials Please enter your username: bob Please enter your password: bob Successfully authenticated. Security context contains: \ org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: \ Principal: bob; Password: [PROTECTED]; \ Authenticated: true; Details: null; \ Granted Authorities: ROLE_USER
请注意,您一般不须要编写这样的代码。该过程一般在内部进行,例如在Web认证过滤器中。咱们刚刚将代码放在这里,以代表Spring Security中实际构成身份验证的问题有一个简单的答案。当SecurityContextHolder包含彻底填充的Authentication对象时,用户将进行身份验证。
1.3.2。直接设置SecurityContextHolder内容
事实上,Spring Security不介意如何将Authentication对象放在SecurityContextHolder中。惟一的关键要求是SecurityContextHolder包含一个Authentication,它表明AbstractSecurityInterceptor以前的一个主体(咱们将在后面看到更多信息)须要受权用户操做。
您能够(而且许多用户)编写本身的过滤器或MVC控制器,以提供与基于Spring Security的身份验证系统的互操做性。例如,您可能正在使用容器管理身份验证,这使得当前用户能够从ThreadLocal或JNDI位置得到。或者您能够为拥有传统专有认证系统的公司工做,这是一种企业“标准”,您几乎没法控制。在这样的状况下,很容易让Spring Security工做,而且仍然提供受权功能。全部您须要作的是编写从位置读取第三方用户信息的过滤器(或等同物),构建一个特定于Spring Security的Authentication对象,并将其放入SecurityContextHolder中。在这种状况下,您还须要考虑一般由内置身份验证基础设施自动处理的内容。例如,在将响应写入客户端以前,您可能须要先占用建立一个HTTP会话来缓存请求之间的上下文。
若是您想知道AuthenticationManager是如何在现实世界中实现的,那么咱们将在核心服务章节中看一下。
1.4。 Web应用程序中的身份验证
如今让咱们来探讨在Web应用程序中使用Spring Security的状况(不启用web.xml安全性)。用户如何认证和安全上下文创建?
考虑一个典型的Web应用程序的认证过程:
- 您访问主页,而后单击连接。
- 请求发送到服务器,而且服务器决定您已请求受保护的资源。
- 因为您目前没有身份验证,服务器会发回一个代表您必须验证的响应。响应将是HTTP响应代码或重定向到特定网页。
- 根据身份验证机制,您的浏览器将重定向到特定网页,以便您填写表单,或者浏览器将以某种方式检索您的身份(经过BASIC身份验证对话框,Cookie,X.509证书等) )。
- 浏览器将向服务器发送回应。这将是一个包含您填写的表单内容的HTTP POST,或包含您的身份验证详细信息的HTTP头。
- 接下来,服务器将决定所提供的凭证是否有效。若是它们有效,下一步就会发生。若是它们无效,一般您的浏览器将被要求再次尝试(因此您返回到上面的第二步)。
- 将重试您致使身份验证过程的原始请求。但愿您已经过足够受权的权限验证访问受保护的资源。若是您有足够的访问权限,则请求将成功。不然,您将收到一个HTTP错误代码403,这意味着“禁止”。
Spring Security具备不一样的类,负责上述大部分步骤。主要参与者(使用它们的顺序)是ExceptionTranslationFilter,AuthenticationEntryPoint和“认证机制”,它负责调用咱们上一节中看到的AuthenticationManager。
1.4.1。ExceptionTranslationFilter
ExceptionTranslationFilter是一个Spring Security过滤器,负责检测抛出的任何Spring Security异常。这种异常一般将由受权服务的主要提供者AbstractSecurityInterceptor抛出。咱们将在下一节中讨论AbstractSecurityInterceptor,可是如今咱们只须要知道它会生成Java异常,而且什么也不知道HTTP或者如何去认证一个主体。相反,ExceptionTranslationFilter提供此服务,具体负责返回错误代码403(若是主体已经经过身份验证,所以彻底缺少足够的访问权限,如上面的第七步),或启动AuthenticationEntryPoint(若是主体未被验证,因此咱们须要开始步骤三)。
1.4.3。认证机制
一旦您的浏览器提交您的身份验证凭据(做为HTTP表单或HTTP标头),服务器上须要“收集”这些身份验证详细信息。到目前为止,咱们在上面列出的第六步。在Spring Security中,咱们有一个特殊名称,用于从用户代理(一般是Web浏览器)收集认证详细信息,将其称为“身份验证机制”。示例是表单登陆和基自己份验证。一旦从用户代理收集了认证详细信息,就构建了一个认证“请求”对象,而后呈现给AuthenticationManager。
认证机制接收到彻底填充的认证对象后,会认为该请求有效,将认证放入SecurityContextHolder,并引发原始请求重试(上述第七步)。另外一方面,若是AuthenticationManager拒绝了请求,则认证机制将要求用户代理重试(第二步)。
1.4.4。在请求之间存储SecurityContext
根据应用程序的类型,可能须要有一个策略来存储用户操做之间的安全上下文。在典型的Web应用程序中,用户登陆一次,随后由其会话ID标识。服务器缓存持续时间会话的主体信息。在Spring Security中,将SecurityContext存储在请求之间的责任归结于SecurityContextPersistenceFilter,默认状况下,它将上下文做为HTTP请求之间的HttpSession属性存储。它将每一个请求的上下文恢复到SecurityContextHolder,而且相当重要的是,在请求完成时清除SecurityContextHolder。为了安全起见,您不该该直接与HttpSession交互。这样作根本没有理由 - 老是使用SecurityContextHolder。
许多其余类型的应用程序(例如,无状态RESTful Web服务)不使用HTTP会话,并将在每一个请求上从新进行身份验证。可是,SecurityContextPersistenceFilter包含在链中仍然很重要,以确保每一个请求后SecurityContextHolder被清除。
在单个会话中接收并发请求的应用程序中,相同的SecurityContext实例将在线程之间共享。 即便正在使用ThreadLocal,它是从HttpSession为每一个线程检索的实例。 若是您但愿临时更改线程正在运行的上下文,则会产生影响。 若是您只是使用SecurityContextHolder.getContext(),并在返回的上下文对象上调用setAuthentication(anAuthentication),则认证对象将在共享相同SecurityContext实例的全部并发线程中更改。 您能够自定义SecurityContextPersistenceFilter的行为,为每一个请求建立一个全新的SecurityContext,从而防止一个线程中的更改影响另外一个线程。 或者,您能够在临时更改上下文的位置建立一个新的实例。 方法SecurityContextHolder.createEmptyContext()老是返回一个新的上下文实例。
1.5。 Spring Security中的访问控制(受权)
负责在Spring Security中进行访问控制决策的主要接口是AccessDecisionManager。它具备一个决定方法,该方法采用表示主要请求访问的Authentication对象,“安全对象”(见下文)和适用于对象的安全元数据属性列表(例如访问所需的角色列表被授予)。
1.5.1。安全和AOP建议
若是您熟悉AOP,您会发现有不一样类型的建议可供选择:以前,以后,投掷和周围。周围的建议是很是有用的,由于顾问能够选择是否继续进行方法调用,不管是否修改响应,以及是否抛出异常。 Spring Security提供了方法调用以及Web请求的各类建议。咱们使用Spring的标准AOP支持来实现方法调用的各类建议,咱们使用标准过滤器来实现对Web请求的各类建议。
对于不熟悉AOP的人员,要了解的重点是Spring Security能够帮助您保护方法调用以及Web请求。大多数人都有兴趣在其服务层上确保方法调用。这是由于服务层是大多数业务逻辑驻留在当前一代Java EE应用程序中的地方。若是您只须要在服务层中保护方法调用,Spring的标准AOP就足够了。若是您须要直接安全域对象,您可能会发现AspectJ值得考虑。
您能够选择使用AspectJ或Spring AOP执行方法受权,也能够选择使用过滤器执行Web请求受权。您能够一块儿使用这些方法中的零,一,二或三。主流使用模式是执行一些Web请求受权,再加上服务层上的一些Spring AOP方法调用受权。
1.5.2。安全对象和AbstractSecurityInterceptor
那么什么是“安全对象”呢? Spring Security使用该术语来引用能够具备应用于其的安全性(例如受权决策)的任何对象。最多见的示例是方法调用和Web请求。
每一个受支持的安全对象类型都有本身的拦截器类,它是AbstractSecurityInterceptor的子类。重要的是,在调用AbstractSecurityInterceptor时,若是主体已通过身份验证,SecurityContextHolder将包含有效的验证。
AbstractSecurityInterceptor为处理安全对象请求提供了一致的工做流,一般为
- 查找与本请求相关联的“配置属性”
- 将安全对象,当前身份验证和配置属性提交给AccessDecisionManager进行受权决定
- 可选地,更改发起调用的身份验证
- 容许安全对象调用继续(假设访问被授予)
- 一旦调用返回,调用AfterInvocationManager。若是调用引起异常,则不会调用AfterInvocationManager。
什么是配置属性?
“配置属性”能够被认为是对AbstractSecurityInterceptor使用的类具备特殊含义的String。它们由框架内的接口ConfigAttribute表示。它们多是简单的角色名称或具备更复杂的含义,这取决于AccessDecisionManager实现的复杂程度。 AbstractSecurityInterceptor配置有一个SecurityMetadataSource,它用于查找安全对象的属性。一般这个配置将被用户隐藏。配置属性将做为安全方法上的注释或做为受保护URL上的访问属性输入。例如,当咱们在命名空间介绍中看到相似<intercept-url pattern ='/ secure / **'access ='ROLE_A,ROLE_B'/>的内容时,这就是说配置属性ROLE_A和ROLE_B适用于匹配的Web请求给定的模式。实际上,使用默认的AccessDecisionManager配置,这意味着任何具备与这两个属性匹配的GrantedAuthority的用户将被容许访问。严格来讲,它们只是属性,解释取决于AccessDecisionManager的实现。使用前缀ROLE_是一个标记,用于指示这些属性是角色,应由Spring Security的“RoleVoter”消耗。这仅在使用基于投票人的AccessDecisionManager时才有用。咱们将在受权章节中看到AccessDecisionManager的实现。
RunAsManager
假设AccessDecisionManager决定容许请求,那么AbstractSecurityInterceptor一般只会继续执行该请求。话虽如此,在极少数状况下,用户可能但愿使用由AccessDecisionManager调用RunAsManager处理的不一样身份验证来替换SecurityContext内的Authentication。这在至关不寻常的状况下多是有用的,例如,若是服务层方法须要调用远程系统并呈现不一样的身份。由于Spring Security自动将安全身份从一个服务器传播到另外一个服务器(假设您正在使用正确配置的RMI或HttpInvoker远程处理协议客户端),这可能颇有用。
AfterInvocationManager
在安全对象调用进行而后返回 - 这可能意味着方法调用完成或过滤器链进行 - AbstractSecurityInterceptor得到处理调用的最后机会。在这个阶段,AbstractSecurityInterceptor有可能修改返回对象。咱们可能但愿发生这种状况,由于在安全对象调用中没法“受权”决定。因为高度可插拔,AbstractSecurityInterceptor会将控件传递给AfterInvocationManager,以便在须要时实际修改该对象。这个类甚至能够彻底替换对象,也能够抛出异常,或者不以任何方式更改它。后调用检查只有在调用成功时才会执行。若是发生异常,将跳过附加检查。
AbstractSecurityInterceptor及其相关对象显示在安全拦截器和“安全对象”模型中
图1.安全拦截器和“安全对象”模型
扩展安全对象模型
只有考虑采用全新方式拦截和受权请求的开发人员才须要直接使用安全对象。例如,能够构建一个新的安全对象来保护对消息系统的调用。任何须要安全性的东西,也提供了一种截取通话语法的方式(如围绕通知语义的AOP),能够被作成一个安全的对象。话虽如此,大多数Spring应用程序将简单地使用三个当前支持的安全对象类型(AOP Alliance MethodInvocation,AspectJ JoinPoint和Web请求FilterInvocation),具备完整的透明度。
1.6。本土化
Spring Security支持最终用户可能看到的异常消息的本地化。若是您的应用程序是为英语用户设计的,那么您无需执行任何操做,所以默认状况下全部安全安全消息均为英文。若是您须要支持其余语言环境,则您须要知道的一切都包含在本节中。
全部异常消息均可以进行本地化,包括与认证失败相关的消息和拒绝访问(受权失败)。专一于开发人员或系统部署者的异常和日志记录消息(包括不正确的属性,违规接口违规,使用不正确的构造函数,启动时间验证,调试级日志记录)不会本地化,而是在Spring Security的代码中用英文硬编码。
在spring-security-core-xx.jar中发送,您将找到一个org.springframework.security包,该包又包含一个messages.properties文件,以及一些经常使用语言的本地化版本。这应该由你的`ApplicationContext`引用,由于Spring Security类实现了Spring的MessageSourceAware接口,并指望消息解析器在应用程序上下文启动时被注入。一般全部您须要作的是在应用程序上下文中注册一个bean来引用消息。一个例子以下所示:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename" value="classpath:org/springframework/security/messages"/> </bean>
messages.properties根据标准资源束命名,并表示Spring Security消息支持的默认语言。此默认文件为英文。
若是您但愿自定义messages.properties文件或支持其余语言,则应该复制文件,并相应地重命名,并将其注册到上述bean定义中。这个文件中没有大量的消息密钥,因此本地化不该该被认为是主要的。若是您确实执行此文件的本地化,请考虑经过记录JIRA任务并附加适当命名的本地化版本的messages.properties来与社区共享您的工做。
Spring Security依赖于Spring的本地化支持,以便实际查找相应的消息。为了使其工做,您必须确保来自传入请求的区域设置存储在Spring的org.springframework.context.i18n.LocaleContextHolder中。 Spring MVC的DispatcherServlet自动为您的应用程序执行此操做,可是因为Spring Security的过滤器在此以前被调用,所以LocaleContextHolder须要设置为在调用过滤器以前包含正确的区域设置。你能够本身作一个过滤器(它必须来自web.xml中的Spring Security过滤器),或者你可使用Spring的RequestContextFilter。有关使用Spring进行本地化的更多详细信息,请参阅Spring Framework文档。
“contacts”示例应用程序设置为使用本地化消息。
2. 核心服务
如今咱们对Spring Security架构及其核心类进行了高级的概述,咱们来仔细研究一两个核心接口及其实现,特别是AuthenticationManager,UserDetailsService和AccessDecisionManager。这些文件的其他部分会按期出现,所以重要的是您知道它们的配置及其操做方式。
2.1。 AuthenticationManager,ProviderManager和AuthenticationProvider
AuthenticationManager只是一个接口,因此实现能够是咱们选择的任何东西,但它在实践中如何工做?若是咱们须要检查多个身份验证数据库或不一样身份验证服务(如数据库和LDAP服务器)的组合,该怎么办?
Spring Security中的默认实现被称为ProviderManager,而不是处理身份验证请求自己,它将委托给已配置的AuthenticationProvider的列表,每一个都会依次查询,以查看是否能够执行身份验证。每一个提供者将抛出异常或返回彻底填充的Authentication对象。记住咱们的好朋友,UserDetails和UserDetailsService?若是没有,回到上一章,刷新你的记忆。验证身份验证请求的最多见方法是加载相应的UserDetails,并根据用户输入的密码检查加载的密码。这是DaoAuthenticationProvider使用的方法(见下文)。当构建彻底填充的Authentication对象(从成功的认证返回并存储在SecurityContext中)时,将使用加载的UserDetails对象(特别是包含的GrantedAuthority)。
若是您使用命名空间,则会在内部建立和维护ProviderManager实例,并经过使用命名空间认证提供程序元素(请参阅命名空间章节)向其添加提供程序。在这种状况下,您不该该在应用程序上下文中声明一个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>
在上面的例子中,咱们有三个提供者。它们按照显示的顺序进行尝试(这经过使用列表来表示),每一个提供者均可以尝试身份验证,或者经过简单地返回null来跳过身份验证。若是全部的实现都返回null,那么ProviderManager将抛出一个ProviderNotFoundException。若是您有兴趣了解有关连接提供程序的更多信息,请参阅ProviderManager JavaDocs。
注册Web表单登陆处理过滤器之类的身份验证机制引用了ProviderManager,并将其称之为处理其身份验证请求。您须要的提供商有时能够与身份验证机制互换,而在其余时间,它们将取决于特定的身份验证机制。例如,DaoAuthenticationProvider和LdapAuthenticationProvider与提交简单的用户名/密码认证请求的任何机制兼容,所以可使用基于表单的登陆或HTTP基自己份验证。另外一方面,一些认证机制建立一个认证请求对象,该对象只能由一种类型的AuthenticationProvider来解释。一个例子是JA-SIG CAS,它使用服务票据的概念,所以只能由一个CasAuthenticationProvider进行身份验证。您没必要太在乎这一点,由于若是您忘记注册一个合适的提供程序,则当尝试进行身份验证时,您将收到一个ProviderNotFoundException。
2.1.1。Erasing Credentials on Successful Authentication
默认状况下(从Spring Security 3.1起),ProviderManager将尝试从认证对象中清除成功认证请求返回的任何敏感凭证信息。这样能够防止密码保留的时间长于必要的信息。
当您使用用户对象缓存时,可能会致使问题,例如提升无状态应用程序的性能。若是身份验证包含对缓存中对象的引用(例如UserDetails实例),并删除其凭据,则将没法再对缓存的值进行身份验证。若是您正在使用缓存,则须要考虑这一点。一个明显的解决方案是首先建立对象的副本,不管是在缓存实现中仍是在建立返回的Authentication对象的AuthenticationProvider中。或者,您能够禁用ProviderManager上的eraseCredentialsAfterAuthentication属性。有关更多信息,请参阅Javadoc。
2.1.2。DaoAuthenticationProvider
Spring Security实现的最简单的AuthenticationProvider是DaoAuthenticationProvider,它也是框架最先支持的。它利用UserDetailsService(做为DAO)来查找用户名,密码和GrantedAuthority。它只需经过将UsernamePasswordAuthenticationToken中提交的密码与UserDetailsService加载的密码进行比较便可对用户进行身份验证。配置提供程序很简单:
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="inMemoryDaoImpl"/> <property name="passwordEncoder" ref="passwordEncoder"/> </bean>
PasswordEncoder是可选的。 PasswordEncoder提供从配置的UserDetailsService返回的UserDetails对象中显示的密码的编码和解码。 这将在下面更详细地讨论。
2.2。 UserDetailsService实现
如本参考指南中的前面提到的,大多数认证提供商利用UserDetails和UserDetailsService接口。 回想一下,UserDetailsService的合同是一种单一的方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的UserDetails是一个接口,它提供了容许非空提供验证信息(如用户名,密码,受权的权限)以及用户账户是启用仍是禁用的getter。大多数身份验证提供程序将使用“UserDetailsService”,即便用户名和密码实际上不被用做身份验证决策的一部分。因为某些其余系统(如LDAP或X.509或CAS等)承担了实际验证凭据的责任,他们可能会将其返回的UserDetails对象用于其GrantedAuthority信息。
给定UserDetailsService是很是简单的实现,用户可使用他们选择的持久性策略来检索身份验证信息很容易。话虽如此,Spring Security确实包含了一些有用的基础实现,下面咱们来看一下。
2.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]
For example
jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled bob=bobspassword,ROLE_USER,enabled
2.2.2。 JdbcDaoImpl
Spring Security还包括能够从JDBC数据源获取认证信息的UserDetailsService。使用内部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配置同样。
权限组
默认状况下,JdbcDaoImpl加载单个用户的权限,假设当局直接映射到用户(参见数据库模式附录)。另外一种方法是将权限划分为组,并将组分配给用户。有些人喜欢这种方式,是管理用户权利的一种手段。有关如何启用组受权的更多信息,请参阅JdbcDaoImpl Javadoc。组织架构也包含在附录中。
2.3。PasswordEncoder
Spring Security的PasswordEncoder接口用于支持使用以某种方式在持久存储中编码的密码。您不该该以纯文本形式存储密码。请务必使用bcrypt等单向密码散列算法,该算法使用内置的每一个存储密码不一样的盐值。不要使用简单的哈希函数,如MD5或SHA,甚至是盐化版本。 Bcrypt故意设计为缓慢,并阻止离线密码破解,而标准散列算法快速,能够轻松地用于在定制硬件上并行测试数千个密码。您可能认为这并不适用于您,由于您的密码数据库是安全的,脱机攻击并非风险。若是是这样作,请进行一些研究,并阅读全部以这种方式受到妥协的高调网站,并为保护密码不安全而被劫持。最好在安全的一面。使用org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder“是安全性的一个不错的选择,还有其余经常使用编程语言中的兼容实现,所以它也是互操做性的好选择。
若是您使用已经散布密码的遗留系统,那么您将须要使用与当前算法相匹配的编码器,至少直到您将用户迁移到更安全的方案(一般这将涉及要求用户设置一个新的密码,由于哈希是不可逆转的)。 Spring Security具备包含传统密码编码实现的包,即org.springframework.security.authentication.encoding。 DaoAuthenticationProvider能够注册新的或旧的PasswordEncoder类型。
2.3.1。什么是hash?
Password hashing不是Spring Security所特有的,但对于不熟悉概念的用户来讲,这是一个常见的混乱来源。散列(或摘要)算法是一种单向函数,它从一些输入数据(如密码)产生一段固定长度的输出数据(散列)。例如,字符串“密码”(十六进制)的MD5哈希值是 5f4dcc3b5aa765d61d8327deb882cf99
在一个意义上说,散列是“单向的”,即在给定散列值的状况下得到原始输入,或者甚至可能产生该散列值的任何可能的输入是很是困难的(其实是不可能的)。此属性使哈希值对于身份验证很是有用。它们能够存储在您的用户数据库中,做为纯文本密码的替代方法,即便这些值受到威胁,也不会当即显示可用于登陆的密码。请注意,这也意味着您没法在编码密码后恢复密码。
2.3.2。Adding Salt to a Hash
使用密码哈希的一个潜在问题是,若是使用通用单词做为输入,则相对容易获得散列的单向属性。人们倾向于选择相似的密码,从之前被黑客攻击的网站上能够在网上得到这些密码的巨大字典。例如,若是您使用谷歌搜索哈希值5f4dcc3b5aa765d61d8327deb882cf99,您将很快找到原始单词“密码”。以相似的方式,攻击者能够从标准单词列表中构建散列词典,并使用它来查找原始密码。帮助防止这种状况的一种方法是具备适当强度的密码策略,以防止使用经常使用单词。另外一个是在计算哈希时使用“盐”。这是在计算哈希以前与每一个用户的一个额外的一串已知数据,与密码相结合。理想状况下,数据应尽量随机,但实际上任何盐值一般都优于无。使用盐意味着攻击者必须为每一个盐值构建一个单独的哈希字典,使攻击更加复杂(但不是不可能)。
Bcrypt在编码时自动为每一个密码生成随机盐值,并以标准格式将其存储在bcrypt字符串中。
处理盐的传统方法是将SaltSource注入到DaoAuthenticationProvider中,该方法将为特定用户获取盐值并将其传递给PasswordEncoder。 使用bcrypt意味着您没必要担忧盐处理的细节(例如存储值的位置),由于它们都在内部完成。 因此咱们强烈推荐你使用bcrypt,除非你已经有一个系统存在盐分开。
2.3.3。Hashing and Authentication
当认证提供者(如Spring Security的DaoAuthenticationProvider)须要根据用户的已知值检查提交的认证请求中的密码,而且以某种方式对存储的密码进行编码时,必须使用彻底相同的方式对提交的值进行编码算法。因为Spring Security没法控制持久值,所以您能够检查这些是否兼容。若是您在Spring Security中为您的身份验证配置添加了密码哈希值,而且数据库中包含明文密码,则认证没法成功。即便您知道您的数据库使用MD5编码密码,例如,您的应用程序配置为使用Spring Security的Md5PasswordEncoder,仍然有可能出现问题。数据库可能具备编码在Base 64中的密码,例如编码器使用十六进制字符串(默认值)时。或者,您的数据库可能使用大写,而编码器的输出为小写。确保您使用已知的密码和盐组合来编写测试以检查已配置密码编码器的输出,并检查它是否符合数据库值,而后再进一步尝试经过应用程序进行身份验证。使用像bcrypt这样的标准将避免这些问题。
若是要直接在Java中生成编码密码以存储在用户数据库中,则能够在PasswordEncoder上使用encode方法。