深入浅析 Spring Security 缓存请求问题
为什么要缓存?
为了更好的描述问题,我们拿使用表单认证的网站举例,简化后的认证过程分为7步:
- 用户访问网站,打开了一个链接(originurl)。
- 请求发送给服务器,服务器判断用户请求了受保护的资源。
- 由于用户没有登录,服务器重定向到登录页面
- 填写表单,点击登录
- 浏览器将用户名密码以表单形式发送给服务器
- 服务器验证用户名密码。成功,进入到下一步。否则要求用户重新认证(第三步)
- 服务器对用户拥有的权限(角色)判定:有权限,重定向到originurl;权限不足,返回状态码403("forbidden").
从第3步,我们可以知道,用户的请求被中断了。
用户登录成功后(第7步),会被重定向到originurl,springsecurity通过使用缓存的request,使得被中断的请求能够继续执行。
使用缓存
用户登录成功后,页面重定向到originurl。浏览器发出的请求优先被拦截器RequestCacheAwareFilter拦截,RequestCacheAwareFilter通过其持有的RequestCache对象实现request的恢复。
publicvoiddoFilter(ServletRequestrequest,ServletResponseresponse, FilterChainchain)throwsIOException,ServletException{ //request匹配,则取出,该操作同时会将缓存的request从session中删除 HttpServletRequestwrappedSavedRequest=requestCache.getMatchingRequest( (HttpServletRequest)request,(HttpServletResponse)response); //优先使用缓存的request chain.doFilter(wrappedSavedRequest==null?request:wrappedSavedRequest, response); }
何时缓存
首先,我们需要了解下RequestCache以及ExceptionTranslationFilter。
RequestCache
RequestCache接口声明了缓存与恢复操作。默认实现类是HttpSessionRequestCache。HttpSessionRequestCache的实现比较简单,这里只列出接口的声明:
publicinterfaceRequestCache{ //将request缓存到session中 voidsaveRequest(HttpServletRequestrequest,HttpServletResponseresponse); //从session中取request SavedRequestgetRequest(HttpServletRequestrequest,HttpServletResponseresponse); //获得与当前request匹配的缓存,并将匹配的request从session中删除 HttpServletRequestgetMatchingRequest(HttpServletRequestrequest, HttpServletResponseresponse); //删除缓存的request voidremoveRequest(HttpServletRequestrequest,HttpServletResponseresponse); }
ExceptionTranslationFilter
ExceptionTranslationFilter是SpringSecurity的核心filter之一,用来处理AuthenticationException和AccessDeniedException两种异常。
在我们的例子中,AuthenticationException指的是未登录状态下访问受保护资源,AccessDeniedException指的是登陆了但是由于权限不足(比如普通用户访问管理员界面)。
ExceptionTranslationFilter持有两个处理类,分别是AuthenticationEntryPoint和AccessDeniedHandler。
ExceptionTranslationFilter对异常的处理是通过这两个处理类实现的,处理规则很简单:
- 规则1.如果异常是AuthenticationException,使用AuthenticationEntryPoint处理
- 规则2.如果异常是AccessDeniedException且用户是匿名用户,使用AuthenticationEntryPoint处理
- 规则3.如果异常是AccessDeniedException且用户不是匿名用户,如果否则交给AccessDeniedHandler处理。
对应以下代码
privatevoidhandleSpringSecurityException(HttpServletRequestrequest, HttpServletResponseresponse,FilterChainchain,RuntimeExceptionexception) throwsIOException,ServletException{ if(exceptioninstanceofAuthenticationException){ logger.debug( "Authenticationexceptionoccurred;redirectingtoauthenticationentrypoint", exception); sendStartAuthentication(request,response,chain, (AuthenticationException)exception); } elseif(exceptioninstanceofAccessDeniedException){ if(authenticationTrustResolver.isAnonymous(SecurityContextHolder .getContext().getAuthentication())){ logger.debug( "Accessisdenied(userisanonymous);redirectingtoauthenticationentrypoint", exception); sendStartAuthentication( request, response, chain, newInsufficientAuthenticationException( "Fullauthenticationisrequiredtoaccessthisresource")); } else{ logger.debug( "Accessisdenied(userisnotanonymous);delegatingtoAccessDeniedHandler", exception); accessDeniedHandler.handle(request,response, (AccessDeniedException)exception); } } }
AccessDeniedHandler默认实现是AccessDeniedHandlerImpl。该类对异常的处理是返回403错误码。
publicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse, AccessDeniedExceptionaccessDeniedException)throwsIOException, ServletException{ if(!response.isCommitted()){ if(errorPage!=null){//定义了errorPage //errorPage中可以操作该异常 request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); //设置403状态码 response.setStatus(HttpServletResponse.SC_FORBIDDEN); //转发到errorPage RequestDispatcherdispatcher=request.getRequestDispatcher(errorPage); dispatcher.forward(request,response); } else{//没有定义errorPage,则返回403状态码(Forbidden),以及错误信息 response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } } }
AuthenticationEntryPoint默认实现是LoginUrlAuthenticationEntryPoint,该类的处理是转发或重定向到登录页面
publicvoidcommence(HttpServletRequestrequest,HttpServletResponseresponse, AuthenticationExceptionauthException)throwsIOException,ServletException{ StringredirectUrl=null; if(useForward){ if(forceHttps&&"http".equals(request.getScheme())){ //FirstredirectthecurrentrequesttoHTTPS. //Whenthatrequestisreceived,theforwardtotheloginpagewillbe //used. redirectUrl=buildHttpsRedirectUrlForRequest(request); } if(redirectUrl==null){ StringloginForm=determineUrlToUseForThisRequest(request,response, authException); if(logger.isDebugEnabled()){ logger.debug("Serversideforwardto:"+loginForm); } RequestDispatcherdispatcher=request.getRequestDispatcher(loginForm); //转发 dispatcher.forward(request,response); return; } } else{ //redirecttologinpage.UsehttpsifforceHttpstrue redirectUrl=buildRedirectUrlToLoginPage(request,response,authException); } //重定向 redirectStrategy.sendRedirect(request,response,redirectUrl); }
了解完这些,回到我们的例子。
第3步时,用户未登录的情况下访问受保护资源,ExceptionTranslationFilter会捕获到AuthenticationException异常(规则1)。页面需要跳转,ExceptionTranslationFilter在跳转前使用requestCache缓存request。
protectedvoidsendStartAuthentication(HttpServletRequestrequest, HttpServletResponseresponse,FilterChainchain, AuthenticationExceptionreason)throwsServletException,IOException{ //SEC-112:CleartheSecurityContextHolder'sAuthentication,asthe //existingAuthenticationisnolongerconsideredvalid SecurityContextHolder.getContext().setAuthentication(null); //缓存request requestCache.saveRequest(request,response); logger.debug("CallingAuthenticationentrypoint."); authenticationEntryPoint.commence(request,response,reason); }
一些坑
在开发过程中,如果不理解SpringSecurity如何缓存request,可能会踩一些坑。
举个简单例子,如果网站认证是信息存放在header中。第一次请求受保护资源时,请求头中不包含认证信息,验证失败,该请求会被缓存,之后即使用户填写了信息,也会因为request被恢复导致信息丢失从而认证失败(问题描述可以参见这里。
最简单的方案当然是不缓存request。
springsecurity提供了NullRequestCache,该类实现了RequestCache接口,但是没有任何操作。
publicclassNullRequestCacheimplementsRequestCache{ publicSavedRequestgetRequest(HttpServletRequestrequest, HttpServletResponseresponse){ returnnull; } publicvoidremoveRequest(HttpServletRequestrequest,HttpServletResponseresponse){ } publicvoidsaveRequest(HttpServletRequestrequest,HttpServletResponseresponse){ } publicHttpServletRequestgetMatchingRequest(HttpServletRequestrequest, HttpServletResponseresponse){ returnnull; } }
配置requestCache,使用如下代码即可:
http.requestCache().requestCache(newNullRequestCache());
补充
默认情况下,三种request不会被缓存。
- 请求地址以/favicon.ico结尾
- header中的content-type值为application/json
- header中的X-Requested-With值为XMLHttpRequest
可以参见:RequestCacheConfigurer类中的私有方法createDefaultSavedRequestMatcher。
附上实例代码:https://coding.net/u/tanhe123/p/SpringSecurityRequestCache
以上所述是小编给大家介绍的SpringSecurity缓存请求问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!