深入浅析 Spring Security 缓存请求问题
为什么要缓存?
为了更好的描述问题,我们拿使用表单认证的网站举例,简化后的认证过程分为7步:
- 用户访问网站,打开了一个链接(origin url)。
- 请求发送给服务器,服务器判断用户请求了受保护的资源。
- 由于用户没有登录,服务器重定向到登录页面
- 填写表单,点击登录
- 浏览器将用户名密码以表单形式发送给服务器
- 服务器验证用户名密码。成功,进入到下一步。否则要求用户重新认证(第三步)
- 服务器对用户拥有的权限(角色)判定: 有权限,重定向到origin url; 权限不足,返回状态码403("forbidden").
从第3步,我们可以知道,用户的请求被中断了。
用户登录成功后(第7步),会被重定向到origin url,spring security通过使用缓存的request,使得被中断的请求能够继续执行。
使用缓存
用户登录成功后,页面重定向到origin url。浏览器发出的请求优先被拦截器requestcacheawarefilter拦截,requestcacheawarefilter通过其持有的requestcache对象实现request的恢复。
public void dofilter(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception { // request匹配,则取出,该操作同时会将缓存的request从session中删除 httpservletrequest wrappedsavedrequest = requestcache.getmatchingrequest( (httpservletrequest) request, (httpservletresponse) response); // 优先使用缓存的request chain.dofilter(wrappedsavedrequest == null ? request : wrappedsavedrequest, response); }
何时缓存
首先,我们需要了解下requestcache以及exceptiontranslationfilter。
requestcache
requestcache接口声明了缓存与恢复操作。默认实现类是httpsessionrequestcache。httpsessionrequestcache的实现比较简单,这里只列出接口的声明:
public interface requestcache { // 将request缓存到session中 void saverequest(httpservletrequest request, httpservletresponse response); // 从session中取request savedrequest getrequest(httpservletrequest request, httpservletresponse response); // 获得与当前request匹配的缓存,并将匹配的request从session中删除 httpservletrequest getmatchingrequest(httpservletrequest request, httpservletresponse response); // 删除缓存的request void removerequest(httpservletrequest request, httpservletresponse response); }
exceptiontranslationfilter
exceptiontranslationfilter 是spring security的核心filter之一,用来处理authenticationexception和accessdeniedexception两种异常。
在我们的例子中,authenticationexception指的是未登录状态下访问受保护资源,accessdeniedexception指的是登陆了但是由于权限不足(比如普通用户访问管理员界面)。
exceptiontranslationfilter 持有两个处理类,分别是authenticationentrypoint和accessdeniedhandler。
exceptiontranslationfilter 对异常的处理是通过这两个处理类实现的,处理规则很简单:
- 规则1. 如果异常是 authenticationexception,使用 authenticationentrypoint 处理
- 规则2. 如果异常是 accessdeniedexception 且用户是匿名用户,使用 authenticationentrypoint 处理
- 规则3. 如果异常是 accessdeniedexception 且用户不是匿名用户,如果否则交给 accessdeniedhandler 处理。
对应以下代码
private void handlespringsecurityexception(httpservletrequest request, httpservletresponse response, filterchain chain, runtimeexception exception) throws ioexception, servletexception { if (exception instanceof authenticationexception) { logger.debug( "authentication exception occurred; redirecting to authentication entry point", exception); sendstartauthentication(request, response, chain, (authenticationexception) exception); } else if (exception instanceof accessdeniedexception) { if (authenticationtrustresolver.isanonymous(securitycontextholder .getcontext().getauthentication())) { logger.debug( "access is denied (user is anonymous); redirecting to authentication entry point", exception); sendstartauthentication( request, response, chain, new insufficientauthenticationexception( "full authentication is required to access this resource")); } else { logger.debug( "access is denied (user is not anonymous); delegating to accessdeniedhandler", exception); accessdeniedhandler.handle(request, response, (accessdeniedexception) exception); } } }
accessdeniedhandler 默认实现是 accessdeniedhandlerimpl。该类对异常的处理是返回403错误码。
public void handle(httpservletrequest request, httpservletresponse response, accessdeniedexception accessdeniedexception) throws ioexception, servletexception { if (!response.iscommitted()) { if (errorpage != null) { // 定义了errorpage // errorpage中可以操作该异常 request.setattribute(webattributes.access_denied_403, accessdeniedexception); // 设置403状态码 response.setstatus(httpservletresponse.sc_forbidden); // 转发到errorpage requestdispatcher dispatcher = request.getrequestdispatcher(errorpage); dispatcher.forward(request, response); } else { // 没有定义errorpage,则返回403状态码(forbidden),以及错误信息 response.senderror(httpservletresponse.sc_forbidden, accessdeniedexception.getmessage()); } } }
authenticationentrypoint 默认实现是 loginurlauthenticationentrypoint, 该类的处理是转发或重定向到登录页面
public void commence(httpservletrequest request, httpservletresponse response, authenticationexception authexception) throws ioexception, servletexception { string redirecturl = null; if (useforward) { if (forcehttps && "http".equals(request.getscheme())) { // first redirect the current request to https. // when that request is received, the forward to the login page will be // used. redirecturl = buildhttpsredirecturlforrequest(request); } if (redirecturl == null) { string loginform = determineurltouseforthisrequest(request, response, authexception); if (logger.isdebugenabled()) { logger.debug("server side forward to: " + loginform); } requestdispatcher dispatcher = request.getrequestdispatcher(loginform); // 转发 dispatcher.forward(request, response); return; } } else { // redirect to login page. use https if forcehttps true redirecturl = buildredirecturltologinpage(request, response, authexception); } // 重定向 redirectstrategy.sendredirect(request, response, redirecturl); }
了解完这些,回到我们的例子。
第3步时,用户未登录的情况下访问受保护资源,exceptiontranslationfilter会捕获到authenticationexception异常(规则1)。页面需要跳转,exceptiontranslationfilter在跳转前使用requestcache缓存request。
protected void sendstartauthentication(httpservletrequest request, httpservletresponse response, filterchain chain, authenticationexception reason) throws servletexception, ioexception { // sec-112: clear the securitycontextholder's authentication, as the // existing authentication is no longer considered valid securitycontextholder.getcontext().setauthentication(null); // 缓存 request requestcache.saverequest(request, response); logger.debug("calling authentication entry point."); authenticationentrypoint.commence(request, response, reason); }
一些坑
在开发过程中,如果不理解spring security如何缓存request,可能会踩一些坑。
举个简单例子,如果网站认证是信息存放在header中。第一次请求受保护资源时,请求头中不包含认证信息 ,验证失败,该请求会被缓存,之后即使用户填写了信息,也会因为request被恢复导致信息丢失从而认证失败(问题描述可以参见这里。
最简单的方案当然是不缓存request。
spring security 提供了nullrequestcache, 该类实现了 requestcache 接口,但是没有任何操作。
public class nullrequestcache implements requestcache { public savedrequest getrequest(httpservletrequest request, httpservletresponse response) { return null; } public void removerequest(httpservletrequest request, httpservletresponse response) { } public void saverequest(httpservletrequest request, httpservletresponse response) { } public httpservletrequest getmatchingrequest(httpservletrequest request, httpservletresponse response) { return null; } }
配置requestcache,使用如下代码即可:
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
以上所述是小编给大家介绍的spring security 缓存请求问题,希望对大家有所帮助