Spring Security之Web应用安全

Web应用安全

大多数Spring Security用户会在使用HTTP和Servlet API的应用程序中使用该框架。这一章将介绍Spring Security是如何为web应用提供认证和访问控制的。

Security Filter Chain

SS(Spring Security,以下简称SS)的web架构完全基于标准的servlet过滤器。内部没有使用任何基于servlet的框架或servlet,所以没有与任何web特定的技术强关联。它处理httpServletRequestHttpServletResponse,但不关心请求从哪里来。

SS内部维护了一个过滤器链,每个过滤器有特殊的功能,并且根据所需的服务,在配置中添加或删除过滤器。过滤器之间有依赖关系所以它们的顺序非常重要。如果使用命名空间配置,就不再需要额外的配置了,SS会自动配置这些过滤器。但若想要完全控制过滤器链或者希望使用命名空间不支持的特性,又或者使用的是自定义的class的版本,就需要定义一些Bean了。

DelegatingFilterProxy

如果使用servlet的过滤器,必须在web.xml中显式声明过滤器。在SS中,filter类也是在应用上下文中定义的Spring Bean,因此能够使用Spring丰富的依赖注入工具和生命周期接口。Spring的DelegationFilterProxt提供了web.xml和应用长下文之间的连接。

如果使用DelegatingFilterProxyweb.xml中会有以下内容:

<filter>
<filter-name>myFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

注意过滤器的类是DelegatingFilterProxy,而不是实际上实现过滤器逻辑的类。DelegatingFilterProxy可以将Filter的方法代理到Spring应用上下文中的某个bean,而且bean还能使用Spring web应用上下文的生命周期支持和灵活的配置。这个bean必须事项javax.servlet.Filter而且名字必须和filter-name元素中的名字一样。查阅DelegatingFilterProxy的JavaDoc获取更多信息。

FilterChainProxy

SS的web架构只能通过代理给FilterChainProxy的实例来使用,不应该使用security filter本身。理论上可以在应用的上下文配置文件中声明所有SS的filter bean,并在web.xml中为每个filter添加正确的DelegatingFilterProxy项,但是这样会很麻烦,而且如果filter很多的话,web.xml会过于杂乱。使用FilterChainProxy只需要在web.xml中添加一项就可以了。它也像上边的示例一样使用DelegatingFilterProxy,但是filter-name设置为bean名“filterChainProxy”。应用上下文中也要声明相同bean名的过滤器链。示例如下:

<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg>
    <list>
    <sec:filter-chain pattern="/restful/```" filters="
        securityContextPersistenceFilterWithASCFalse,
        basicAuthenticationFilter,
        exceptionTranslationFilter,
        filterSecurityInterceptor" />
    <sec:filter-chain pattern="/```" filters="
        securityContextPersistenceFilterWithASCTrue,
        formLoginFilter,
        exceptionTranslationFilter,
        filterSecurityInterceptor" />
    </list>
</constructor-arg>
</bean>

filter-chain元素用于快捷配置一你用中需要的security过滤器链。它将url模式映射到一组filter上,filter的名字是filters元素中声明的bean名,并将filter合并到SecurityFilterChain类型的bean中。pattern属性采用Ant路径,最具体的URI应先声明。运行时,FilterChainProxy会找到第一个匹配当前请求的URI模式,并且按照filters属性指定的顺序调用每个filter。

在上面的示例配置的过滤器链中声明了两个SecurityContextPersistenceFilterASCallowSessionCreation的缩写,它是SecurityContextPersistenceFilter中的一个属性)。由于Web service请求中不会有jsessionid,没有必要这样的用户代理创建HttpSession。如果大型应用需要最大的可扩展性,建议使用上面显示的方法。对于小型应用,单个SecurityContextPersistenceFilterallowSessionCreation属性默认为true)就足够了。

注意FilterChainProxy不会再配置的过滤器上调用标准过滤器的生命周期方法。建议使用Spring应用的生命周期接口。

跳过过滤器链

使用filter="none"属性替代上述的filter bean列表。任何匹配此URI模式的请求都不需要认证和授权。securityContext中也没有任何内容,因为没有调用任何安全的过滤器链。

过滤器顺序

过滤器链中定义过滤器的顺序是非常重要的,不管使用那种过滤器,顺序应该如下:
- ChannelProcessingFilter,因为它可能重定向到不同的协议
- SecurityContextPersistenceFilterSecurityContext可以在web请求的开始就设置到SecurityContextHolder中,当web请求结束的时候任何SecurityContext的改动都会复制到HttpSession中(准备被下一个web请求使用)。
- ConcurrentSessionFilte,它使用了SecurityContextHolder的功能且需要更新SessionRegistry以反映principal的持续请求
- 认证处理机制-UserPasswordAuthenticationFilterCasAuthenticationFilterBasicAuthenticationFilter等等-SecurityContextHolder可以被修改为包含有效的Authentication请求token
- SecurityContextHolderAwareRequestFilter,如果使用它将SS感知的HttpServletRequestWrapper安装到servlet容器中
- JaasApiIntegegrationFilter,如果SecurityContextHolder中存储的是JaasAuthenticationToken,则会将FilterChain作为JaasAuthenticationToken中的Subject处理
- RememberMeAuthenticationFilter,如果之前的认证处理机制没有更新SecurityContextHolder,但请求的cookie启用remember-me服务,则SecurityContextHolder中会存储合适的rememberedAuthentication对象
- AnonymousAuthenticationFilter,如果之前的认证处理机制没有更新SecurityContextHolder,则会讲一个匿名的Authentication对象存储进去
- ExceptionTranslationFilter,捕捉所有的SS异常,然后返回HTTP错误或者启用适当的AuthenticationEntryPoint
- FilterSecurityInterceptor,保护web URI,如果访问被拒绝,则抛出异常

请求匹配和HttpFirewall

SS有几个地方会根据使用者定义的模式对请求进行匹配,并决定如何处理这个请求:当FilterChainProxy决定请求应该传递给哪个过滤器链时;FilterSecurityInterceptor决定请求适用哪些安全性约束时。了解模式匹配的机制以及使用什么URL值非常重要。

