Spring Security 先後端分離登陸,非法請求直接返回 JSON

hello 各位小夥伴,國慶節終於過完啦,鬆哥也回來啦,今天開始我們繼續發乾貨!前端

關於 Spring Security,鬆哥以前發過多篇文章和你們聊聊這個安全框架的使用:java

  1. 手把手帶你入門 Spring Security!
  2. Spring Security 登陸添加驗證碼
  3. SpringSecurity 登陸使用 JSON 格式數據
  4. Spring Security 中的角色繼承問題
  5. Spring Security 中使用 JWT!
  6. Spring Security 結合 OAuth2

不過,今天要和小夥伴們聊一聊 Spring Security 中的另一個問題,那就是在 Spring Security 中未獲認證的請求默認會重定向到登陸頁,可是在先後端分離的登陸中,這個默認行爲則顯得很是不合適,今天咱們主要來看看如何實現未獲認證的請求直接返回 JSON ,而不是重定向到登陸頁面。json

前置知識

這裏關於 Spring Security 的基本用法我就再也不贅述了,若是小夥伴們不瞭解,能夠參考上面的 6 篇文章。後端

你們知道,在自定義 Spring Security 配置的時候,有這樣幾個屬性:跨域

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .formLogin()
            .loginProcessingUrl("/doLogin")
            .loginPage("/login")
            //其餘配置
            .permitAll()
            .and()
            .csrf().disable();
}

這裏有兩個比較重要的屬性:瀏覽器

  • loginProcessingUrl:這個表示配置處理登陸請求的接口地址,例如你是表單登陸,那麼 form 表單中 action 的值就是這裏填的值。
  • loginPage:這個表示登陸頁的地址,例如當你訪問一個須要登陸後才能訪問的資源時,系統就會自動給你經過重定向跳轉到這個頁面上來。

這種配置在先後端不分的登陸中是沒有問題的,在先後端分離的登陸中,這種配置就有問題了。我舉個簡單的例子,例如我想訪問 /hello 接口,可是這個接口須要登陸以後才能訪問,我如今沒有登陸就直接去訪問這個接口了,那麼系統會給我返回 302,讓我去登陸頁面,在先後端分離中,個人後端通常是沒有登陸頁面的,就是一個提示 JSON,例以下面這樣:安全

@GetMapping("/login")
public RespBean login() {
    return RespBean.error("還沒有登陸,請登陸!");
}

完整代碼你們能夠參考個人微人事項目。app

也就是說,當我沒有登陸直接去訪問 /hello 這個接口的時候,我會看到上面這段 JSON 字符串。在先後端分離開發中,這個看起來沒問題(後端再也不作頁面跳轉,不管發生什麼都是返回 JSON)。可是問題就出在這裏,系統默認的跳轉是一個重定向,就是說當你訪問 /hello 的時候,服務端會給瀏覽器返回 302,同時響應頭中有一個 Location 字段,它的值爲 http://localhost:8081/login ,也就是告訴瀏覽器你去訪問 http://localhost:8081/login 地址吧。瀏覽器收到指令以後,就會直接去訪問 http://localhost:8081/login 地址,若是此時是開發環境而且請求仍是 Ajax 請求,就會發生跨域。由於先後端分離開發中,前端咱們通常在 NodeJS 上啓動,而後前端的全部請求經過 NodeJS 作請求轉發,如今服務端直接把請求地址告訴瀏覽器了,瀏覽器就會直接去訪問 http://localhost:8081/login 了,而不會作請求轉發了,所以就發生了跨域問題。框架

解決方案

很明顯,上面的問題咱們不能用跨域的思路來解決,雖然這種方式看起來也能解決問題,但不是最佳方案。前後端分離

若是咱們的 Spring Security 在用戶未獲認證的時候去請求一個須要認證後才能請求的數據,此時不給用戶重定向,而是直接就返回一個 JSON,告訴用戶這個請求須要認證以後才能發起,就不會有上面的事情了。

這裏就涉及到 Spring Security 中的一個接口 AuthenticationEntryPoint ,該接口有一個實現類:LoginUrlAuthenticationEntryPoint ,該類中有一個方法 commence,以下:

/**
 * Performs the redirect (or forward) to the login form URL.
 */
public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException authException) {
    String redirectUrl = null;
    if (useForward) {
        if (forceHttps && "http".equals(request.getScheme())) {
            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 {
        redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
    }
    redirectStrategy.sendRedirect(request, response, redirectUrl);
}

首先咱們從這個方法的註釋中就能夠看出,這個方法是用來決定究竟是要重定向仍是要 forward,經過 Debug 追蹤,咱們發現默認狀況下 useForward 的值爲 false,因此請求走進了重定向。

那麼咱們解決問題的思路很簡單,直接重寫這個方法,在方法中返回 JSON 便可,再也不作重定向操做,具體配置以下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .formLogin()
            .loginProcessingUrl("/doLogin")
            .loginPage("/login")
            //其餘配置
            .permitAll()
            .and()
            .csrf().disable().exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException authException) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                RespBean respBean = RespBean.error("訪問失敗!");
                if (authException instanceof InsufficientAuthenticationException) {
                    respBean.setMsg("請求失敗,請聯繫管理員!");
                }
                out.write(new ObjectMapper().writeValueAsString(respBean));
                out.flush();
                out.close();
            }
        });
}

在 Spring Security 的配置中加上自定義的 AuthenticationEntryPoint 處理方法,該方法中直接返回相應的 JSON 提示便可。這樣,若是用戶再去直接訪問一個須要認證以後才能夠訪問的請求,就不會發生重定向操做了,服務端會直接給瀏覽器一個 JSON 提示,瀏覽器收到 JSON 以後,該幹嗎幹嗎。

結語

好了,一個小小的重定向問題和小夥伴們分享下,不知道你們有沒有看懂呢?這也是我最近在重構微人事的時候遇到的問題。預計 11 月份,微人事的 Spring Boot 版本會升級到目前最新版,請小夥伴們留意哦。

關注公衆號【江南一點雨】,專一於 Spring Boot+微服務以及先後端分離等全棧技術,按期視頻教程分享,關注後回覆 Java ,領取鬆哥爲你精心準備的 Java 乾貨!

相關文章
相關標籤/搜索