从 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