前言

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下认证成功后可以自动跳转回认证前想访问的资源了。



作者:zerouwar
链接:https://www.jianshu.com/p/39f46c8de9c1
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

Logo

Authing 是一款以开发者为中心的全场景身份云产品,集成了所有主流身份认证协议,为企业和开发者提供完善安全的用户认证和访问管理服务

更多推荐