Spring Oauth2: Redirect back to origin url after login successfully.

肖老闆發表於2018-08-17

Spring OAuth2 登入成功後跳轉到原來的地址

本文介紹基於Spring Cloud Zuul實現的OAuth2 Clinet在跳轉到OAuth2 Server的登入頁面成功登入後如何redirect跳轉回原來的地址,即redirect back to origin url after login successfully.

Spring OAuth SSO的基本過程有以下幾步:

  1. 使用者訪問網站,開啟了一個連結(origin url) ;
  2. 訪問請求傳送給Zuul Gateway伺服器(該伺服器同時也是OAuth2 Client),伺服器判斷該請求是否訪問了受保護的資源;
  3. 如果是訪問受保護的資源則判斷登入狀態,沒有登入則重定向到OAuth2 Server的登入頁面;
  4. 使用者輸入賬戶資訊進行登入,登入成功後將使用者資訊保持在Spring Security Context中並進行頁面跳轉。

由於之前系統框架中只有一個客戶端,而且我們的客戶端是基於Vue的單頁面應用,所以就在OAuth2 Server中直接配置了一個登入成功後的預設地址。最近需要增加一個客戶端,在使用同一個OAuth2 Server作為統一登入時就面臨著必須讓使用者從哪裡來回哪裡去的問題!經過查閱文件和跟蹤除錯,OAuth2 Client在檢測到使用者未登入訪問受保護的資源時會直接redirectOAuth2 Server,redirect的url是基於OAuth2 Client配置的security.oauth2.client.user-authorization-urisecurity.oauth2.client.client-id進行拼接而成的,如下所示:

http://127.0.0.1:8080/auth/oauth/authorize?client_id=oauth2_client_id&redirect_uri=http://127.0.0.1/login&response_type=code&state=B5c3xa

除了這段redirect url中的資料之外,OAuth2 Client沒有再給OAuth2 Server任何其他資料,所以我需要將origin url作為query string引數放到這段redirect url裡面去。解決方案分為三步:

  1. OAuth2 Client中獲取到origin url並在組裝oauth2的redirect url時新增到query string中傳遞給OAuth2 Server
  2. OAuth2 Server在自己的LoginSuccessHandler中從request session中拿出SPRING_SECURITY_SAVED_REQUEST獲取到redirect url並解析出original url
  3. response.sendRedirect(originUrl)

基於spring cloud zuul gatewayOAuth2 Client配置:

  1. 重寫LoginUrlAuthenticationEntryPointorigin url儲存在session中;
  2. 重寫DefaultRedirectStrategysession中的origin url拼接在redirect url
/**
 * UI代理伺服器,基於zuul的 oauth2 client.
 */
@SpringBootApplication
@EnableOAuth2Sso
@EnableZuulProxy
public class UIProxyApplication extends WebSecurityConfigurerAdapter{

    public static void main(String[] args) {
        SpringApplication.run(UIProxyApplication.class, args);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println(logoutUrl);
        http.exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint("/login"));
        http.authorizeRequests()
            .antMatchers("/login", "/api/**").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable()
    }

    @Bean
    public LogoutHandler logoutHandler() {
        return new MyLogoutHandler();
    }

    @Bean
    public FilterRegistrationBean oauth2ClientFilterRegistration(
            OAuth2ClientContextFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        filter.setRedirectStrategy(new OAuthRedirectStrategy());
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }

    class OAuthRedirectStrategy extends DefaultRedirectStrategy {
        @Override
        public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
            String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
            redirectUrl = response.encodeRedirectURL(redirectUrl);
            if (logger.isDebugEnabled()) {
                logger.debug("Custom BMA SecurityConfiguration Redirecting to '" + redirectUrl + "'");
            }

            String requestUrl = request.getSession().getAttribute("requestUrl").toString();
            redirectUrl += "&request_url=" + requestUrl;
            response.sendRedirect(redirectUrl);
        }
    }

        //轉發或者重定向到登入頁面
    public class UnauthorizedEntryPoint extends LoginUrlAuthenticationEntryPoint {
        public UnauthorizedEntryPoint(String loginFormUrl) {
            super(loginFormUrl);
        }

        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            request.getSession().setAttribute("requestUrl", request.getRequestURL());
            super.commence(request, response, authException);
        }
    }
}

OAuth2 ServerLoginSuccessHandler處理redirect urlredirectorigin url

public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication) throws IOException,
            ServletException {

        Object principal = authentication.getPrincipal();
        String username = "";
        if (principal instanceof UserDetails) {
            username = ((UserDetails) principal).getUsername();
        } else {
            username = principal.toString();
        }

        if (request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST") != null) {
            String savedRequest = request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST").toString();
            String[] params = savedRequest.split("&");
            for (int i = 0; i < params.length; i++) {
                if (params[i].indexOf("request_url") != -1) {
                    String requestUrl = params[i].split("=")[1].split("]")[0];
                    response.sendRedirect(requestUrl);
                }
            }
        } else {
            super.onAuthenticationSuccess(request, response, token);
        }
    }
}

OAuth2 Server中配置LoginSuccessHandler

/**
 * 安全配置類
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //customize login page.
        httpSecurity
                .authorizeRequests().antMatchers(
                "/login/**",
                "/js/**",
                "/css/**",
                "/img/**").permitAll()
                .anyRequest().authenticated()
                .and().formLogin().loginPage("/login").permitAll()
                .and().csrf().disable()

        httpSecurity.addFilter(myUsernamePasswordAuthenticationFilter());
    }

    @Bean
    FilterRegistrationBean forwardedHeaderFilter() {
        FilterRegistrationBean filterRegBean = new FilterRegistrationBean();
        filterRegBean.setFilter(new ForwardedHeaderFilter());
        filterRegBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return filterRegBean;
    }

    @Bean
    public UsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() throws Exception{
        MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();

        filter.setAuthenticationManager(authenticationManager());
        filter.setAuthenticationSuccessHandler(loginSuccessHandler());
        return filter;
    }

    @Bean
    public LoginSuccessHandler loginSuccessHandler() {
        return new LoginSuccessHandler();
    }

}

一些說明:

  1. OAuth2是基於JWT 的,相關配置沒有在這裡給出;
  2. 增加UsernamePasswordAuthenticationFilterLoginSuccessHandler是原有專案需求,不是為了這次的跳轉才加的;
  3. 這個方法可能還不是common practise,有更好的方法歡迎交流!
  4. 參考連結: https://stackoverflow.com/questions/51456479/stateless-spring-jwt-application-enableoauth2client
  5. https://segmentfault.com/a/1190000012137647
  6. https://blog.csdn.net/honghailiang888/article/details/52679264
  7. https://www.baeldung.com/spring-redirect-and-forward

相關文章