美文网首页Spring SecurityJava学习笔记Spring Boot
Spring Security认证成功后回跳(解决前后端分离下O

Spring Security认证成功后回跳(解决前后端分离下O

作者: zerouwar | 来源:发表于2018-01-21 15:43 被阅读1086次

    前言

    Spring Security(后面简称SS)用了很长时间了,但之前一直没注意到一个有趣的特性,直到最近弄前后端分离,在OAuth2提供者(github)认证后,需要跳回前端页面(前端页面和服务端不在同个域下),然后突然一般情况下(同域),SS认证后会自动跳回认证前用户想访问的资源。由此开始寻找这个magic。

    OAuth2 的一个sso demo,有兴趣可以看一下

    问题:SS是怎么在认证成功后自动跳转到认证前用户想访问的资源(url)?

    原本我以为SS是跟SS OAuth的实现一样,通过在http报文里面传递这个url,但是看浏览器的报文内容,在认证过程中是没有传递过url的。而且在OAuth里面,OAuth2 provider这种第三方服务,不可能帮你传递这个参数,所以比较好的办法就是利用session,这也解释前后端分离情况下,SS不会(不能)帮我们跳回去前端页面

    问题的切口:SavedRequestAwareAuthenticationSuccessHandler

    之前我用SS经常接触到这个类,但是我只知道它作为认证成功的handler,在认证成功后会进行一个跳转操作。这里是部分源码

    public class SavedRequestAwareAuthenticationSuccessHandler extends
            SimpleUrlAuthenticationSuccessHandler {
        protected final Log logger = LogFactory.getLog(this.getClass());
    
        private RequestCache requestCache = new HttpSessionRequestCache();
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                HttpServletResponse response, Authentication authentication)
                throws ServletException, IOException {
            //这里取出了一个request
            SavedRequest savedRequest = requestCache.getRequest(request, response);
    
            if (savedRequest == null) {
                super.onAuthenticationSuccess(request, response, authentication);
    
                return;
            }
            String targetUrlParameter = getTargetUrlParameter();
            if (isAlwaysUseDefaultTargetUrl()
                    || (targetUrlParameter != null && StringUtils.hasText(request
                            .getParameter(targetUrlParameter)))) {
                requestCache.removeRequest(request, response);
                super.onAuthenticationSuccess(request, response, authentication);
    
                return;
            }
    
            clearAuthenticationAttributes(request);
    
            // Use the DefaultSavedRequest URL !!关键的一句!!
            String targetUrl = savedRequest.getRedirectUrl();
            logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
            // 这里有一个很明显的跳转操作,追踪targetUrl怎么来的
            getRedirectStrategy().sendRedirect(request, response, targetUrl);
        }
    
        public void setRequestCache(RequestCache requestCache) {
            this.requestCache = requestCache;
        }
    }
    

    这里有一个很奇怪的属性——savedRequest,从上面就可以看到,它是来自requestCacherequestCache的类型是HttpSessionRequestCache。这里可以看到,第一行代码就从requestCachegetRequest方法中取出了savedRequest,看一下这个方法

    public SavedRequest getRequest(HttpServletRequest currentRequest,
                HttpServletResponse response) {
            HttpSession session = currentRequest.getSession(false);
    
            if (session != null) {
                return (SavedRequest) session.getAttribute(SAVED_REQUEST);
            }
    
            return null;
        }
    

    这里就印证了我们前面的猜测,的确是保存在session里面,那么SS什么时候放进去的?毕竟我想要前后端分离下OAuth2认证后跳回前端页面

    很容易猜测是调用HttpSessionRequestCachesetRequest方法放进去,可以搜索,这里我是用上面的sso demo的日志找到ExceptionTranslationFiltersendStartAuthentication方法(把日志级别调到debug)

    ExceptionTranslationFilter

    ExceptionTranslationFilter源码里面注释的第一句话就说明了它的用处

    Handles any AccessDeniedException and AuthenticationException
    thrown within the filter chain.
    

    这里也解释了各种认证filter诸如UsernamePasswordAuthenticationFilter为什么可以随便抛出AuthenticationException

    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);
            requestCache.saveRequest(request, response);
            logger.debug("Calling Authentication entry point.");
            authenticationEntryPoint.commence(request, response, reason);
        }
    

    到这里大概明白了SS怎么保存和跳转回认证前用户想访问的资源,总结一下。SS通过ExceptionTranslationFilter在认证开始前把request缓存到session中,当认证成功后,在SavedRequestAwareAuthenticationSuccessHandler里取出缓存的request,跳转回认证前用户想访问的url

    解决前后端分离下OAuth2认证后跳回前端页面

    理解了SS怎么处理认证成功自动跳转问题,解决前后端分离下OAuth2认证成功跳转就很容易了。这里先说一下前后端跨域下OAuth2认证的流程(OAuth2协议具体请自己谷歌,我也说不清楚)

    oauth2.png

    这个流程比同域OAuth2情况下少了一步,同域下第一步应该是访问一个受保护资源,然后才开始上面流程,所以我暂时的做法是

    在/login接口处接受一个auth_url参数,表示认证成功后跳转到这个url,然后认证成功后取出这个url进行跳转

    这样只需要修改两处地方

    继承OAuth2ClientAuthenticationProcessingFilter

    class MyOAuth2ClientAuthenticationProcessingFilter extends OAuth2ClientAuthenticationProcessingFilter{
    
            private RequestCache requestCache = new HttpSessionRequestCache();
    
            public MyOAuth2ClientAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
                super(defaultFilterProcessesUrl);
            }
    
            @Override
            public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
                requestCache.saveRequest(request, response);
                return super.attemptAuthentication(request, response);
            }
        }
    

    重写这个filter的AuthenticationSuccessHandler

    filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                String authUrl = request.getParameter("auth_url");
                response.sendRedirect(authUrl);
            });
    

    这样就简单粗暴地实现了OAuth2下认证成功后可以自动跳转回认证前想访问的资源了。

    相关文章

      网友评论

      • 恒剑:有具体的实现代码吗?如何注入MyOAuth2ClientAuthenticationProcessingFilter到应用里面

      本文标题:Spring Security认证成功后回跳(解决前后端分离下O

      本文链接:https://www.haomeiwen.com/subject/muthbxtx.html