前言
- 想象一个场景:
- 当我们想要在某个论坛访问某个帖子的时候,因为没有认证,此时就会跳转到登录页
- 然后进行认证,认证完成后就会自动跳转到那个帖子中
- 而这个功能正是基于RequestCacheAwareFilter来实现了
1. RequestCacheConfigurer
- RequestCacheConfigurer是RequestCacheAwareFilter对应的配置类,也正是默认开启的配置类之一
1.1 requestCache(...)
- RequestCacheConfigurer中可供用户调用的只有一个requestCache(...)
java
public RequestCacheConfigurer<H> requestCache(RequestCache requestCache) {
getBuilder().setSharedObject(RequestCache.class, requestCache);
return this;
}
- 可以看出就是往SharedObject中注册一个RequestCache
- RequestCache:在身份认证发生后,缓存当前请求以供后续使用
java
public interface RequestCache {
/**
* <ul>
* <li>
* 在身份验证发生后,缓存当前请求以供以后使用
* 比如说:在一个论坛的帖子中,进行回帖,然后因为没有登录,先将回帖的信息保存到请求缓存器中,再重定向到登录页
* 然后登陆成功后就会获取请求缓存器中上次保存的回帖信息,然后将当前request进行包装,变成一个回帖请求
* </li>
* <li>
* 通常发生在{@link org.springframework.security.web.access.ExceptionTranslationFilter}中
* </li>
* </ul>
*/
void saveRequest(HttpServletRequest request, HttpServletResponse response);
/**
* 返回已保存的请求,保留其缓存状态。
*/
SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
/**
* 如果与当前请求匹配,则返回保存的请求的包装器。保存的请求应该从缓存中删除。
*/
HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response);
/**
* 删除缓存的请求缓存
*/
void removeRequest(HttpServletRequest request, HttpServletResponse response);
}
- RequestCache有两个实现,区别就在于将原请求信息保存在哪:
- CookieRequestCache:
- HttpSessionRequestCache:
- 简单的看下HttpSessionRequestCache的saveRequest(...)方法
java
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
//某些请求不允许缓存请求数据
if (!this.requestMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(
LogMessage.format("Did not save request since it did not match [%s]", this.requestMatcher));
}
return;
}
//创建默认保存对象
DefaultSavedRequest savedRequest = new DefaultSavedRequest(request, this.portResolver);
//保存到Session中
if (this.createSessionAllowed || request.getSession(false) != null) {
request.getSession().setAttribute(this.sessionAttrName, savedRequest);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Saved request %s to session", savedRequest.getRedirectUrl()));
}
}
else {
this.logger.trace("Did not save request since there's no session and createSessionAllowed is false");
}
}
- 可以看到原请求被保存为DefaultSavedRequest对象
java
public DefaultSavedRequest(HttpServletRequest request, PortResolver portResolver) {
Assert.notNull(request, "Request required");
Assert.notNull(portResolver, "PortResolver required");
//添加Cookie
addCookies(request.getCookies());
//添加请求头
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
//某些请求头不需要缓存
if (HEADER_IF_MODIFIED_SINCE.equalsIgnoreCase(name) || HEADER_IF_NONE_MATCH.equalsIgnoreCase(name)) {
continue;
}
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
this.addHeader(name, values.nextElement());
}
}
//添加环境
addLocales(request.getLocales());
//添加参数
addParameters(request.getParameterMap());
// Primitives
this.method = request.getMethod();
this.pathInfo = request.getPathInfo();
this.queryString = request.getQueryString();
this.requestURI = request.getRequestURI();
this.serverPort = portResolver.getServerPort(request);
this.requestURL = request.getRequestURL().toString();
this.scheme = request.getScheme();
this.serverName = request.getServerName();
this.contextPath = request.getContextPath();
this.servletPath = request.getServletPath();
}
1.2 init(...)
-
没什么好说的,就是确保SharedObject中会有一个RequestCache
java@Override public void init(H http) { http.setSharedObject(RequestCache.class, getRequestCache(http)); } /** * 获得请求缓存器 */ private RequestCache getRequestCache(H http) { //先尝试从sharedObjects中获取 RequestCache result = http.getSharedObject(RequestCache.class); if (result != null) { return result; } //尝试从容器中获取 result = getBeanOrNull(RequestCache.class); if (result != null) { return result; } //还是没有,就创建一个基于HttpSession的请求缓冲器 HttpSessionRequestCache defaultCache = new HttpSessionRequestCache(); defaultCache.setRequestMatcher(createDefaultSavedRequestMatcher(http)); return defaultCache; }
- 但是这里有一个重点就是有哪些请求才需要被保存呢,这就需要注册指定的请求匹配器(RequestMatcher)
- 下图就是RequestCacheConfigurer中默认注册的请求匹配器了
javaprivate RequestMatcher createDefaultSavedRequestMatcher(H http) { //第一个:不缓存路径为/**/favicon.*的请求 RequestMatcher notFavIcon = new NegatedRequestMatcher(new AntPathRequestMatcher("/**/favicon.*")); //第二个:不缓存异步请求 RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); boolean isCsrfEnabled = http.getConfigurer(CsrfConfigurer.class) != null; List<RequestMatcher> matchers = new ArrayList<>(); //如果开启了Csrf的保护 if (isCsrfEnabled) { //第三个:为了安全考虑,只能缓存GET方式的请求 RequestMatcher getRequests = new AntPathRequestMatcher("/**", "GET"); matchers.add(0, getRequests); } matchers.add(notFavIcon); //第四个:不缓存媒体类型为 application/json 的请求 matchers.add(notMatchingMediaType(http, MediaType.APPLICATION_JSON)); matchers.add(notXRequestedWith); //第四个:不缓存媒体类型为 multipart/form-data 的请求 matchers.add(notMatchingMediaType(http, MediaType.MULTIPART_FORM_DATA)); //第四个:不缓存媒体类型为 text/event-stream 的请求 matchers.add(notMatchingMediaType(http, MediaType.TEXT_EVENT_STREAM)); return new AndRequestMatcher(matchers); }
- 要注意的是这里注册的是一个AndRequestMatcher,也就说请求需要满足内部的所有RequestMatcher才算匹配成功
1.3 configure(...)
-
configure(...):注册过滤器
java@Override public void configure(H http) { //获得请求缓存器 RequestCache requestCache = getRequestCache(http); //创建对应过滤器 RequestCacheAwareFilter requestCacheFilter = new RequestCacheAwareFilter(requestCache); requestCacheFilter = postProcess(requestCacheFilter); http.addFilter(requestCacheFilter); } /** * 获得请求缓存器 */ private RequestCache getRequestCache(H http) { //先尝试从sharedObjects中获取 RequestCache result = http.getSharedObject(RequestCache.class); if (result != null) { return result; } //尝试从容器中获取 result = getBeanOrNull(RequestCache.class); if (result != null) { return result; } //还是没有,就创建一个基于HttpSession的请求缓冲器 HttpSessionRequestCache defaultCache = new HttpSessionRequestCache(); defaultCache.setRequestMatcher(createDefaultSavedRequestMatcher(http)); return defaultCache; }
2. ExceptionTranslationFilter
-
前面说了RequestCacheAwareFilter是当用户未认证的情况下,将原请求保存起来供后续认证完成后使用的
-
所以说就一定有一个地方是判断认证失败了的,然后将请求保存起来的,这个地方就是ExceptionTranslationFilter
-
ExceptionTranslationFilter:是专门用于处理FilterSecurityInterceptor抛出的两大异常
- 认证异常:AuthenticationException
- 访问被拒绝异常:AccessDeniedException
-
在对应配置类的configure(...)方法中从SharedObject中获得了RequestCache并将其保存在ExceptionTranslationFilter中
java@Override public void configure(H http) { ... //创建处理异常的过滤器,还传入了请求缓存器 ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, getRequestCache(http)); ... } /** * 重点:从SharedObject获得RequestCache */ private RequestCache getRequestCache(H http) { RequestCache result = http.getSharedObject(RequestCache.class); if (result != null) { return result; } return new HttpSessionRequestCache(); }
-
而在ExceptionTranslationFilter的sendStartAuthentication(...)方法中,就将原请求保存起来了
java/** * 处理认证异常 */ protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { //清除存储在线程级别的上下文策略的认证信息,HttpSession级别的会在SecurityContextPersistenceFilter的finally代码块中被更新 //因为现有的认证不再被认为有效 SecurityContext context = SecurityContextHolder.createEmptyContext(); SecurityContextHolder.setContext(context); //将当前的请求放入请求缓存器 //这样当重新登录后,还能将请求包装为这一次请求 this.requestCache.saveRequest(request, response); //执行认证异常处理器 this.authenticationEntryPoint.commence(request, response, reason); }
3 RequestCacheAwareFilter
- 此过滤器的源码较少, 直接看doFilter(...)方法
- 是直接包装当前请求,转换为认证前的请求
java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//从请求缓冲器中获得上一次请求是数据,并重写包装为一个新的HttpServletRequest
HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest) request,
(HttpServletResponse) response);
chain.doFilter((wrappedSavedRequest != null) ? wrappedSavedRequest : request, response);
}
- 最后再讲讲在SpringSecurity中RequestCache机制的完整逻辑
- 用户访问/A接口,没有对应的权限就会被ExceptionTranslationFilter包装当前请求
- /A接口:需要认证或者说当前用户没有权限的接口
- 然后会被身份认证入口点(AuthenticationEntryPoint)重定向到登录页
- 用户在登录页输入用户名和密码向服务器发起认证请求
- 紧接着在UsernamePasswordAuthenticationFilter中完成认证后,到达RequestCacheAwareFilter,并将当前请求包装为原请求也就是/A请求
- 注意此时的请求路径已经由/login变为了/A,而此时就会进入SpringMVC的DispatcherServlet中
- 然后我们看专门处理@RequestMapping的RequestMappingHandlerMapping中是如何获取处理方法的
- 用户访问/A接口,没有对应的权限就会被ExceptionTranslationFilter包装当前请求
- 没错正是根据请求路径/A获取对应的处理方法