安全的复杂之处:安全 web 请求的架构
借助于 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 过滤器是如何封装一个用户请求的:
Spring Security 在 XML 配置文件中的自动配置属性,建立了十个 servlet 过滤器,它们通过使用 Java EE 的 servlet 过滤器链按顺序组合起来。 Filter chain 是 Java EE Servlet API 的一个概念,通过接口 javax.servlet.FilterChain 进行定义,它允许在 web 应用中的一系列的 servlet 过滤器能够应用于任何给定的请求。
与生活中金属制定的链类似,每一个 servelt 过滤器代表链上的一环,它会进行方法的调用以处理用户的请求。请求穿过整个过滤器链,按顺序调用每个过滤器。
正如你能从链这个词汇中推断出的那样, 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 的 HTML |
o.s.s.web.authentication.www.BasicAuthenticationFilter | 监控 HTTP 基础认证的头信息并进行处理 |
o.s.s.web.savedrequest. RequestCacheAwareFilter | 用于用户登录成功后,重新恢复因为登录被打断的请求。 |
o.s.s.web.servletapi. SecurityContextHolderAwareRequest Filter | 用一个扩展了 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 中找到。
在 auto-config 场景下,发生了什么事情?
在 Spring Security 3 中,使用 auto-config 会自动提供以下三个认证相关的功能:
<!--[if !supportLists]-->l <!--[endif]-->HTTP 基本认证
<!--[if !supportLists]-->l <!--[endif]-->Form 登录认证
<!--[if !supportLists]-->l <!--[endif]-->退出
值得注意的是,也可以使用配置元素实现这三个功能,能够实现比使用 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 用户名和密码认证的请求下,这些类的处理过程:
![](http://dl.iteye.com/upload/attachment/503771/2240db15-ab2e-3dfe-9205-6243cab08832.png)
让我们看一下在较高层次示意图中反映出的抽象工作流程,并将其细化到这个基于表单认证的具体实现。你可以看到 UsernamePasswordAuthenticationFilter 负责(通过代理从它的抽象父类中)创建 UsernamePasswordAuthenticationToken 对象( Authentication 接口的一个实现),并部分填充这个对象依赖的信息,这些信息来自 HttpServletRequet 。但是它是从哪里获取用户名和密码的呢?
spring_security_login 是什么?我们怎么到达这个界面的?
你可能已经发现,当你试图访问我们 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 包含两个域,可能在提供调试失败信息以及报告信息给用户方面很有用处。
<!--[if !supportLists]-->l <!--[endif]-->authentication :存储关联认证请求的 Authentication 实例;
<!--[if !supportLists]-->l <!--[endif]-->extraInformation :根据特定的异常可以存储额外的信息。如 UsernameNotFoundException 在这个域上存储了用户名。
我们在下面的表格中,列出了常见的异常。完整的认证异常列表可以在附录:参考资料 中找到:
异常类 | 何时抛出 | extraInformation 内容 |
BadCredentialsException | 如何没有提供用户名或者密码与认证存储中用户名对应的密码不匹配 | UserDetails |
LockedException | 如果用户的账号被发现锁定了 | UserDetails |
UsernameNotFoundException | 如果用户名不存在或者用户没有被授予的 GrantedAuthority | String (包含用户名) |
这些以及其他的异常将会传递到过滤器链上,通常将会被 request 请求的过滤器捕获并处理,要么将用户重定向到一个合适的界面(登录或访问拒绝),要么返回一个特殊的 HTTP 状态码,如 HTTP 403 (访问被拒绝)。