1. 程式人生 > >Spring Security認證成功後回跳(解決前後端分離下OAuth2認證成功回跳)

Spring Security認證成功後回跳(解決前後端分離下OAuth2認證成功回跳)

前言

Spring Security(後面簡稱SS)用了很長時間了,但之前一直沒注意到一個有趣的特性,直到最近弄前後端分離,在OAuth2提供者(github)認證後,需要跳回前端頁面(前端頁面和服務端不在同個域下),然後突然一般情況下(同域),SS認證後會自動跳回認證前使用者想訪問的資源。由此開始尋找這個magic。

問題: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下認證成功後可以自動跳轉回認證前想訪問的資源了。



作者:zerouwar
連結:https://www.jianshu.com/p/39f46c8de9c1
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。