博客 / 詳情

返回

一個HTTPS轉HTTP的Bug,他們竟然忍了2年?原諒我無法接受,加班改了!

今天這篇文章給大家講一個追查Bug的故事和過程。個人一直認為:事出反常必有妖,程序中的Bug也是如此

希望通過這個Bug的排查故事,大家不僅能夠學到一系列的知識點,同時也能學會如何解決問題,如何更加專業的做事。而解決問題的方式及思維比單純的技術更加重要。

Let's go!

故事的起因

剛接手新團隊新項目沒多久,在發佈一個系統時,同事友善的提醒:發佈xx系統時,在測試環境要註釋掉一行代碼,上線發佈時再放開註釋。

聽此友善提醒,一驚:這又是什麼黑科技啊?!在我的經驗裏,還沒有什麼系統需要這樣處理,暗下決心要排查此問題。

終於抽出時間,週五折騰了多半天,沒解決掉,週末還心裏惦記着,於是加班也搞定這個問題。

Bug的存在及操作

項目是基於JSP的,沒有做前後端分離。在JSP頁面中引入了一個公共的head.jsp,該文件內有這樣一行代碼和註釋:

<!-- 解決線上HTTPS瀏覽器轉圈的問題,測試環境要註釋掉下面的一句話 -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />

同事友善提醒的就是註釋上的操作,測試環境註釋掉(不然無法訪問),生產環境需要放開,不然也無法訪問(轉圈圈啊)。據註釋説明,大概知道是用來解決HTTPS相關的問題。

那麼,是什麼原因導致了要這樣操作?有沒有更簡單的操作?大家只是在這麼做,沒人尋找問題的根源,也沒人能出答案,只能自己去尋找了。

HTTPS中的HTTP請求

先來看看配置META元素是幹什麼用的。

其中http-equiv指定的“Content-Security-Policy”就"網頁安全政策",縮寫CSP,常用來防止XSS攻擊。

通常的使用方法就是在HTML中通過meta標籤來進行定義:

<meta http-equiv="content-security-policy" content="策略">
<meta http-equiv="content-security-policy-report-only" content="策略">

其中,在content中可以指定涉及安全的各類限制策略。

項目中使用的upgrade-insecure-requests便是限制策略之一,作用是:自動將網頁上所有加載外部資源的HTTP鏈接換成HTTPS協議

此刻稍微明白了一點,原來最初寫這行代碼是想將HTTP請求強制轉換成HTTPS請求啊。

但正常情況來説,只要在Nginx或SLB中配置了HTTP轉HTTPS便不會出現這類問題,而系統中是有對應的配置的。

於是,在線上另起一個服務實驗了一下,註釋掉這段代碼,部分功能還真的在轉圈圈,誠不欺我!

為什麼HTTPS中不允許HTTP請求

查看瀏覽器中的請求,發現轉圈圈原來是如下錯誤引起的:

Mixed Content: The page at 'https://example.com' was loaded over HTTPS, but requested an insecure stylesheet 'http://example.com/xxx'. This request has been blocked; the content must be served over HTTPS.

其中,Mixed Content即混合內容。所謂的混合內容通常出現在以下情況:初始的HTML的內容是通過HTTPS加載的,但其他資源(比如,css樣式、js、圖片等)則通過不安全的HTTP請求加載。此時,同一個頁面,同時使用了HTTP和HTTPS的內容,而HTTP協議會降低整個頁面的安全性。

因此,現代瀏覽器會針對HTTPS中的HTTP請求進行警告,阻斷請求,並拋出上述異常信息。

現在,問題的原因基本明確了:HTTPS請求中出現了HTTP請求。

那麼,解決方案有幾種:

  • 方案一:在HTML中添加meta標籤,強制將HTTP請求轉換成HTTPS請求。這也是上面的使用方式,但這種方式的弊端也很明顯,在沒有使用HTTPS的測試環境,需要手動的註釋掉。否則,也無法正常訪問。
  • 方案二:通過Nginx或SLB的配置,將HTTP請求轉換成HTTPS請求。
  • 方案三:最笨的方法,找到項目中存在HTTP請求的問題,逐個修復。

初步改造,略顯成效

目前使用的第一種方案很顯然不符合要求,而第二種方案已經配置了,但部分頁面依舊不起效。那麼,還有其他方案嗎?

經過大量排查,發現導致不起效的原因是:項目中大量使用了redirect方式的跳轉。