servlet规范为HttpServletRequest定义了若干属性,这些属性可以通过getter访问,可以对这些属性进行匹配。这些属性是:contextPathservletPathpathInfoQueryString。SS只对应用的受保护的路径感兴趣,所以contextPath排除掉。不幸的是,对于特定的请求URI,servlet规范没有准确定义servletPathpathInfo的值所包含的内容。例如,RFC 2369中定义的URL的每段路径都可能包含参数。规范没有明确说明这些是否应该包含在servletPathpathInfo值中,并且不同servlet容器之间的行为也不同。某些容器不会从这些值中删除路径参数,所以将应用部署在这些容器上会有危险,攻击者可以将它们添加到请求的URL中,使模式匹配成功或意外失败。也有可能传入URL的其他变体。例如,可能包含路径遍历的字符串(类似/../)或者多个斜线(//)也会使模式匹配失败。一些容器在执行servlet影射之前将这些规范化,但其他容器则没有。为了杜绝此类问题,FilterChainProxy使用了一个HttpFirewall·策略来检查并包装请求。默认情况下会自动拒绝不规范的请求,路径参数和重复的斜线会被删除掉以进行模式匹配。因此必须使用FilterChainProxy来管理过滤器链。注意servletPathpathInfo值会被容器解码,应用不要出现包含分号的路径,这些部分会被删除以进行模式匹配。

如上所述,默认策略是使用Ant模式的路径进行匹配,这可能是大多数用户的最佳选择。该策略在AntPathRequestMatcher类中实现,该类使用Spring的AntPathMatcher对连接的servletPathpathInfo进行不区分大小写的模式匹配,并忽略queryString掉。

如果处于某种原因,需要更强到的匹配策略,可以使用正则表达式。这个策略的实现是RegexRequestMatcher。查看这个类的JavaDoc获取更多信息。

实际开发中,建议在service层使用方法安全来控制对应用的访问,不要完全依赖于在web应用层级中定义的安全约束。URL很容易发生变化,而且很难考虑到应用支持所有可能的URL以及请求应该如何处理。应该尝试限制使用简单易懂的ant路径。始终尝试通过在最后面定义一个全能通配符(/或其他)并拒绝访问以使用“默认拒绝”策略。

在service层定义的安全更具有健壮性而且很难跳过,因此应该使用使用SS的方法安全选项。

HttpFirewall还通过拒绝HTTP响应头新行中的字符来防止HTTP响应拆分。

默认情况下会使用StrictHttpFirewall。这个实现会拒绝看似恶意的请求。如果觉得这个实现过于严格,可以自定义拒绝什么样的请求,但需要知道这可能使应用更容易被攻击。例如,如果希望使用Spring MVC的Matrix Variables,xml中可以使用一下配置:

<b:bean> id="httpFirewall" 
        class="org.springframework.security.web.firewall.StrictHttpFirewall" 
        p:allowSemicolon="true"/>
<http-firewall ref="httpFirewall" />

也可以使用JavaConfig暴露一个StrictHttpFirewallbean来实现上面的效果:

@Bean
public StrictHttpFirewall httpFirewall(){
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}

和其他基于过滤器的框架一起使用

如果正在使用的其他框架也是基于过滤器的,必须要把SS的filter放在最前面。SecurityContextHolder会在其他过滤器之前被填充,后续的filter可以直接使用。示例是使用SiteMesh来装饰网页或像Wicket这样的Web框架,它使用过滤器来处理请求。

高级命名空间配置

正如之前在命名空间章节中看到的那样,可以使用多个http元素为不同的URL模式定义不同的安全配置。每个元素会创建一个过滤器链,过滤器链内部是FilterChainProxy,URL模式应该被映射到FilterChainProxy上。元素会以声明的顺序被添加,最具体的模式应该被先声明。这是另一个例子,与上面的情况相似,应用同时支持无状态RESTful API以及用户使用表单登录的普通Web应用。

<!-- Stateless RESTful service using Basic authentication -->
<http pattern="/restful/**" create-session="stateless">
<intercept-url pattern='/**' access="hasRole('REMOTE')" />
<http-basic />
</http>

<!-- Empty filter chain for the login page -->
<http pattern="/login.htm*" security="none"/>

<!-- Additional filter chain for normal users, matching all other requests -->
<http>
<intercept-url pattern='/**' access="hasRole('USER')" />
<form-login login-page='/login.htm' default-target-url="/home.htm"/>
<logout />
</http>

安全核心过滤器

使用SS的时候通常会在web应用中使用某些重要的的过滤器,因此首先要了解一下它们以及支撑它们的类和接口。这里不会涵盖所有的功能,可以查阅JavaDoc获取详细信息。

FilterSecurityInterceptor

在讨论访问控制时,已经简单了解了FilterSecurityInterceptor,并将它与命名空间一起使用,其中<intercept-url>元素被组合在一起以在内部配置它。现在将看到如何显示配置它以与FilterChainProxy一起使用,以及它的过滤器`ExceptionTranslationFilter。典型的配置如下:

<bean id="filterSecurityInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="securityMetadataSource">
    <security:filter-security-metadata-source>
    <security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>
    <security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
    </security:filter-security-metadata-source>
</property>
</bean>

FilterSecurityInterceptor负责处理HTTP资源的安全性,它需要引用AuthenticationManagerAccessDecisionManager,它还提供了用于不同HTTP URL请求的配置属性。
FilterSecurityInterceptor可以通过两种方式来配置它的配置属性:第一种,如上,使用<filter-security-metadata-source>命名空间元素,这有点像命名空间章节中的<http>元素,但<intercept-url>子元素仅使用patternaccess属,逗号用于分隔适用于每个HTTP URL的不同配置属性;第二种,编写自己的SecurityMetadataSource,这种方式超出了本文的范围。无论是用什么方法,SecurityMetadataSource都负责返回包含与单个安全HTTP URL关联的所有配置属性的List<ConfigAttribute>

应该注意的是,FilterSecurityInterceptor.setSecurityMetadataSource()方法实际上需要FilterInvocationSecurityMetadataSource类型的实例作为参数。 这个类型是SecurityMetadataSource的子类,也是一个标记接口。它只是表示SecurityMetadataSource了解FilterInvocation。为了简单起见,继续将FilterInvocationSecurityMetadataSource称为SecurityMetadataSource,因为它们之间的区别与绝大多数用户没有关联。
由命名空间创建的SecurityMetadataSource通过将请求URL与配置的pattern属性进行匹配来获取特定FilterInvocation的配置属性。这与命名空间配置的方式相同。默认情况是将所有表达式视为Apache Ant路径,对于更复杂的情况也支持正则表达式。request-matcher属性用于指定正在使用的pattern的类型。无法在同一定义中混合表达式语法。 例如,使用正则表达式而不是Ant路径的配置将编写如下:

<bean id="filterInvocationInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="runAsManager" ref="runAsManager"/>
<property name="securityMetadataSource">
    <security:filter-security-metadata-source request-matcher="regex">
    <security:intercept-url pattern="\A/secure/super/.*\Z" access="ROLE_WE_DONT_HAVE"/>
    <security:intercept-url pattern="\A/secure/.*\" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
    </security:filter-security-metadata-source>
</property>
</bean>

匹配模式始终以定义的顺序被计算,因此,匹配模式越具体,越要早定义。这在上面的示例中也有体现,更具体的/secure/super/模式定义在不具体的/secure/模式之前。如果它们的位置互换,/secure/模式会始终匹配成功而/secure/super/模式永远也不会匹配到。

ExceptionTranslationFilter

在安全过滤器栈中,ExceptionTranslationFilter设置在FilterSecurityInterceptor之前。但它实际上不执行任何安全处理,它处理的是安全拦截器抛出的异常并提供适当的HTTP响应。

<bean id="exceptionTranslationFilter"
class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
<property name="accessDeniedHandler" ref="accessDeniedHandler"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>

<bean id="accessDeniedHandler"
    class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.htm"/>
</bean>

AuthenticationEntryPoint

如果用户未经认证请求受保护的HTTP资源,AuthenticationEntryPoint会被调用。安全拦截器在调用栈上抛出合适的AuthenticationException或者AccessDeniedException,触发AuthenticationEntryPointcommence方法。向用户提供正确的响应以开始进行认证。SS在这里使用的是LoginUrlAuthenticationEntryPoint,它重定向请求到其他URL(通常是登录页)。开发中实际上使用哪种实现取决于应用中使用的认真机制。

AccessDeniedHandler

如果用户已经认证并访问受保护的资源会发生什么呢?通常不会发生这种情况,因为应用工作流程应该严格限制用户有权限的操作。例如,管理员页面的链接应该对没有管理员角色的用户隐藏。但是不能依赖于隐藏链接来确保安全,用户很可能尝试直接输入URL来跳过这个限制,或者修改一个RESTful URL的某些参数。应用必须在这些场景下也受到保护,否则应用就是不安全的。通常可以使用简单的web层安全来限制基本的URL,或在服务层接口使用更具体的基于方法的安全来真正确定什么是可以访问的。

如果用户已经认证但抛出AccessDeniedException异常,这意味着某些请求正在试图进行无权限的操作。这种情况下,ExceptionTranslationFilter会执行第二种策略,也就是AccessDeniedHandler。默认情况下会使用AccessDeniedHandlerImpl,只发送403(禁止访问)给客户端。或者可以自己显式配置一个实例(实例如上)并设置一个错误页面的URL,请求会转发到这个URL上。页面可以是一个简单的“拒绝访问”页面,例如JSP,也可以是一个复杂的MCV 控制器。当然,也可以自己实现此接口并使用。

使用命名空间配置应用时,也可以提供自定义AccessDeniedHandler。有关详细信息,请参阅命名空间附录

SavedRequest和RequestCache接口

ExceptionTranslationFilter的另外一个作用是在执行AuthenticationEntryPoint之前保存当前请求。便于用户认证后恢复请求。例如,用户使用form表单登录,然后通过默认的SavedRequestAwareAuthenticationSuccessHandler重定向到原始的请求。

RequestCache封装了保存和恢复HttpServletRequest实例的功能。默认使用将请求存储在HttpSession中的HttpSessionRequestCache。当用户重定向到原来的URL的时候,RequestCacheFilter会从缓存中恢复之前存储的请求。

正常情况下,不需要修改这里的任何功能,但是包村请求的处理是一个”best-effort”的方法,可能会存在默认的配置无法处理的情况。SS 3.0及以上使用这些完全可拔插的接口。

SecurityContextPersistenceFilter

技术概述(译者注:详见架构与实现-技术概述-web应用认证)章节中已经介绍过了这个非常重要的过滤器的用途,如果有需要,可以回头再看看相关内容。首先看看如何配置并和FilterChainProxy一起使用它。最基本的配置只需要这个bean:

<bean id="securityContextPersistenceFilter"
class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>

如前所述,这个过滤器有两个任务。它负责在HTTP请求之间存储SecurityContext内容,并在请求完成时清除SecurityContextHolder。 清除ThreadLocal中存储的上下文是必不可少的,否则可能会将线程替换到servlet容器的线程池中,而此时当前用户的安全上下文仍然存储在这个线程中。这个线程之后可能会被使用到,然后使用错误的凭证执行操作。

SecurityContextRepository

从SS 3.0开始,加载和存储安全上下文的工作都代理给了一个单独的策略接口:

public interface SecurityContextRepository {

SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);

void saveContext(SecurityContext context, HttpServletRequest request,
        HttpServletResponse response);
}

HttpRequestResponseHolde是传入请求和响应对象的容器,允许实现用包装类替换它们。返回的内容将传递给过滤器链。

默认实现是HttpSessionSecurityContextRepository,它将安全上下文存储为HttpSession属性。 此实现的最重要的配置参数是allowSessionCreation属性,默认为true,因此如果需要一个会话来为认证过的用户存储安全上下文,则允许该类创建会话(除非进行了身份验证并且安全上下文的内容已更改,否则不会创建会话)。 如果不想创建会话,则可以将此属性设置为false:

<bean id="securityContextPersistenceFilter"
    class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
<property name='securityContextRepository'>
    <bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
    <property name='allowSessionCreation' value='false' />
    </bean>
</property>
</bean>

或者定义一个NullSecurityContextRepository的实例,这是一个空对象实现,即使在请求期间已经创建了会话,也会不会进行安全上下文的存储。

UsernamePasswordAuthenticationFilter

现在已经看到了SS Web配置中始终存在的三个主要的过滤器。它们是由命名空间<http>元素自动创建,不能用替代品替换。 现在唯一缺少的是允许用户进行认证的认证机制。UsernamePasswordAuthenticationFilter过滤器是最常用的认证过滤器,也是最常被自定义的过滤器。 它还提供了命名空间中<form-login>元素使用的实现。配置它需要三个步骤:
1. 配置一个带有登录页URL的LoginUrlAuthenticationEntryPoint,并将它设置给ExceptionTranslationFilter
2. 使用JSP或者Spring MVC的controller实现上述的登陆页
3. 在应用上下文中配置一个UsernamePasswordAuthenticationFilter实例
4. 将过滤器bean添加到过滤器链代理(确保添加的顺序)

登录页仅包含usernamepassword输入框,并将用户的输入POST给过滤器监听的URL(默认是/login)。最简单的过滤器配置如下:

<bean id="authenticationFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

认证成功或失败的应用流程

过滤器调用配置的AuthenticationManager来处理认证请求。认证成功或失败后的动作分别由AuthenticationSuccessHandlerAuthenticationFailureHandler策略接口控制。如果要完全自定义过滤器的行为的话,也可以自己设置这两个属性。SS提供的标准实现有SimpleUrlAuthenticationSuccessHandler,SavedRequestAwareAuthenticationSuccessHandler,SimpleUrlAuthenticationFailureHandler,ExceptionMappingAuthenticationFailureHandlerDelegatingAuthenticationFailureHandler。可以看一下这些类以及AbstractAuthenticationProcessingFilter的JavaDoc以了解其工作原理及其支持的特性。

如果认证成功,返回的Authentication会放置到SecurityContextHolder中。然后配置的AuthencicationSuccessHandler会被调用,并重定向或者转发用户的请求到合适的地方去。默认会使用SavedRequestAwareAuthenticationSuccessHandler,用户会被重定向到登录之前的页面。

ExceptionTranlationFilter缓存用户原本的请求。当用户认证完成后,请求处理器会使用它缓存的请求来获取用户原本的请求并重定向到请求的地址。原本的请求会被重新构建并作为替代方案。
如果认证失败,会执行配置的AuthenticationFailureHandler

Servlet API集成

这一节讨论SS是如何与Servlet API集成的。servletapi-xml实例应用展示了每种方法的用法。

Servlet 2.5+ 集成

HttpServletRequest.getRemoteUser()

HttpServletRequest.getRemoteUser()方法会返回SecurityContextHolder.getContext().getAuthentication().getName()的返回值,也就是当前的用户名。如果要在应用中展示当前用户名的话可以使用它。另外,检查返回值是否为空可以判断用户是否被认证或者是不是匿名的。根据当前用户是否已经认证来确定是否应该显示某些UI元素(即,仅当用户被认证时才应该显示注销链接)。

HttpServletRequest.getUsePrincipal()

HttpServletRequest.getUsePrincipal()方法返回SecurityContextHolder.getContext().getAuthentication()的返回值。也就是Authentication,在使用基于用户名密码认证的时候,它就是一个UsernamePasswordAuthenticationTolen实例。如果需要用户的更多信息的时候,可以使用它。例如,如果已经创建了自定义的UserDetailsService并返回自定义的UserDetails,而UserDetails包含用户的姓和名。你可以通过一下代码获取这些信息:

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by defauft, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomerUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();

注意,在整个应用中执行过多的逻辑通常是不好的做法。相反,应该将这些逻辑集中起来减少SS和Servlet API之间的耦合

HttpServletRequest.isUserInRole(String)

HttpServletRequest.isUserInRole(String)会判断SecurityContextHolder.getContext().getAuthentication().getAuthorities()是否包含传入isUserInRole(String)角色的GrantedAuthority。通常不要传入带有”ROLE_”前缀的参数到这个方法中,因为前缀会被自动添加。例如,如果要判断当前用户是否有”ROLE_ADMIN”的权限,可以使用如下代码:

boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");

可以据此确定是否应该显示某些UI组件。例如,只有当前用户是管理员的时候,才可以显示管理员连接。

Servlet 3+ 集成

本节描述了SS集成的Servlet 3+的方法

HttpServletRequest.authenticate(HttpServletResponse)

HttpServletRequest.authenticate(HttpServletResponse)方法可以用来确保某用户是认证过得。如果没有认证,AuthenticationEntryPoint会要求用户进行认证(即,重定向到登录页)。

HttpServletRequest.login(String,String)

HttpServletRequest.login(String,String)方法使用当前AuthenticationManager认证用户。例如,以下代码认证用户名为”user”密码为”password”的用户:

try {
httpServletRequest.login("user","password");
} catch(ServletException e) {
// fail to authenticate
}

如果希望SS处理认证失败的话,不用捕捉ServletException。

HttpServletRequest.logout()

HttpServletRequest.logout()方法用来登出当前用户。

通常这意味着SecurityContextHolder会被清空,HttpSession会失效,任何”Remember Me”认证都会被清除等等。但是,配置的LogoutHandler实现将根据SS配置而有所不同。需要注意的是,在调用HttpServletRequest.logout()之后,使用者仍然负责编写响应。 通常,这将涉及重定向到登录页面。

AsyncContext.start(Runnable)

AsyncContext.start(Runnable)方法确保凭证会传播到的新的线程。使用SS的并发支持,SS覆盖了AsyncContext.start(Runnable)方法来确保处理Runnable的时候使用当前SecurityContext。例如,以下代码会输出当前用户的Authentication:

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {
    public void run() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        try {
            final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();
            asyncResponse.setStatus(HttpServletResponse.SC_OK);
            asyncResponse.getWriter().write(String.valueOf(authentication));
            async.complete();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
});

支持异步Servlet

如果使用基于Java的配置,那么可以直接开始使用。如果使用的是XML配置,有一些必要的更新需要注意。第一步是确保web.xml已经至少更新为3.0 schema,如下:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

</web-app>

接下来确保springSecurityFilterChain被设置为处理异步请求。

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
    org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

仅此而已。现在SS会确保SecurityContext会在异步请求上传播。

但它是如何工作的?如果对此不太感兴趣,可以跳过本节的剩余部分。大部分内容都是Servlet规范内置的,但还是有一些调整,SS会确保正确处理异步请求。SS 3.2之前,一旦HttpServletReponse被提交,SecurityContextHolder中的SecurityContext就会自动保存。在异步环境下这将导致某些问题。例如,考虑以下代码:

httpServletRequest.startAsync();
new Thread("AsyncThread") {
    @Override
    public void run() {
        try {
            // Do work
            TimeUnit.SECONDS.sleep(1);

            // Write to and commit the httpServletResponse
            httpServletResponse.getOutputStream().flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}.start();

问题是这个线程不了解SS,所以SecurityContext没有传播到这个线程中。这意味着提交HttpServletResponse的时候,没有SecurityContext。当SS在提交HttpServletResponse时自动保存SecurityContext后,会丢失掉登陆的用户信息。

从3.2版本开始,SS更加智能,只要调用了HttpServletRequest.startAsync(),就不会在提交HttpServletResponse时自动保存SecurityContext。

Servlet 3.1+集成

本节描述了SS集成的Servlet 3.1的方法。

HttpServletRequest.changeSessionId()

在Servlet 3.1以及更高的版本中,HttpServletRequest#changeSessionId()是预防会话固定攻击(Session Fixation)的默认方法。

Basic 和 Digest认证

Basic 和 Digest认证是web应用中常用的可选认证机制。 Basic认证通常用于每个请求都传递凭证的无状态客户端。将它和基于表单的认证结合使用是很常见的,其中应用通过基于浏览器的用户界面和作为web服务被使用。然而,Basic认证将密码作为纯文本传输,因此只能在加密的传输层(如HTTPS)上使用。

BasicAuthenticationFilter

BasicAuthenticationFilter负责处理HTTP头中的Basic认证凭证。这可以用于Spring远程协议(例如Hessian和Burlap)以及普通浏览器。管理HTTP Basic认证的标准由RFC 1945第11节定义,BasicAuthenticationFilter遵循此标准。Basic认证是一种很受人欢迎的认证方法,因为他在用户代理中部署得非常广泛,而且实现起来非常简单(它只是在HTTP头中指定的username:password的Base64编码)。

配置

要实现HTTP Basic认证,需要添加BasicAuthenticationFilte到过滤器链中。应用上下文应包含`BasicAuthenticationFilter及其所需的其他组件:

<bean id="basicAuthenticationFilter"
class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
<property name="realmName" value="Name Of Your Realm"/>
</bean>

配置的AuthenticationManager用于处理每个认证请求。如果认证失败,配置的authenticationEntryPoint会被用来重试认证过程。通常应该将这个过滤器与BasicAuthenticationEntryPoint一起使用,BasicAuthenticationEntryPoint返回401响应以及适当的头部来重新发起HTTP Basic认证。如果认真成功,返回的Authentication对象照常放入SecurityContextHolder中。

如果认证事件成功,或者由于HTTP头部不包含支持的认证请求而没有尝试认证,过滤器链将正常继续。过滤器链唯一的中断是在认证失败并调用AuthenticationEntryPoint时。

DigestAuthenticationFilter

DigestAuthenticationFilter能够处理HTTP头部中的Digest认证凭证。Digest认证希望解决Basic认证的许多缺点,特别是确保不会通过网络以明文方式传递凭证。许多用户代理支持Digest认证,包括Mozilla Firefox和Internet Explorer。 管理HTTP Digest认证的标准由RFC 2617定义,RFC 2617更新了RFC 2069规定的Digest认证标准的早期版本。大多数用户代理实现RFC 2617。SS的DigestAuthenticationFilter兼容RFC 2617规定的”auth”保护质量(`qop),还提供与RFC 2069的向后兼容性。如果需要使用未加密的HTTP(即没有TLS / HTTPS)并希望最大限度地提高认证过程的安全性,Digest认证更好。事实上,Digest认证是WebDAV协议的强制性要求,如RFC 2518第17.1节所述。

不应该在应用中使用Digest,因为它是不安全的。最明显的问题是必须以明文,加密或MD5格式存储密码。所有这些存储格式都是不安全的。 应该使用单向自适应密码哈希(即bCrypt,PBKDF2,SCrypt等)。


Digest认证的中心是”nonce”。这是服务器生成的值。 SS的nonce采用以下格式:

base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime:   The date and time when the nonce expires, expressed in milliseconds
key:              A private key to prevent modification of the nonce token

有关Digest认证的更多信息可以参考官网文档,这里不再详述。

Remember-Me认证

有关Remember-Me认证的更多信息可以参考官网文档,这里不再详述。

跨站请求伪造(CSRF)

本章讨论SS的CSRF保护支持

CSRF攻击

在讨论SS如何保护应用防止SCRF攻击之前,得先解释一下CSRF是啥。下面看一个具体的实例来了解什么是CSRF。

假设你银行的网站提供了从当前登录用户转账到其他银行用户的表单。例如,HTTP可能是这样的:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

假如你已经在银行网页上认证过,并且没有注销,然后点击了一个恶意的网站。恶意站点包含如下表单的HTML页面:

<form action="https://bank.example.com/transfer" method="post">
<input type="hidden"
    name="amount"
    value="100.00"/>
<input type="hidden"
    name="routingNumber"
    value="evilsRoutingNumber"/>
<input type="hidden"
    name="account"
    value="evilsAccountNumber"/>
<input type="submit"
    value="Win Money!"/>
</form>

你想要赢钱,所以点击了提交按钮。在这个过程中,你已经在无意之间转账100刀给攻击者了。为啥为这样呢,因为虽然恶意站点不能看到你的cookies,但与银行站点相关的cookies还是会随请求一起发送。

更糟糕的是,整个过程可以通过JS代码自动完成。也就是说,你不点击提交按钮也会中招。所以如何防止此类攻击呢?

Synchronizer Token Pattern(令牌同步模式)

问题是来自银行站点的请求和恶意站点的请求是完全相同的,也就是说没办法分辨它们然后拒绝恶意网站的请求并允许银行网站的请求。防止CSRF攻击需要某些恶意站点在请求中无法提供的东西。

其中一个解决方案就是令牌同步模式。这个方案确保每个请求需要携带除了Session的cookie之外,还需要一个随机生成的token作为HTTP参数。当请求被提交,服务器需要查找参数的预期值,并将它与参数中的实际值比较,如果值不匹配,应该拒绝这个请求。

还可以放松约束,只需要更新状态的请求携带token(通常由服务器返回)。这可以安全地完成,因为恶意站点无法读取响应。此外,不要在HTTP GET中包含随机令牌,因为这可能导致令牌泄露。

那么上面的示例应该如何修改以防止CSRF攻击呢。假设随机值在HTTP参数中的名字为“_csrf”。例如,转账的请求应该如下:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>

注意在参数中加入了“_csrf”参数和随机值。现在恶意站点不可能猜测到“_csrf”参数的正确值,当服务器接受到恶意站点的请求的时候,对比传入的参数值和期望值,发现不匹配,然后拒绝此请求。

什么时候使用CSRF保护

建议对普通用户可以由浏览器处理的任何请求使用CSRF保护。如果只创建非浏览器客户端使用的服务,则可能需要禁用CSRF保护。

SCRF防御和JSON

一个常见的问题是“是否需要保护javascript发出的JSON请求?”,回答是,视情况而定。然而,还是要非常小心,因为存在可能影响JSON请求的CSRF漏洞。例如,攻击者可以使用一下表单创建带JSON的CSRF:

<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
    value="Win Money!"/>
</form>

它会生成以下JSON:

{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

如果应用没有验证Content-Type,就会暴露此漏洞。,验证Content-Type的Spring MVC应用仍然会暴露此漏洞,具体方式是更新URL的后缀为“.json”,如下:

<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
    value="Win Money!"/>
</form>

CSRF和无状态的浏览器应用

如果应用是无状态的,也并不一定意味着应用就受到保护了。实际上,即使用户不需要在Web浏览器中对给定请求执行任何操作,也容易受到CSRF攻击。

例如,某个应用使用包含所有状态的自定义cookie来进行认证而不是JSESSJONID。当进行CSRF攻击时,自定义cookie将与请求一起发送,其方式与我们上一个示例中发送的JSESSIONID cookie相同。

使用Basic认证的也容易受到CSRF攻击,因为浏览器会自动在任何请求中包含用户名密码,其方式与我们上一个示例中发送的JSESSIONID cookie相同。

使用SS的CSRF保护

那么使用SS保护网站免受CSRF攻击的必要步骤是什么呢? 使用SS的CSRF保护的步骤概述如下:
- 使用正确的HTTP动词
- 配置CSRF保护
- 引入CSRF令牌

使用正确的HTTP动词

防止CSRF攻击的第一步就是使用正确的HTTP动词。具体来说,在使用SS的CSRF支持之前,需要确保应用正在使用PATCH,POST,PUT和/或DELETE来修改状态。

这不是SS支持的限制,而是对于CSRF保护的一般性要求。因为在GET请求中包含私有信息可能导致信息泄露。有关使用POST获取敏感信息的通用指南,请参阅RFC 2616第15.1.3节“在URI中编码敏感信息”

配置CSRF保护

对于无效的CSRF令牌,默认情况下SS的CSRF保护会产生HTTP 403拒绝访问。当然也可以通过配置AccessDeniedHandler来自定义如何处理InvalidCsrfTokenException

SS 4.0中,如果使用XML配置,默认启用了CSRF保护。如果要禁用CSRF保护,可以在XML中如下配置:

<http>
    <!-- ... -->
    <csrf disabled="true"/>
</http>

如果使用Java配置,也默认启用了CSRF保护。如果要禁用它,可以如下配置。其他的CSRF保护自定义配置,可以参阅csrf()的Javadoc。

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    .csrf().disable();
}
}

引入CSRF令牌

表单提交

最后一步是确保所有的PATCH,POST,PUT和DELETE方法都引入了CSRF令牌。一种方式是使用_csrf请求属性来获取当前的CsrfToken。JSP示例如下:

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
    method="post">
<input type="submit"
    value="Log out" />
<input type="hidden"
    name="${_csrf.parameterName}"
    value="${_csrf.token}"/>
</form>

更简单的方法是使用SS JSP标签库中的csrfInput标签。

如果使用的是Spring MVC <form:form>标签或Thymeleaf 2.1+并且使用了@EnableWebSecurity,则会自动引入CsrfToken(使用CsrfRequestDataValueProcessor)。

Ajax和JSON请求

如果使用的是JSON,无法在HTTP参数中提交CSRF令牌。 但可以在HTTP标头中提交令牌。典型的模式是在meta标签中包含CSRF标签。 JSP的示例如下:

<html>
<head>
    <meta name="_csrf" content="${_csrf.token}"/>
    <!-- default header name is X-CSRF-TOKEN -->
    <meta name="_csrf_header" content="${_csrf.headerName}"/>
    <!-- ... -->
</head>
<!-- ... -->

可以使用SSJSP标签库中更简单的csrfMetaTags标签。

然后可以在所有的Ajax请求中引入令牌。如果使用的是JQuery,示例如下:

$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
    xhr.setRequestHeader(header, token);
});
});

CookieCsrfTokenRepository

某些用户可能希望将CsrfToken持久化到cookie中。默认情况下,CookieCsrfTokenRepository将令牌写入名为XSRF-TOKEN的cookie,并从名为X-XSRF-TOKEN的头或HTTP参数_csrf中读取它。这些默认值来自AngularJS。
XML配置如下:

<http>
    <!-- ... -->
    <csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
    class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
    p:cookieHttpOnly="false"/>

该示例显式设置cookieHttpOnly = false。 即允许JavaScript(即AngularJS)读取token。如果不需要直接使用JavaScript读取cookie,建议省略cookieHttpOnly = false以提高安全性。

java配置方式如下:

@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

该示例显式设置cookieHttpOnly = false。 即允许JavaScript(即AngularJS)读取token。如果不需要直接使用JavaScript读取cookie,建议省略cookieHttpOnly = false(使用new CookieCsrfTokenRepository())以提高安全性。

CORS

Spring 框架为CORS提供了支持。 但必须在SS之前处理CORS,因为预检请求(pre-flight reques)不包含任何cookie(即JSESSIONID)。 如果请求不包含任何cookie并且SS首先处理请求,则该请求将确定用户未经过身份验证(因为请求中没有cookie)并拒绝它。

确保首先处理CORS的最简单方法是使用CorsFilter。用户可以通过使用以下内容提供CorsConfigurationSource来将CorsFilter与SS集成:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // by default uses a Bean by the name of corsConfigurationSource
            .cors().and()
            ...
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

或者XML配置:

<http>
    <cors configuration-source-ref="corsSource"/>
    ...
</http>
<b:bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
    ...
</b:bean>

如果正在使用Spring MVC的CORS支持,则可以省略CorsConfigurationSource的声明,SS将使用提供给Spring MVC的CORS配置:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // if Spring MVC is on classpath and no CorsConfigurationSource is provided,
            // Spring Security will use CORS configuration provided to Spring MVC
            .cors().and()
            ...
    }
}

或者XML配置:

<http>
    <!-- Default to Spring MVC's CORS configuration -->
    <cors />
    ...
</http>

安全的HTTP响应头

本章讨论SS支持的添加到响应中的各种安全头部。

默认安全头部

SS允许用户轻松地添加默认的安全头部,以保证应用的安全。SS的默认配置包含以下头部:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Strict-Transport-Security头部只在HTTPS请求中添加

HTTP安全头部剩余部分略。

Session管理

HTTP会话相关功能由SessionManagementFilterSessionAuthenticationStrategy接口一起处理,过滤器讲工作委托给它们。 典型用法包括会话固定保护攻击预防,会话超时检测以及经过身份验证的用户可能同时打开的会话数限制。

SessionManagementFilter

SessionManagementFilter根据SecurityContextHolder的当前内容检查SecurityContextRepository中的内容,以确定用户是否在当前请求期间进行了身份验证,通常是通过非交互式认证机制(如预认或remember-me)。如果Repository包含安全上下文,则过滤器不执行任何操作。如果没有,并且线程本地SecurityContext包含(非匿名)Authentication对象,则过滤器假定它们已由之前的过滤器进行了身份验证。然后它将调用配置的SessionAuthenticationStrategy

如果用户当前未经过身份验证,则过滤器将检查是否请求的是无效会话ID(例如,由于超时),并将调用配置的InvalidSessionStrategy(如果已配置)。通常会重定向到固定的URL,这些行为封装在标准实现SimpleRedirectInvalidSessionStrategy中。如前所述,在通过命名空间配置失效会话URL时也会使用后者。

SessionAuthenticationStrategy

SessionAuthenticationStrategySessionManagementFilterAbstractAuthenticationProcessingFilter使用,因此,如果使用自定义的表单登录类,则需要将其注入这两个bean。这种情况下,组合命名空间和自定义bean的典型配置可能如下所示:

<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <beans:property name="sessionAuthenticationStrategy" ref="sas" />
    ...
</beans:bean>

<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />

注意,如果在实现HttpSessionBindingListener的会话中存储bean(包括Spring会话范围的bean),则使用默认的SessionFixationProtectionStrategy可能会导致问题。 有关更多信息,请参阅此类的Javadoc。

并发控制

SS能够防止principle同时在一个应用上进行多次认证。许多ISV利用此功能来强制执行许可,而网络管理员喜欢此功能,因为它有助于防止共享登录名。 例如,可以防止用户“Batman”从两个不同的会话登录Web应用程序。可以使之前的登录过期,也可以在第二次登录时报错。注意,如果使用的是第二种方法,那么未显式注销的用户(例如,刚刚关闭浏览器的用户)将无法再次登录,直到原来的会话过期为止。

命名空间支持并发控制,因此请查看之前的命名空间章节以获取配置。 但有时还是需要自定义。

实现使用SessionAuthenticationStrategy的特殊版本,称为ConcurrentSessionControlAuthenticationStrategy

以前的并发认证检查是由ProviderManager进行的,它可以注入ConcurrentSessionController。 后者将检查用户是否超过允许的会话数。 但是,这种方法需要提前创建HTTP会话。 在SS 3中,用户首先由AuthenticationManager进行身份验证,一旦成功通过身份验证,就会创建一个会话,并检查是否允许他们打开另一个会话。

要使用并发会话支持,需要将以下内容添加到web.xml

<listener>
    <listener-class>
    org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
</listener>

此外,需要将ConcurrentSessionFilter添加到FilterChainProxyConcurrentSessionFilter需要两个属性,sessionRegistry(通常指向SessionRegistryImpl的实例)和expiredUrl(指向会话到期时要显示的页面)。 使用命名空间创建FilterChainProxy和其他默认bean的配置可能如下所示:

<http>
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />

<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:property name="sessionRegistry" ref="sessionRegistry" />
<beans:property name="expiredUrl" value="/session-expired.htm" />
</beans:bean>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>

<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg>
    <beans:list>
    <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
        <beans:constructor-arg ref="sessionRegistry"/>
        <beans:property name="maximumSessions" value="1" />
        <beans:property name="exceptionIfMaximumExceeded" value="true" />
    </beans:bean>
    <beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
    </beans:bean>
    <beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
        <beans:constructor-arg ref="sessionRegistry"/>
    </beans:bean>
    </beans:list>
</beans:constructor-arg>
</beans:bean>

<beans:bean id="sessionRegistry"
    class="org.springframework.security.core.session.SessionRegistryImpl" />

将监听器添加到web.xml后,每次HttpSession开始或终止时都将ApplicationEvent发布到Spring ApplicationContext。 这很关键,因为它允许在会话结束时通知SessionRegistryImpl。如果没有这个监听器,一旦用户超过他们的会话允许,即使他们退出另一个会话或会话超时,用户也永远无法再次登录。

获取SessionRegistry以及当前登陆的用户列表和用户的Session

通过命名空间或使用普通bean设置并发控制有一个有用的副作用,即提供可直接在应用中使用的SessionRegistry的引用,因此即使不想限制用户可能拥有的会话,也有必要配置。可以将maximumSession属性设置为-1以允许无限制的会话。 如果正在使用命名空间,则可以使用session-registry-alias属性为内部创建的SessionRegistry设置别名,从而提供可以注入到自己的bean中的引用。
getAllPrincipals()方法提供当前经过身份验证的用户的列表。可以通过调用getAllSessions(Object principa, boolean includeExpiredSessions)方法列出用户的会话,该方法返回SessionInformation对象的列表。还可以通过在SessionInformation实例上调用expireNow()来使用户的会话到期。这些方法在管理程序中很有用。有关更多信息,请查看Javadoc。

匿名认证

概览

通常认为采用“默认拒绝”策略是一种良好的安全措施,可以明确指定允许的内容并禁止其他内容。定义未经身份验证的用户可访问的内容的情况与此类似,尤其是对于Web应用。许多站点要求除了几个URL(例如主页和登录页面)之外必须对用户进行身份验证。 在这种情况下,最简单的方法是为这些特定URL定义访问配置属性。换句话说,有时可以说默认情况下需要ROLE_SOMETHING,并且只允许此规则的某些例外,例如应用程序的登录,注销和主页。也可以完全从过滤器链中忽略这些页面,从而绕过访问控制检查,但这个能不太符合需要,特别是如果页面对经过身份验证的用户的行为不同的时候。

这就是匿名认证的含义。 请注意,“匿名认证”的用户与未经认证的用户之间没有真正的概念差异。SS的匿名认证只是提供了一种更方便的方法来配置访问控制属性。 例如,调用servlet API(例如getCallerPrincipal)仍将返回null,即使SecurityContextHolder中存在匿名认证对象。
在其他情况下,匿名认证很有用,例如某个拦截器查询SecurityContextHolder以确定哪个principle负责执行当前操作。 如果类知道SecurityContextHolder始终包含Authentication对象而不是null,会更具有健壮性。

配置

使用HTTP配置SS 3.0时会自动提供匿名身份验证支持,并且可以使用<anonymous>元素自定义(或禁用)。除非使用传统的bean配置,否则不需要配置此处描述的bean。

三个类一起提供匿名身份验证功能:AnonymousAuthenticationToken实现了Authentication,存储匿名principle的GrantedAuthority;它有一个对应的的AnonymousAuthenticationProvider,这个Provider连接在ProviderManager上,接受AnonymousAuthenticationToken参数;最后,还有一个AnonymousAuthenticationFilter,它链接在正常的身份验证机制之后,如果没有已存在的Authentication,则会自动将AnonymousAuthenticationToken添加到SecurityContextHolder。 过滤器和身份验证提供程序的定义如下所示:

<bean id="anonymousAuthFilter"
    class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
<property name="key" value="foobar"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>

<bean id="anonymousAuthenticationProvider"
    class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>

key在过滤器和AuthenticationProvider之间共享,因此前者创建的令牌被后者使用。userAttributeusernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]的形式表示。这与InMemoryDaoImpluserMap属性的等号后面使用的语法相同。

如前所述,匿名认证的好处是所有URI模式都可以应用安全性。 例如:

<bean id="filterSecurityInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="securityMetadata">
    <security:filter-security-metadata-source>
    <security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/**' access='ROLE_USER'/>
    </security:filter-security-metadata-source>" +
</property>
</bean>

AuthenticationTrustResolver

最后再看一看AuthenticationTrustResolver接口及其对应的AuthenticationTrustResolverImpl实现。 此接口提供isAnonymous(Authentication)方法。ExceptionTranslationFilter在处理AccessDeniedException时使用此接口。 如果抛出AccessDeniedException,且身份验证是匿名类型,则过滤器将启动AuthenticationEntryPoint,而不是抛出403(禁止)响应,以便principle可以正确地进行认证。这是区别是必要的,否则principle将始终被视为“经过身份验证”,并且永远不会有机会通过表单、Basic、Digest或其他一些正常的身份验证机制进行登录。

上面的拦截器配置中的ROLE_ANONYMOUS属性会经常被替换为IS_AUTHENTICATED_ANONYMOUSLY,这在定义访问控制时实际上是相同的。 IS_AUTHENTICATED_ANONYMOUSLY是使用AuthenticatedVoter的一个例子,其他信息将在授权章节中讨论。AuthenticatedVoter使用AuthenticationTrustResolver处理特定配置属性并授予匿名用户访问权限。使用AuthenticatedVoter的方法更强大,因为它允许区分匿名,Remember-me和完全认证的用户。如果不需要此功能,仍然可以使用ROLE_ANONYMOUS,它将由SS的标准RoleVoter处理。

WebSocket安全

展开阅读全文

没有更多推荐了,返回首页