摘要:問題復現經過一段時間的調研工作,終于將公司的環境改造成支持訪問模式,信心滿滿的打開公司測試環境主頁,。當一個網頁出現這種情況時,它被稱為混合內容頁面。請求,默認行為。
前言
最近在主導公司網站進行全站Https改造工作,本文記錄在改造過程中遇到的一個由于后端302跳轉導致前端瀏覽器阻止訪問的問題,感覺這樣的問題有一定通用性,所以編輯成文,希望能給遇到類似問題的人們有所幫助。
問題復現經過一段時間的調研工作,終于將公司的環境改造成支持https訪問模式,信心滿滿的打開公司測試環境主頁,https://test.xxx.com。一切正常,就在我以為改造工作就要完成的時候,問題就出現了。
進入主頁正常,輸入用戶名和密碼登錄,頁面就不動了。調出Firefox的控制臺查看,發現這么一行報錯。
(圖一)
打開網絡面板查看得到如下內容
(圖二)
前端發起了一個https的Ajax請求,后端返回狀態碼為302,location為http://開頭網址,這樣就造成了混合訪問。本應該有Ajax自動處理的302跳轉就這樣被瀏覽器禁止了。
問題分析 1. 什么是混合內容當用戶訪問使用HTTPS的頁面時,他們與web服務器之間的連接是使用SSL加密的,從而保護連接不受嗅探器和中間人攻擊。
如果HTTPS頁面包括由普通明文HTTP連接加密的內容,那么連接只是被部分加密:非加密的內容可以被嗅探者入侵,并且可以被中間人攻擊者修改,因此連接不再受到保護。當一個網頁出現這種情況時,它被稱為混合內容頁面。
詳情可見https://developer.mozilla.org...
2. 為什么經過后端跳轉后Location由https變為了http。我們后端采用Java開發,部署與Tomcat,對于Servlet來說一般采用HttpServletResponse.sendRedirect(String url) 方法實現頁面跳轉(302跳轉)。那么問題是不是出在這個方法呢?答案是否定的。
sendRedirect(String url)方法中url參數可以傳入絕對地址和相對地址。我們使用的時候一般傳入相對地址,這樣由方法內部自動轉換為絕對地址也就是返回給瀏覽器中Location參數中的地址,sendRedirect()方法內部會根據當前訪問的scheme來決定拼接后絕對地址的scheme,也就是說如果訪問地址是https開頭那么跳轉鏈接的絕對地址也會是https的,http同理。在本次實例中我們傳入的就是相對地址,跳轉鏈接的絕對路徑地址開頭是由請求地址決定的,也就是后端程序收到的HttpServletRequest請求協議一定是http開頭的。
我們看到(圖二)中地址請求地址是由https開頭的,為什么到了后端程序后就成為了http請求呢?我們接著往下說。
(圖三)
為了方便說明我畫了一張https配置的架構圖,我們使用Nginx作為反向代理服務器,上游服務器使用Tomcat,我們在Nginx層進行Https配置,由Nginx負責處理Https請求。但是Nginx自身處理方式規定向上游服務器發送請求的時候是以http的方式請求的。這也就說明了為什么我們后端代碼收到的請求是http協議,真想終于大白了。
解決方法問題終于明了了,接下來就是解決的時候。
1.解決方案1.0既然經過Nginx代理后Tomcat服務器運行的代碼都變成了http請求,然后sendRedirect方法傳入相對地址就會隨著請求地址也變成http。那么我們不再使用相對地址而使用絕對地址。這樣跳轉地址就全部由我們做主,想跳轉到哪里就跳轉的哪里,媽媽再也不用擔心我們跳轉了。
先期改造:
/** * 重新實現sendRedirect。 * @param request * @param response * @param url * @throws IOException */ public static void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException{ if(url.startsWith("http://")||url.startsWith("https://")){ //絕對路徑,直接跳轉。 response.sendRedirect(url); return; } // 收集請求信息,為拼接絕對地址做準備。 String serverName = request.getServerName(); int port = request.getServerPort(); String contextPath = request.getContextPath(); String servletPath = request.getServletPath(); String queryString = request.getQueryString(); // 拼接絕對地址 StringBuilder absoluteUrl = new StringBuilder(); // 強制使用https absoluteUrl.append("https").append("://").append(serverName); //80和443位http和https默認接口,無需拼接。 if (port != 80 && port != 443) { absoluteUrl.append(":").append(port); } if (contextPath != null) { absoluteUrl.append(contextPath); } if (servletPath != null) { absoluteUrl.append(servletPath); } // 將相對地址加入。 absoluteUrl.append(url); if (queryString != null) { absoluteUrl.append(queryString); } // 跳轉到絕對地址。 response.sendRedirect(absoluteUrl.toString()); }
我們自己了一個sendRedirect()方法,但是還有一點小小的瑕疵,我們將所有相對地址都轉化成http開頭的絕對地址,對于那些我們即支持https由支持http的網站來說,這樣就不適合了,所以我們需要和前端請求做一個預定,讓前端再發類似于Ajax訪問的時候,自定義一個request的header,告訴我們是https訪問還是http訪問,我們在后端代碼中判斷這個自定義header,決定代碼行為。
/** * 重新實現sendRedirect。 * @param request * @param response * @param url * @throws IOException */ public static void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException{ if(url.startsWith("http://")||url.startsWith("https://")){ //絕對路徑,直接跳轉。 response.sendRedirect(url); return; } //假設前端請求頭為http_https_scheme,可以傳入的值有http或https,不傳默認為https。 if(("http").equals(request.getHeader("http_https_scheme"))){ //http請求,默認行為。 response.sendRedirect(url); return; } // 收集請求信息,為拼接絕對地址做準備。 String serverName = request.getServerName(); int port = request.getServerPort(); String contextPath = request.getContextPath(); String servletPath = request.getServletPath(); String queryString = request.getQueryString(); // 拼接絕對地址 StringBuilder absoluteUrl = new StringBuilder(); // 強制使用https absoluteUrl.append("https").append("://").append(serverName); //80和443位http和https默認接口,無需拼接。 if (port != 80 && port != 443) { absoluteUrl.append(":").append(port); } if (contextPath != null) { absoluteUrl.append(contextPath); } if (servletPath != null) { absoluteUrl.append(servletPath); } // 將相對地址加入。 absoluteUrl.append(url); if (queryString != null) { absoluteUrl.append(queryString); } // 跳轉到絕對地址。 response.sendRedirect(absoluteUrl.toString()); }
以上為改造之后的代碼,增加了請求頭判斷邏輯。這樣我們的方法就支持http和https混合模式了。
更進一步:
讓我們對上面的代碼更進一步,其實我們就是對sendRedirect的邏輯重新編排,只不過我們使用的靜態方法的模式,可不可以直接重寫response中的sendRedirect()方法?
/** * 重寫sendRedirect方法。 * */ public class HttpsServletResponseWrapper extends HttpServletResponseWrapper { private final HttpServletRequest request; public HttpsServletResponseWrapper(HttpServletRequest request,HttpServletResponse response) { super(response); this.request=request; } @Override public void sendRedirect(String location) throws IOException { if(location.startsWith("http://")||location.startsWith("https://")){ //絕對路徑,直接跳轉。 super.sendRedirect(location); return; } //假設前端請求頭為http_https_scheme,可以傳入的值有http或https,不傳默認為https。 if(("http").equals(request.getHeader("http_https_scheme"))){ //http請求,默認行為。 super.sendRedirect(location); return; } // 收集請求信息,為拼接絕對地址做準備。 String serverName = request.getServerName(); int port = request.getServerPort(); String contextPath = request.getContextPath(); String servletPath = request.getServletPath(); String queryString = request.getQueryString(); // 拼接絕對地址 StringBuilder absoluteUrl = new StringBuilder(); // 強制使用https absoluteUrl.append("https").append("://").append(serverName); //80和443位http和https默認接口,無需拼接。 if (port != 80 && port != 443) { absoluteUrl.append(":").append(port); } if (contextPath != null) { absoluteUrl.append(contextPath); } if (servletPath != null) { absoluteUrl.append(servletPath); } // 將相對地址加入。 absoluteUrl.append(location); if (queryString != null) { absoluteUrl.append(queryString); } // 跳轉到絕對地址。 super.sendRedirect(absoluteUrl.toString()); } }
具體邏輯一樣,我們只是繼承了HttpServletResponseWrapper 這個包裝類,在這里使用了一個觀察者模式重新編寫了sendRedirect()方法邏輯。
我們可以這樣使用我們自定義等HttpsServletResponseWrapper
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String location="/login"; new HttpsServletResponseWrapper(request, response).sendRedirect(location); }
再進一步:
既然我們有了新的HttpServletResponseWrapper ,我們在需要的地方手動包裝HttpServletResponse 就顯得有點多余了。我們可以利用servlet的filter機制來自動包裝。
public class HttpsServletResponseWrapperFilter implements Filter{ @Override public void destroy() { // TODO Auto-generated method stub } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(request, new HttpsServletResponseWrapper((HttpServletRequest)request, (HttpServletResponse)response)); } @Override public void init(FilterConfig arg0) throws ServletException { // TODO Auto-generated method stub } }
在web.xml中設置filter映射,可以直接使用HttpServletResponse 對象,無需包裝,因為在請求經過HttpsServletResponseWrapperFilter 的時候response已經被包裝為HttpsServletResponseWrapper。
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String location="/login"; response.sendRedirect(location); }
至此,我們已經代碼邏輯無縫的嵌入到我們的后端代碼中,看上去更優雅了。
2.解決方案2.0在1.0版本中我們的關注點都是Nginx上游服務中運行的后端代碼,我們通過對代碼的改造達到我們的目的?,F在我們轉換一下思路,將關注點放在Nginx上,既然是Nginx代理之后,我們的scheme丟失,那么Nginx有沒有給我們提供一種機制保留代理之后的scheme呢,答案是肯定的。
location / { proxy_set_header X-Forwarded-Proto $scheme; }
一行簡單的配置,就解決了我們的問題,Nginx在代理的時候保留了scheme,這樣我們在跳轉的時候可以直接使用HttpServletResponse.sendRedirect()方法。
小結通過解決方案1.0的修改代碼方式和2.0的修改配置方式,我們都解決了問題。在日常開發中解決問題的方式很多,只要你了解產生問題的原理,在產生問題的任意環節都可以尋求解決方案。這篇工作記錄就寫到這里,當然這個問題還有其他的解決方式,如果你有其他的解決方案可以留言告訴我。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/40028.html
摘要:最近在幫人解決下的一些兼容問題。驗證不通過的話,輸入框會加上紅色的邊框。然后妹紙在描述中說的是瀏覽器中,修改密碼頁面,輸入框中不輸入任何字符,輸入框顏色也是紅的我還以為又是哪里的寫得不對呢。最后發現,輸入框好像都帶了個屬性。 因為工作一年多以來,做的工作基本都是和webkit系列打交道。 先是做m站,后來做了兩個app內嵌的hybrid項目,從來只考慮webkit前綴和相關的偽類。 最...
摘要:最近在幫人解決下的一些兼容問題。驗證不通過的話,輸入框會加上紅色的邊框。然后妹紙在描述中說的是瀏覽器中,修改密碼頁面,輸入框中不輸入任何字符,輸入框顏色也是紅的我還以為又是哪里的寫得不對呢。最后發現,輸入框好像都帶了個屬性。 因為工作一年多以來,做的工作基本都是和webkit系列打交道。 先是做m站,后來做了兩個app內嵌的hybrid項目,從來只考慮webkit前綴和相關的偽類。 最...
摘要:的方法在安卓底下會崩潰,結果竟然是要在里面設置正確的,因為我是用官網的腳手架搭起來的項目,不知道大家會不會遇到,改一下就可以解決問題了。 目錄 Weex系列(序) —— 總要知道原生的一點東東(iOS) Weex系列(序) —— 總要知道原生的一點東東(Android) Weex系列(1) —— Hello World項目 Weex系列(2) —— 頁面跳轉和通信 Weex系列(3)...
摘要:的方法在安卓底下會崩潰,結果竟然是要在里面設置正確的,因為我是用官網的腳手架搭起來的項目,不知道大家會不會遇到,改一下就可以解決問題了。 目錄 Weex系列(序) —— 總要知道原生的一點東東(iOS) Weex系列(序) —— 總要知道原生的一點東東(Android) Weex系列(1) —— Hello World項目 Weex系列(2) —— 頁面跳轉和通信 Weex系列(3)...
閱讀 825·2023-04-26 00:13
閱讀 2794·2021-11-23 10:08
閱讀 2432·2021-09-01 10:41
閱讀 2112·2021-08-27 16:25
閱讀 4177·2021-07-30 15:14
閱讀 2359·2019-08-30 15:54
閱讀 857·2019-08-29 16:22
閱讀 2736·2019-08-26 12:13