探究 Spring Security 缓存请求 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
CodingNET
V2EX    Java

探究 Spring Security 缓存请求

  •  1
     
  •   CodingNET
    Coding 2015-12-22 17:21:49 +08:00 3074 次点击
    这是一个创建于 3647 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文: https://blog.coding.net/blog/Explore-the-cache-request-of-Security-Spring
    作者:tanhe123@Coding

    为什么要缓存?

    为了更好的描述问题,我们拿使用表单认证的网站举例,简化后的认证过程分为 7 步:

    1. 用户访问网站,打开了一个链接(origin url)。

    2. 请求发送给服务器,服务器判断用户请求了受保护的资源。

    3. 由于用户没有登录,服务器重定向到登录页面

    4. 填写表单,点击登录

    5. 浏览器将用户名密码以表单形式发送给服务器

    6. 服务器验证用户名密码。成功,进入到下一步。否则要求用户重新认证(第三步)

    7. 服务器对用户拥有的权限(角色)判定: 有权限,重定向到 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 不会被缓存。

    1. 请求地址以/favicon.ico结尾
    2. header 中的content-type值为application/json
    3. header 中的X-Requested-With值为XMLHttpRequest

    可以参见: RequestCacheConfigurer 类中的私有方法 createDefaultSavedRequestMatcher 。

    附上实例代码: https://coding.net/u/tanhe123/p/SpringSecurityRequestCache

    3 条回复    2015-12-23 02:14:49 +08:00
    Lpl
        1
    Lpl  
       2015-12-22 19:52:19 +08:00 via Android   1
    security 相对于 shiro 来说有点重,而且没有 shiro 更灵活一点
    colincat
        2
    colincat  
       2015-12-23 00:34:46 +08:00 via iPhone   1
    还是 shiro 好用
    zonghua
        3
    zonghua  
       2015-12-23 02:14:49 +08:00 via iPhone   1
    Spring 全家都好,就是 security 不好
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3268 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 04:48 PVG 12:8 LAX 20:48 JFK 23:48
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86