@RequestMapping(value = "delete")
public String delete(RedirectAttributes redirectAttributes) {
        //.. do something
        addMessage(redirectAttributes, "刪除xxx成功");
        return "redirect:" + Global.getAdminPath() + "/list";
}

redirect方式的跳轉在HTTPS的環境下會重定向到HTTP協議,導致無法訪問。

這也太坑了,難怪上面HTTP轉HTTPS的設置都配置完成了,部分頁面還不起效。

而導致這個問題的根本原因是Spring的ViewResolver對HTTP 1.0協議的兼容。

針對此問題,將其關閉即可解決,具體改造方案有兩個。

方案一,將redirect改為RedirectView類來實現:

modelAndView.setView(new RedirectView(Global.getAdminPath() + "/list", true, false));

其中RedirectView的最後一個參數設置為false,就是將http10Compatible的開關關閉,不對HTTP 1.0協議進行兼容。

方案二:配置Spring的ViewResolver的redirectHttp10Compatible屬性。通過這種方案,可以實現全局關閉。

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/" />
  <property name="suffix" value=".jsp" />
  <property name="redirectHttp10Compatible" value="false" />
</bean>

由於項目中使用redirect較多,於是就採用了第二種方案。修改之後,發現大部分問題都解決了。

為了防止遺漏,就多點了一些頁面,竟然還有漏網之魚!

Shiro攔截器又作祟

解決了重定向導致的問題,以為萬事大吉了,結果涉及到Shiro重定向的頁面又出現了類似的問題。原因很簡單:某些頁面的權限驗證需要經過Shiro,但Shiro將HTTPS請求攔截之後,重定向時轉換成了HTTP請求。

那麼,為什麼視圖層將redirectHttp10Compatible設置為false不起效呢?

追蹤了Shiro攔截器中的代碼,發現Shiro在攔截器中默認將redirectHttp10Compatible設置為true,又是一坑~

查看源碼可以發現,Shiro的登錄過濾器FormAuthenticationFilter的方法中調用了saveRequestAndRedirectToLogin方法:

protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
    saveRequest(request);
    redirectToLogin(request, response);
}

// 進而調用redirectToLogin方法
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
   String loginUrl = getLoginUrl();
   WebUtils.issueRedirect(request, response, loginUrl);
}

// 通過WebUtils.issueRedirect進行設置
public static void issueRedirect(ServletRequest request, ServletResponse response, String url) throws IOException {
    issueRedirect(request, response, url, (Map)null, true, true);
}

// 通過WebUtils.issueRedirect重載方法
public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException {
    RedirectView view = new RedirectView(url, contextRelative, http10Compatible);
    view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response));
}

通過上述代碼追蹤,可以看到,最終在WebUtils的issueRedirect方法中調用了兩次issueRedirect,而http10Compatible參數值默認為true。

找到問題的根源,解決起來就簡單了,重寫FormAuthenticationFilter攔截器:

public class CustomFormAuthenticationFilter extends FormAuthenticationFilter {
 
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                return executeLogin(request, response);
            } else {
                return true;
            }
        } else {
            saveRequestAndRedirectToLogin(request, response);
            return false;
        }
    }
    
    protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        saveRequest(request);
        redirectToLogin(request, response);
    }
    
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        String loginUrl = getLoginUrl();
        WebUtils.issueRedirect(request, response, loginUrl, null, true, false);
    }
}

示例中,將onAccessDenied中需要原本調用WebUtils.issueRedirect方法的http10Compatible參數改為false即可。

上面只是示例,實際上不僅包括成功頁面,還包括失敗頁面等,都需要重新實現一下對應的方法。最後,在shiroFilter中配置自定義的攔截器。

    <!-- 自定義的登錄過濾器-->
    <bean id="customFilter" class="com.senzhuang.shiro.CustomFormAuthenticationFilter" />
 
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/login.html"></property>
        <property name="unauthorizedUrl" value="/refuse.html"></property>
        <property name="filters">
            <map>
                <entry key="authc" value-ref="customFilter"/>
            </map>
        </property>
    </bean>

經過上述的改造,關於HTTPS中的HTTP請求問題已經得到解決了。

為了防止遺漏,又挨個點了一些頁面,又發了問題了!哎,咋那麼手欠呢……

LayUI的坑

本來以為解決了上面的問題,就徹底解決了,可以吃頓燒烤慶祝一下了。結果,在前端頁面中又發現了類似的錯誤。但此時錯誤信息來自訪問登錄頁面的路徑:

