从 Firefox 18 开始,如果 HTTPS 页面中包含非加密的 HTTP 内容,浏览器会在控制台输出警告,记录 Mixed Active Content 请求。而从 Firefox 23 开始,浏览器会默认阻止 HTTPS 页面中可能影响网页安全的 HTTP 请求(即阻止 Mixed Active Content)。这样做会牺牲一些网站的兼容性,但对安全性的提高是很有帮助的。
获取 Mixed Content 相当于发起部分加密的连接,其中未加密的部分存在被中间人攻击的可能。不同类型的 Mixed Content 所产生的危害程度也有所不同,Mixed Passive Content 可能会使中间人获取到用户的设备信息,或让用户看到不正确的图片、音频等信息。而 Mixed Active Content 则可能导致用户的敏感数据被窃取,比如账号密码等。
为什么 Mixed Content Blocker 不是阻止所有的 HTTP 请求?
Mixed Content 可以分为两类:
Mixed Passive Content
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跳转就这样被浏览器禁止了。
问题分析
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。那么我们不再使用相对地址而使用绝对地址。这样跳转地址就全部由我们做主,想跳转到哪里就跳转的哪里,妈妈再也不用担心我们跳转了。
先期改造:
代码语言:javascript复制 /**
* 重新实现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());
}
代码语言:javascript复制
我们自己了一个sendRedirect()方法,但是还有一点小小的瑕疵,我们将所有相对地址都转化成http开头的绝对地址,对于那些我们即支持https由支持http的网站来说,这样就不适合了,所以我们需要和前端请求做一个预定,让前端再发类似于Ajax访问的时候,自定义一个request的header,告诉我们是https访问还是http访问,我们在后端代码中判断这个自定义header,决定代码行为。
代码语言:javascript复制/**
* 重新实现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());
}
代码语言:javascript复制
以上为改造之后的代码,增加了请求头判断逻辑。这样我们的方法就支持http和https混合模式了。
更进一步: 让我们对上面的代码更进一步,其实我们就是对sendRedirect的逻辑重新编排,只不过我们使用的静态方法的模式,可不可以直接重写response中的sendRedirect()方法?
代码语言:javascript复制/** * 重写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
}
}
代码语言:javascript复制
在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上游服务中运行的后端代码,我们通过对代码的改造达到我们的目的。现在我们转换一下思路,将关注点放在Nginx上,既然是Nginx代理之后,我们的scheme丢失,那么Nginx有没有给我们提供一种机制保留代理之后的scheme呢,答案是肯定的。
代码语言:javascript复制location / {
proxy_set_header X-Forwarded-Proto $scheme;
}
一行简单的配置,就解决了我们的问题,Nginx在代理的时候保留了scheme,这样我们在跳转的时候可以直接使用HttpServletResponse.sendRedirect()
方法。
小结
通过解决方案1.0的修改代码方式和2.0的修改配置方式,我们都解决了问题。在日常开发中解决问题的方式很多,只要你了解产生问题的原理,在产生问题的任意环节都可以寻求解决方案。这篇工作记录就写到这里,当然这个问题还有其他的解决方式,如果你有其他的解决方案可以留言告诉我。