一個詭異的登錄問題

美好週末,從解 BUG 開始!

週五本來想早點下班,臨了有一個簡單的需求突然提上來,心想著整完了就走,沒想到一下折騰了 1 個多小時才搞定,愉快的週末就從加班中開啟了。回到家裡把這件事覆盤一下,小夥伴們看看是否能夠從中 GET 到一些未知的東西。

需求是這樣的:

項目是 Spring Boot 項目,裡邊對請求進行了劃分,有的請求是 HTTP 協議,有的請求是 HTTPS 協議,項目規定,有一些請求必須是 HTTPS 協議,例如 /https 接口,該接口必須使用 HTTPS 協議訪問,如果用戶使用了 HTTP 協議訪問,那麼會自動發生請求重定向,重定向到 HTTPS 協議上;同時也有一些請求必須是 HTTP 協議,例如 /http 接口,該接口必須使用 HTTP 協議訪問,如果用戶使用了 HTTPS 協議訪問,那麼會自動發生請求重定向,重定向到 HTTP 協議上。對於一些沒有明確規定的接口,當用戶訪問 HTTP 協議時,不需要自動跳轉到 HTTPS 協議上,即用戶如果使用 HTTP 協議就是 HTTP 協議,用戶如果使用 HTTPS 協議就是 HTTPS 協議。

這個任務實在是小 case,由於項目本身已經支持 HTTPS 了,我只需要再添加一個 HTTP 監聽的端口即可(Spring Boot 中配置 Https),添加如下配置:

<code>@Configuration
public class TomcatConfig {
    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addAdditionalTomcatConnectors(createTomcatConnector());
        return factory;
    }
    private Connector createTomcatConnector() {
        Connector connector = new
                Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(8080);
        return connector;
    }
}
/<code>

添加完成後,項目啟動日誌如下:

一個詭異的登錄問題

可以看到,項目已經同時支持 HTTPS 和 HTTP 了,兩者分別在不同的端口上監聽。

接下來利用 Spring Security 中的 HTTPS 校驗轉發功能對請求進行區分:

<code>@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
  //省略其他
                .requiresChannel()
                .antMatchers("/https").requiresSecure()
                .antMatchers("/http").requiresInsecure()
                .and()
                .csrf().disable();
    }
}
/<code>

大功告成,so easy!

配置完成後,啟動項目,如下兩個地址都可以訪問到登錄頁面:

  • http://localhost:8080/login
  • https://localhost:8444/login

可以使用任意一個地址登錄。

假如使用了 HTTP 協議登錄,登錄成功後,如果直接訪問 http://localhost:8080/http 請求,可以直接訪問到,沒有任何問題;如果登錄成功後訪問 http://localhost:8080/https 請求,則會自動重定向到 https://localhost:8444/https,一切看起來都很完美。

似乎可以下班了。

別急,我再用 HTTPS 登錄測試了,打開 https://localhost:8444/login 頁面,登錄成功,請求 https://localhost:8444/https 地址沒有問題,請求 https://localhost:8444/http ,傻眼了。

當我使用 HTTPS 登錄成功後,請求 https://localhost:8444/http 地址時,按理說會重定向到 http://localhost:8080/http,結果並沒有,而是重定向到登錄頁面,這是咋回事?更為詭異的是,現在在登錄頁面,無論我怎麼做,都登錄失敗。

看來 965 到底是海市蜃樓,還是繼續解決問題吧。

那就從登錄開始,好端端的為什麼突然就無法登錄了呢?

先清除瀏覽器緩存試試?咦,清除瀏覽器緩存後登錄成功了!

經過多次嘗試後,我總結出來了如下規律:

如果使用 HTTP 協議登錄,登錄成功後,HTTP 協議和 HTTPS 協議之間互相重定向沒有任何問題。如果使用了 HTTPS 協議登錄,登錄成功後,HTTPS 協議重定向到 HTTP 協議時,需要重新登錄,並且在登錄頁面總是登錄失敗,需要清除瀏覽器緩存才能登錄成功。

