借助于Spring Security的强大基础配置功能以及内置的认证功能,我们在前面讲述的三步配置是很快就能完成的;它们的使用是通过添加auto-config属性和http元素实现的。
但不幸的是,应用实现的考量、架构的限制以及基础设施集成的要求可能使你的Spring Security实现远较这个简单的配置所提供的复杂。很多用户一使用比基本配置复杂的功能就会遇到麻烦,那是因为他们不了解这个产品的架构以及各个元素是如何协同工作以实现一个整体的。
理解web请求的整体流程以及它们是如何穿越实现功能的拦截器链,对我们成功了解Spring Security的高级话题至关重要。记住认证和授权的基本概念,因为它们贯穿我们要保护的系统架构的始终。
Spring Security的架构在很大程度上依赖代理(delegates)和servlet过滤器,来实现环绕在web应用请求前后的功能层。
Servlet过滤器(Servlet Filter,实现javax.servlet.Filter接口的类)被用来拦截用户请求来进行请求之前或之后的处理,或者干脆重定向这个请求,这取决于servlet过滤器的功能。在JBCP Pets在线商店中,最终的目标servlet是Spring MVC 分发servlet,但是在理论上它可能是任何一个web servlet。下面的图描述了一个servlet过滤器是如何封装一个用户请求的:
正如你能从链这个词汇中推断出的那样,servlet请求按照一定的顺序从一个过滤器到下一个穿过整个过滤器链,最终到达目标servlet。与之相对的是,当servelt处理完请求并返回一个response时,过滤器链按照相反的顺序再次穿过所有的过滤器。
Spring Security使用了过滤器链的概念并实现了自己抽象,提供了VirtualFilterChain,它可以根据Spring Security XML配置文件中设置的URL模式动态的创建过滤器链(可以将它与标准的Java EE过滤器链进行对比,后者需要在web应用的部署描述文件中进行设置)。
【Servlet过滤器除了能够如它的名字所描述的那样进行过滤功能(或阻止请求)以外,还可以用于很多其他的目的。实际上,很多的servlet过滤器的功能类似于在web运行的环境中对请求进行AOP式的代理拦截,因为它们可以允许一些功能在任何发往servelt容器的请求处理之前或之后执行。过滤器能实现的多功能在Spring Security中页得到了体现,因为很多过滤器实际上并不直接影响用户的请求。】
自动配置的选项为你建立了十个Spring Security的过滤器。理解这些过滤器的默认行为以及它们在哪里以及如何配置的,对使用Spring Security的高级功能至关重要。
这些过滤器以及它们使用的顺序,在下面的表格中进行了描述。大多数这些过滤器在我们完善JBCP Pets在线商店的过程中都会被再次提到,所以如果你现在不明白它们的确切功能也不必担心。
过滤器名称 | 描述 |
o.s.s.web.context.SecurityContextPersistenceFilter | 负责从SecurityContextRepository获取或存储SecurityContext。SecurityContext代表了用户安全和认证过的session。 |
o.s.s.web.authentication.logout.LogoutFilter | 监控一个实际为退出功能的URL(默认为/j_spring_security_logout),并且在匹配的时候完成用户的退出功能。 |
o.s.s.web.authentication.UsernamePasswordAuthenticationFilter | 监控一个使用用户名和密码基于form认证的URL(默认为/j_spring_security_check),并在URL匹配的情况下尝试认证该用户。 |
o.s.s.web.authentication.ui.DefaultLoginPageGeneratingFilter | 监控一个要进行基于forn或OpenID认证的URL(默认为/spring_security_login),并生成展现登录form |
o.s.s.web.authentication.www.BasicAuthenticationFilter | 监控HTTP 基础认证的头信息并进行处理 |
o.s.s.web.savedrequest.RequestCacheAwareFilter |
用于用户登录成功后,重新恢复因为登录被打断的请求。 |
o.s.s.web.servletapi.SecurityContextHolderAwareRequestFilter |
用一个扩展了HttpServletRequestWrapper的子类(o.s.s.web. servletapi.SecurityContextHolderAwareRequestWrapper)包装HttpServletRequest。它为请求处理器提供了额外的上下文信息。 |
o.s.s.web.authentication.AnonymousAuthenticationFilter |
如果用户到这一步还没有经过认证,将会为这个请求关联一个认证的token,标识此用户是匿名的。 |
o.s.s.web.session.SessionManagementFilter |
根据认证的安全实体信息跟踪session,保证所有关联一个安全实体的session都能被跟踪到。 |
o.s.s.web.access.ExceptionTranslationFilter |
解决在处理一个请求时产生的指定异常 |
o.s.s.web.access.intercept.FilterSecurityInterceptor |
简化授权和访问控制决定,委托一个AccessDecisionManager完成授权的判断 |
Spring Security拥有总共大约25个过滤器,它们能够根据你的需要进行适当的应用以改变用户请求的行为。当然,如果需要的话,你也可以添加你自己实现了javax.servlet.Filter接口的过滤器。
请记住,如果你在XML配置文件中使用了auto-config属性,以上表格中列出的过滤器自动添加的。通过使用一些额外的配置指令,以上列表中的过滤器能够精确的控制是否被包含,在后续的章节章将会进行介绍。
你可能会完全从头做起来配置过滤器链。尽管这会很单调乏味,因为有很多的依赖关系要配置,但是它为配置和应用场景的匹配方面提供了更高层次的灵活性。我们将在第六章讲述在启动的过程中所依赖的Spring Bean的声明。
你可能想知道DelegatingFilterProxy是怎样找到Spring Security配置的过滤器链的。让我们回忆一下,在web.xml文件中,我们需要给DelegatingFilterProxy一个过滤器的名字:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> </filter>
这个过滤器的名字并不是随意配置的,实际上会跟根据这个名字把Spring Security织到DelegatingFilterProxy。
除非明确配置,否则DelegatingFilterProxy会在Spring WebApplicationContext中寻找同名的配置bean(名字是在filter-name中指明的)。更多配置DelegatingFilterProxy的细节可以在这个类对应的Javadoc中找到。
在Spring Security 3中,使用auto-config会自动提供以下三个认证相关的功能:
HTTP基本认证
Form登录认证
退出
值得注意的是,也可以使用配置元素实现这三个功能,能够实现比使用auto-config提供的功能更精确。我们将在随后的章节中看到它们的使用以提供更高级的功能。
【auto-config和以前不一样了!在Spring Security3之前的版本中,auto-config属性提供了比现在更多的启动项。在Spring Security2中通过auto-config配置的功能,现在可以使用security命名空间样式的配置很容易的实现。请参考第13章:迁移至Spring Security 3来获取更多从Spring Security2迁移到3的详细信息。】
除了以上认证相关的功能,其它过滤器链的配置是通过使用<http>元素来实现的。
在我们的安全系统中,当一个用户在我们的登录form中提供凭证后,这些凭证信息必须与凭证存储中的数据进行校验以确定下一步的行为。凭证的校验涉及到一系列的逻辑组件,它们封装了认证过程。
我们将会深入讲解我们例子中的用户名和密码登录form,与之对应的接口和实现都是特定于用户名和密码认证的。
但是,请记住,整体的认证是相同的,不管你是使用基于form的登录请求,或者使用一个外部的认证提供者如集中认证服务(CAS),抑或用户的凭证信息存在一个数据库或在一个LDAP目录中。在本书的第二部分,我们将会看到在基于form登录中学到的概念是如何应用到更高级的认证机制中。
涉及到认证功能的重要接口在下边的图标中有一个概览性的描述:
站在一个较高层次上看,你可以看到有三个主要的组件负责这项重要的事情:
接口名 | 描述/角色 |
AbstractAuthenticationProcessingFilter | 它在基于web的认证请求中使用。处理包含认证信息的请求,如认证信息可能是form POST提交的、SSO信息或者其他用户提供的。创建一个部分完整的Authentication对象以在链中传递凭证信息。 |
AuthenticationManager | 它用来校验用户的凭证信息,或者会抛出一个特定的异常(校验失败的情况)或者完整填充Authentication对象,将会包含了权限信息。 |
AuthenticationProvider | 它为AuthenticationManager提供凭证校验。一些AuthenticationProvider的实现基于凭证信息的存储,如数据库,来判定凭证信息是否可以被认可。 |
有两个重要接口的实现是在认证链中被这些参与的类初始化的,它们用来封装一个认证过(或还没有认证过的)的用户的详细信息和权限。
o.s.s.core.Authentication是你以后要经常接触到的接口,因为它存储了用户的详细信息,包括唯一标识(如用户名)、凭证信息(如密码)以及本用户被授予的一个或多个权限(o.s.s.core.
GrantedAuthority)。开发人员通常会使用Authentication对象来获取用户的详细信息,或者使用自定义的认证实现以便在Authentication对象中增加应用依赖的额外信息。
以下列出了Authentication接口可以实现的方法:
方法签名 | 描述 |
Object getPrincipal() | 返回安全实体的唯一标识(如,一个用户名) |
Object getCredentials() | 返回安全实体的凭证信息 |
List<GrantedAuthority> getAuthorities() |
得到安全实体的权限集合,根据认证信息的存储决定的。 |
Object getDetails() | 返回一个跟认证提供者相关的安全实体细节信息 |
你可能会担心的发现,Authentication接口有好几个方法的返回值是简单的java.lang.Object。这可能会导致在编译阶段很难知道调用Authentication对象的方法返回值是什么类型的对象。
需要注意的一点是AuthenticationProvider并不是直接被AuthenticationManager接口使用或引用的。但是Spring Security只提供了AuthenticationManager的一个具体实现类,即o.s.s.authentication.ProviderManager,它会使用一个或更多以上描述的AuthenticationProvider实现类。因为AuthenticationProvider的使用非常普遍并且被很好的集成在ProviderManager中,所以理解它在最常见的基本配置下是如何工作的就非常重要了。
让我们更仔细的看看在基于web用户名和密码认证的请求下,这些类的处理过程:
让我们看一下在较高层次示意图中反映出的抽象工作流程,并将其细化到这个基于表单认证的具体实现。你可以看到UsernamePasswordAuthenticationFilter负责(通过代理从它的抽象父类中)创建UsernamePasswordAuthenticationToken对象(Authentication接口的一个实现),并部分填充这个对象依赖的信息,这些信息来自HttpServletRequet。但是它是从哪里获取用户名和密码的呢?
你可能已经发现,当你试图访问我们JBCP Pets商店的主页时,你被重定向到http://localhost:8080/JBCPPets/spring_security_login:
URL的spring_security_login部分表明这是一个默认的登录的页面并且是在DefaultLoginPageGeneratingFilter中命名的。我们可以使用配置属性来修改这个页面的名字从而使得它对于我们应用来说是唯一的。
【建议修改登录页URL的默认值。修改后不仅能够对应用或搜索引擎更友好,而且能够隐藏你使用Spring Security作为安全实现的事实。通过这种方式来掩盖Spring Security能够使得万一Spring Security被发现存在安全漏洞时,恶意黑客寻找你应用漏洞的难度。尽管通过这种方式的安全掩盖不会降低你应用的脆弱性,但是它确实能够使得一些传统的黑客工具很难确定你的应用能够承担的住什么类型的攻击。需要注意的是,这里并不是“spring”名称在URL中出现的唯一地方。我们将在后面的章节详细阐述。】
让我们看一下这个form的HTML源码(忽略布局信息),来看一下UsernamePasswordAuthenticationFilter期望得到的信息:
<form name='f' action='/JBCPPets/j_spring_security_check' method='POST'> User:<input type='text' name='j_username' value=''> Password:<input type='password' name='j_password'/> <input name="submit" type="submit"/> <input name="reset" type="reset"/> </form>
你可以看到用户名和密码对应的form文本域有独特的名字((j_username和j_password),并且form的action地址j_spring_security_check也并不是我们配置的。它们是怎么来的呢?
文本域的名字是UsernamePasswordAuthenticationFilter规定的,并借鉴了Java EE Servlet 2.x的规范(在SRV.12.5.3章节中),规范要求登录的form使用特定的名字并且form的action要为特定的j_security_check值。这样的实际模式目标是允许基于Java EE servlet-based的应用能够与servlet容器的安全设施以标准的方式连接起来。
因为我们的应用没有使用到servlet容器的安全组件,所以可以明确置UsernamePasswordAuthenticationFilter以使得文本域有不同的名字。这种特定的配置变化可能会比你想象的复杂。现在,我们将要回顾一下UsernamePasswordAuthenticationFilter的生命周期,看一下它是如何进入我们配置的(尽管我们将会在第六章再次讲述这个配置)。
UsernamePasswordAuthenticationFilter是通过<http>配置指令的<form-login>子元素来进行配置的。正如在本章前面讲述的,我们设置的auto-config元素将会在你没有明确添加的情况下包含了<form-login>功能。正如你可能猜测的那样,j_spring_security_check并不对应任何应用中的物理资源。它只是UsernamePasswordAuthenticationFilter监视的一个基于form登录的URL。实际上,在Spring Security中有好几个这样的特殊的URL来实现特定的全局功能。你能在附录:参考资料中找到这些URL的一个列表。
在我们的简单的三步配置文件中,我们使用了一个基于内存的凭证存储实现快速的部署和运行:
<authentication-manager alias="authenticationManager"> <authentication-provider> <user-service> <user authorities="ROLE_USER" name="guest" password="guest"/> </user-service> </authentication-provider> </authentication-manager>
我们没有将AuthenticationProvider与任何具体的实现相关联,在这里我们再次看到了security命名空间默认为我们做了许多机械的配置工作。但是需要记住的是AuthenticationManager支持配置一个或多个AuthenticationProvider。
<authentication-provider>声明默认谁实例化一个内置的实现,即o.s.s.authentication.dao.DaoAuthenticationProvider。<authentication-provider>声明会自动的将这个AuthenticationProvider对象织入到配置的AuthenticationManager中,当然在我们这个场景中AuthenticationManager是自动配置的。
DaoAuthenticationProvider是AuthenticationProvider的简单封装实现并委托o.s.s.core.userdetails.UserDetailsService接口的实现类进行处理。UserDetailsService负责返回o.s.s.core.userdetails.UserDetails的一个实现类。
如果你查看UserDetails的Javadoc,你会发现它与我们前面讨论的Authentication接口非常类似。尽管它们在方法名和功能上有些重叠的部分,但是请不要混淆,它们有着截然不同的目的:
接口 | 目的 |
Authentication | 它存储安全实体的标识、密码以及认证请求的上下文信息。它还包含用户认证后的信息(可能会包含一个UserDetails的实例)。通常不会被扩展,除非是为了支持某种特定类型的认证。 |
UserDetails | 为了存储一个安全实体的概况信息,包含名字、e-mail、电话号码等。通常会被扩展以支持业务需求。 |
我们对<user-service>子元素的声明将会触发对o.s.s.core.userdetails.memory.InMemoryDaoImpl的配置,它是UserDetailsService的一个实现。正如你可能期望的那样,这个实现将在安全XML文件中配置的用户信息放在一个内存的数据存储中。这个service的实现支持其它属性的设置,从而实现账户的禁用和锁定。
让我们更直观的看一下DaoAuthenticationProvider是如何交互的,从而AuthenticationManager提供认证支持:
正如你可能想象的那样,认证是相当可配置化的。大多数的Spring Security例子要么使用基于内存的用户凭证存储要么使用JDBC(在数据库中)的用户凭证存储。我们已经意识到修改JBCP Pets应用以实现数据库存储用户凭证是一个好主意,我们将会在第四章来处理这个配置变化。
Spring Security很好的使用应用级异常(expected exceptions)来表示处理各种的结果情况。你可能在使用Spring Security的日常工作中不会与这些异常打交道,但是了解它们以及它们为何被抛出将会在调试问题或理解应用流程中非常有用。
所有认证相关的异常都继承自o.s.s.core.AuthenticationException基类。除了支持标准的异常功能,AuthenticationException包含两个域,可能在提供调试失败信息以及报告信息给用户方面很有用处。
authentication:存储关联认证请求的Authentication实例;
extraInformation:根据特定的异常可以存储额外的信息。如UsernameNotFoundException在这个域上存储了用户名。
我们在下面的表格中,列出了常见的异常。完整的认证异常列表可以在附录:参考资料中找到:
异常类 | 何时抛出 | extraInformation内容 |
BadCredentialsException | 如何没有提供用户名或者密码与认证存储中用户名对应的密码不匹配 | UserDetails |
LockedException | 如果用户的账号被发现锁定了 | UserDetails |
UsernameNotFoundException | 如果用户名不存在或者用户没有被授予的GrantedAuthority | String(包含用户名) |
这些以及其他的异常将会传递到过滤器链上,通常将会被request请求的过滤器捕获并处理,要么将用户重定向到一个合适的界面(登录或访问拒绝),要么返回一个特殊的HTTP状态码,如HTTP 403(访问被拒绝)。