http://example.com/a/login

奇了怪了,已經登錄成功了,為什麼業務操作頁面還會再請求login頁面呢?而且跳轉過去還是HTTP請求,而不是HTTPS的請求。

查看了一下login的請求結果:

303錯誤

排查了相關的業務代碼,登錄完成之後,再也沒有請求登錄請求了啊,為什麼會再次請求一次login呢?難道是訪問某些資源受限,導致重定向到登錄頁面了?

於是,查看了一下HTML調用的”Initiator“:

Initiator

原來是LayUI請求對應的layer.css資源時,觸發了login的登錄操作。

首先想到的是Shiro中沒有放開靜態資源的攔截,於是在Shiro中放開了layui的攔截權限,但問題已經存在。

再次排查,發現頁面中沒有主動引入layer.css文件,於是主動引入了layer.css文件,但問題還是存在。

沒辦法,只好查看layui.js,看看為什麼要發起這個請求。此時,還留意到請求路徑中有一個"undefinedcss"的詞。

用過js的朋友都知道,undefined是js中變量未初始化的默認值,類似Java中的null。

在layui.js中搜索”css/“,還真找到這樣一段代碼:

return layui.link(o.dir + "css/" + e, t, n)

對照起來,也就是説o.dir的值為"undefined",與後面的css連接起來就變成了"undefinedcss",而這個路徑並不存在,也沒在Shiro中進行權限配置,默認會走到登錄界面去。而這裏是內部的一個異步的redirect請求,不會在頁面呈現,要查看瀏覽器的錯誤信息才能發現。

找到問題原因了,改造起來就簡單了,將layui的link方法參數進行修改:

// 註釋掉
// return layui.link(o.dir + "css/" + e, t, n)

// 改為
return layui.link((o.dir ? o.dir:"/static/sc_layui/") +"css/"+e, t, n)

改造的基本思路是:如果o.dir有值(js中有值即為true)則使用o.dir的值;如果o.dir為undefined則採用指定的默認值。

其中"/static/sc_layui/"為項目中存放layui組件的路徑。由於layui.js可能是壓縮後的js,可通過搜索”css/“或”layui.link“找到對應的代碼。

重啓項目,清除瀏覽器緩存,再次訪問頁面,問題得到徹底解決。

可以安心吃烤串了

週末又花了半天時間,終於把這個問題徹底解決了,現在可以安心去吃頓烤串慶祝一下了。

最後,回顧一下這個過程,看看你能從中收穫到什麼:

  • 出現問題:不同環境(HTTP和HTTPS)需要手動改代碼;
  • 尋找問題:為了安全,HTTPS內不允許發起HTTP請求;
  • 解決問題:兩種方式關閉http10Compatible
  • Shiro問題:Shiro中默認為關閉http10Compatible,重寫Filter,實現關閉操作;
  • LayUI Bug修復:LayUI代碼bug,導致發起http(登錄)請求。修復此Bug;

在這個過程中,如果你只是安於現狀,”遵守規則“,每次上線時修改一下文件,不僅費時費力,而且不知為什麼要這麼做。

但如果像筆者一樣,刨根問底的追蹤一下,你將會學到一系列的知識:

  • HTTP請求的CSP,upgrade-insecure-requests配置;
  • HTTPS中為什麼不能發起HTTP請求;
  • Spring視圖解析器中配置http10Compatible
  • redirect方式視圖返回的弊端;
  • Nginx中如何將HTTP請求轉為HTTPS請求;
  • HTTP請求的混合內容(Mixed Content)概念及錯誤;
  • HTTP 1.0、HTTP 1.1、HTTP2.0協議的區別;
  • Shiro攔截器自定義Filter;
  • Shiro攔截器過濾指定URL訪問;
  • Shiro攔截器的配置及部分源碼實現;
  • LayUI的一個bug;
  • 其他排查該問題時用到或學到的技術;

這些技術你學到了嗎?解決問題的思路和方式方法你學到了嗎?如果本文有那麼一點內容啓發到你了,我不吝分享,你也不要吝嗇,點個贊吧。

博主簡介:《SpringBoot技術內幕》技術圖書作者,酷愛鑽研技術,寫技術乾貨文章。

公眾號:「程序新視界」,博主的公眾號,歡迎關注~

技術交流:請聯繫博主微信號:zhuan2quan

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.