Spring Security 前後端分離登錄,非法請求直接返回 JSON
- 2019 年 10 月 10 日
- 筆記
hello 各位小夥伴,國慶節終於過完啦,松哥也回來啦,今天開始咱們繼續發乾貨!
關於 Spring Security,松哥之前發過多篇文章和大家聊聊這個安全框架的使用:
- 手把手帶你入門 Spring Security!
- Spring Security 登錄添加驗證碼
- SpringSecurity 登錄使用 JSON 格式數據
- Spring Security 中的角色繼承問題
- Spring Security 中使用 JWT!
- Spring Security 結合 OAuth2
不過,今天要和小夥伴們聊一聊 Spring Security 中的另外一個問題,那就是在 Spring Security 中未獲認證的請求默認會重定向到登錄頁,但是在前後端分離的登錄中,這個默認行為則顯得非常不合適,今天我們主要來看看如何實現未獲認證的請求直接返回 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("尚未登錄,請登錄!"); } 完整程式碼大家可以參考我的微人事項目。
也就是說,當我沒有登錄直接去訪問 /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 乾貨!
