为什么要缓存?
为了更好的描述问题,我们拿使用表单认证的网站举例,简化后的认证过程分为7步:
用户访问网站,打开了一个链接(origin url)。
请求发送给服务器,服务器判断用户请求了受保护的资源。
由于用户没有登录,服务器重定向到登录页面
填写表单,点击登录
浏览器将用户名密码以表单形式发送给服务器
服务器验证用户名密码。成功,进入到下一步。否则要求用户重新认证(第三步)
服务器对用户拥有的权限(角色)判定: 有权限,重定向到 origin url; 权限不足,返回状态码 403(“forbidden”).
从第3步,我们可以知道,用户的请求被中断了。
用户登录成功后(第7步),会被重定向到 origin url,spring security 通过使用缓存的 request,使得被中断的请求能够继续执行。
使用缓存
用户登录成功后,页面重定向到 origin url。浏览器发出的请求优先被拦截器 RequestCacheAwareFilter 拦截,RequestCacheAwareFilter 通过其持有的 RequestCache 对象实现 request 的恢复。
1 | public void doFilter(ServletRequest request, ServletResponse response, |
何时缓存
首先,我们需要了解下 RequestCache 以及 ExceptionTranslationFilter。
RequestCache
RequestCache 接口声明了缓存与恢复操作。默认实现类是 HttpSessionRequestCache
。HttpSessionRequestCache 的实现比较简单,这里只列出接口的声明:
1 | public interface RequestCache { |
ExceptionTranslationFilter
ExceptionTranslationFilter 是 Spring Security 的核心 filter 之一,用来处理 AuthenticationException 和 AccessDeniedException 两种异常。
在我们的例子中,AuthenticationException 指的是未登录状态下访问受保护资源,AccessDeniedException 指的是登陆了但是由于权限不足(比如普通用户访问管理员界面)。
ExceptionTranslationFilter 持有两个处理类,分别是 AuthenticationEntryPoint 和 AccessDeniedHandler。
ExceptionTranslationFilter 对异常的处理是通过这两个处理类实现的,处理规则很简单:
规则1: 如果异常是 AuthenticationException,使用 AuthenticationEntryPoint 处理
规则2: 如果异常是 AccessDeniedException 且用户是匿名用户,使用 AuthenticationEntryPoint 处理
规则3: 如果异常是 AccessDeniedException 且用户不是匿名用户,如果否则交给 AccessDeniedHandler 处理
对应以下代码
1 | private void handleSpringSecurityException(HttpServletRequest request, |
AccessDeniedHandler 默认实现是 AccessDeniedHandlerImpl。该类对异常的处理是返回403错误码。
1 | public void handle(HttpServletRequest request, HttpServletResponse response, |
AuthenticationEntryPoint 默认实现是 LoginUrlAuthenticationEntryPoint, 该类的处理是转发或重定向到登录页面
1 | public void commence(HttpServletRequest request, HttpServletResponse response, |
了解完这些,回到我们的例子。
第3步时,用户未登录的情况下访问受保护资源,ExceptionTranslationFilter 会捕获到 AuthenticationException 异常(规则1)。页面需要跳转,ExceptionTranslationFilter 在跳转前使用 requestCache 缓存 request。
1 | protected void sendStartAuthentication(HttpServletRequest request, |
一些坑
在开发过程中,如果不理解 Spring Security 如何缓存 request,可能会踩一些坑。
举个简单例子,如果网站认证是信息存放在 header 中。第一次请求受保护资源时,请求头中不包含认证信息 ,验证失败,该请求会被缓存,之后即使用户填写了信息,也会因为request被恢复导致信息丢失从而认证失败(问题描述可以参见这里。
最简单的方案当然是不缓存request。
spring security 提供了 NullRequestCache, 该类实现了 RequestCache 接口,但是没有任何操作。
1 | public class NullRequestCache implements RequestCache { |
配置 requestCache,使用如下代码即可:
1 | http.requestCache().requestCache(new NullRequestCache()); |
补充
默认情况下,三种 request 不会被缓存。
- 请求地址以
/favicon.ico
结尾 - header中的
content-type
值为application/json
- header中的
X-Requested-With
值为XMLHttpRequest
可以参见:RequestCacheConfigurer
类中的私有方法 createDefaultSavedRequestMatcher
。
附上实例代码: https://coding.net/u/tanhe123/p/SpringSecurityRequestCache