深入浅析 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缓存请求问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!