從 Firefox 18 開始,若是 HTTPS 頁面中包含非加密的 HTTP 內容,瀏覽器會在控制檯輸出警告,記錄 Mixed Active Content 請求。而從 Firefox 23 開始,瀏覽器會默認阻止 HTTPS 頁面中可能影響網頁安全的 HTTP 請求(即阻止 Mixed Active Content)。這樣作會犧牲一些網站的兼容性,但對安全性的提升是頗有幫助的。前端
獲取 Mixed Content 至關於發起部分加密的鏈接,其中未加密的部分存在被中間人攻擊的可能。不一樣類型的 Mixed Content 所產生的危害程度也有所不一樣,Mixed Passive Content 可能會使中間人獲取到用戶的設備信息,或讓用戶看到不正確的圖片、音頻等信息。而 Mixed Active Content 則可能致使用戶的敏感數據被竊取,好比帳號密碼等。java
Mixed Content 能夠分爲兩類:web
Mixed Passive Contentsegmentfault
Mixed Active Content後端
Mixed Passive Content (a.k.a. Mixed Display Content)瀏覽器
Mixed Passive Content 是在 HTTPS 頁面中一些對安全性影響不大的 HTTP 內容,好比 Image、Audio、Video 等。即便這些內容被中間人篡改,所產生的影響也只是 —— 中間人得知了用戶的瀏覽器信息 (through user-agent included by HTTP headers)、用戶看到了一張不正確的圖片,這些被篡改的內容沒法修改 DOM 樹,也沒法執行。另外,Mixed Passive Content 在 Web 上廣泛存在。所以 Firefox 默認不會阻止 Mixed Passive Content。安全
Mixed Active Content (a.k.a. Mixed Script Content)服務器
Mixed Active Content 是在 HTTPS 頁面中一些可以修改 DOM 樹的 HTTP 內容,如 JavaScript、CSS、XMLHttpRequest、iFrame 等。這些 HTTP 內容被中間人修改之後,可能會影響原有 HTTPS 內容的安全性,致使敏感的用戶數據被盜。所以 Firefox 會默認阻止 Mixed Active Content。網絡
爲何 Frame 應該是 Mixed Active Content?架構
Frame 之因此不能被分類爲 Mixed Passive Content 主要有如下幾個緣由:
一個 frame 能夠將外層可靠的 HTTPS 頁面跳轉到惡意盜取信息的仿造頁面。
若是一個 HTTPS 頁面嵌套着 HTTP frame,而這個 frame 包含表單用以輸入用戶信息,那麼用戶信息將會以 HTTP 方式傳送,有被中間攻擊者竊取的危險,而用戶卻絕不知情,還覺得一切都在安全的 HTTPS 裏。
如何斷定 Mixed Content 是 Active 仍是 Passive?
該 Mixed Content 是否會影響頁面的 DOM 結構。(Yes -> Active, No -> Passive)
若是頁面中包含了如 JavaScript、CSS、XMLHttpRequest、iFrame 等這些 HTTP 內容。
使用相對連接
修改http連接爲https(須要鏈接支持https)
讓瀏覽器自動判斷http訪問仍是https訪問 好比 <img src="//www.sslzhengshu.com/statics/images/logo1.jpg" />
方案二:
最近在主導公司網站進行全站Https改造工做,本文記錄在改造過程當中遇到的一個因爲後端302跳轉致使前端瀏覽器阻止訪問的問題,感受這樣的問題有必定通用性,因此編輯成文,但願能給遇到相似問題的人們有所幫助。
通過一段時間的調研工做,終於將公司的環境改形成支持https訪問模式,信心滿滿的打開公司測試環境主頁,https://test.xxx.com。一切正常,就在我覺得改造工做就要完成的時候,問題就出現了。
進入主頁正常,輸入用戶名和密碼登陸,頁面就不動了。調出Firefox的控制檯查看,發現這麼一行報錯。
(圖一)
打開網絡面板查看獲得以下內容
(圖二)
前端發起了一個https的Ajax請求,後端返回狀態碼爲302,location爲http://開頭網址,這樣就形成了混合訪問。本應該有Ajax自動處理的302跳轉就這樣被瀏覽器禁止了。
當用戶訪問使用HTTPS的頁面時,他們與web服務器之間的鏈接是使用SSL加密的,從而保護鏈接不受嗅探器和中間人攻擊。
若是HTTPS頁面包括由普通明文HTTP鏈接加密的內容,那麼鏈接只是被部分加密:非加密的內容能夠被嗅探者入侵,而且能夠被中間人攻擊者修改,所以鏈接再也不受到保護。當一個網頁出現這種狀況時,它被稱爲混合內容頁面。
詳情可見https://developer.mozilla.org...
咱們後端採用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協議,真想終於大白了。
問題終於明瞭了,接下來就是解決的時候。
既然通過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); }
至此,咱們已經代碼邏輯無縫的嵌入到咱們的後端代碼中,看上去更優雅了。
在1.0版本中咱們的關注點都是Nginx上游服務中運行的後端代碼,咱們經過對代碼的改造達到咱們的目的。如今咱們轉換一下思路,將關注點放在Nginx上,既然是Nginx代理以後,咱們的scheme丟失,那麼Nginx有沒有給咱們提供一種機制保留代理以後的scheme呢,答案是確定的。
location / { proxy_set_header X-Forwarded-Proto $scheme; }
一行簡單的配置,就解決了咱們的問題,Nginx在代理的時候保留了scheme,這樣咱們在跳轉的時候能夠直接使用HttpServletResponse.sendRedirect()
方法。
經過解決方案1.0的修改代碼方式和2.0的修改配置方式,咱們都解決了問題。在平常開發中解決問題的方式不少,只要你瞭解產生問題的原理,在產生問題的任意環節均可以尋求解決方案。這篇工做記錄就寫到這裏,固然這個問題還有其餘的解決方式,若是你有其餘的解決方案能夠留言告訴我。
轉載:https://segmentfault.com/a/1190000015722535