先找到規律這個很重要,有的小夥伴微信問松哥問題時候,喜歡說,這個東西它一會可以一會又不行,老實說,這個問題提的非常業餘!所有看似無規律的 BUG 背後都是有規律的,找到規律才是解決 BUG 的第一步。

在整個過程中,最為詭異的是從 HTTPS 重定向到 HTTP 之後,無論怎麼樣都登錄不了,服務端重啟也沒用,只能清除瀏覽器緩存,這個非常奇怪,我覺得就先從這個地方入手 DEBUG。

那就 DEBUG,瀏覽器發送登錄請求,服務端我把 Spring Security 登錄流程走了一遍,貌似沒問題,登錄成功後重定向到 http://localhost:8080/ ,這也是正常的,繼續 DEBUG,重定向到 http://localhost:8080/ 地址時,出現了一點點意外,該請求在 Spring Security 過濾器鏈的最後一個環節 FilterSecurityInterceptor 中執行時候拋出異常了,異常原因是因為檢查用戶身份,發現這是個匿名用戶!(一文搞定 Spring Security 異常處理機制!)

不對呀,一開始已經登錄成功了,怎麼會是匿名用戶呢?Spring Security 在登錄成功後,會將用戶信息保存在 SecurityContextHolder 中(在 Spring Security 中,我就想從子線程獲取用戶登錄信息,怎麼辦?),是不是沒保存?重新檢查登錄過程,發現登錄成功後是保存了用戶信息的。但是當登錄成功後再次發送請求卻說我沒登錄,還剩一種可能,是不是前端請求的問題,JSESSIONID 拿錯了?或者沒拿?

瀏覽器 F12 檢查前端請求,發現登錄成功後,重定向到 http://localhost:8080/ 地址時,果然沒有攜帶 Cookie!

現在的問題是為什麼它就不攜帶 Cookie 呢?

一瞬間腦子裡閃過了諸多可能性,是不是瀏覽器 SameSite 機制導致的?是不是。。。最後思維定格在 Cookie 的 Secure 標記上。

如果請求是 HTTPS,則服務端響應的 Cookie 中含有 Secure 標記:

一個詭異的登錄問題

這個標記表示該 Cookie 只可以在安全環境下(HTTPS)傳輸,如果請求是 HTTP 協議,則不會攜帶該 Cookie。這樣就能解釋通為什麼登錄成功後重定向時不攜帶 Cookie 了。

新的問題來了,我使用的是 HTTP 協議登錄,為什麼 Cookie 中有 Secure 標記呢?回答這個問題,我們要完整的梳理一遍登錄過程。

首先我們使用 HTTPS 協議登錄,登錄成功後,返回的 Cookie 中含有 Secure 標記,接下來我們訪問 https://localhost:8444/http,該請求重定向到 http://localhost:8080/http,重定向的請求是 HTTP 請求,而 Cookie 只可以在 HTTPS 環境下傳輸,所以不會攜帶 Cookie,服務端以為這是一個匿名請求,所以要求重定向到登錄頁面,回到登錄頁面繼續登錄,此時發起的登錄是 HTTP 請求,即端口是 8080,由於 Cookie 並不會區分端口號,所以使用 8080 登錄成功後,使用的還是之前 8444 生成的 Cookie,但是 8080 又無法在發送請求時,自動攜帶該 Cookie,所以看到的就是總是登錄失敗,當清除瀏覽器緩存後,8444 的 Cookie 就被清除了,8080 再次登錄就可以生成自己的沒有 Secure 標記的 Cookie,此時一切又恢復正常了。

這裡邊其實主要涉及到兩個知識點:

  1. 含有 Secure 標記的 Cookie 只可以在安全環境下(HTTPS)傳輸。
  2. Cookie 是不區分端口號的,如果 Cookie 名相同,會自動覆蓋,並且讀取的是相同的數據。所以 8080 和 8444 並不會自動使用兩個 Cookie。

至此,總算搞清楚這個詭異的登錄問題了。那麼接下來的解決方案就很容易了。

還是那句話,所有看似無規律的 BUG 都是有規律的,找到規律才有解決問題的可能性!


分享到:


相關文章: