当前位置: 首页 > news >正文

工作踩坑系列——https访问遇到“已阻止载入混合活动内容”

前言

最近在主导公司网站进行全站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 就显得有点多余了。我们可以利用servletfilter机制来自动包装。

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上游服务中运行的后端代码,我们通过对代码的改造达到我们的目的。现在我们转换一下思路,将关注点放在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

转载于:https://www.cnblogs.com/xiaoshen666/articles/11258522.html

相关文章:

  • 浅析温州两家三甲医院上百名医生“回扣门”
  • Python - 模块
  • 【评论】Lisp天才神话
  • 近期橡胶抓大放小的过程与思路 (2018-08-22 18:32:59)
  • 云计算与虚拟化之后:网络威胁成新挑战
  • Redux学习总结
  • js如何隐藏表格的行与列
  • spring整合LOG4J2日志
  • redis 详细讲解
  • 安装SQL-SERVER提示重启计算机
  • C++学习笔记-2-数组
  • 强化VB.NET编程多线程句柄技巧(转载)
  • Ansible 自动化运维部署
  • IdHTTP处理HTTP 302遇到的问题
  • 如何进行数据库,比如ORACLE,SQL SERVER的逆向工程,将数据库导入到PowerDesigner中...
  • 2018天猫双11|这就是阿里云!不止有新技术,更有温暖的社会力量
  • IP路由与转发
  • nodejs:开发并发布一个nodejs包
  • Service Worker
  • 初识MongoDB分片
  • 讲清楚之javascript作用域
  • 两列自适应布局方案整理
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • ​软考-高级-系统架构设计师教程(清华第2版)【第15章 面向服务架构设计理论与实践(P527~554)-思维导图】​
  • #laravel 通过手动安装依赖PHPExcel#
  • #Z0458. 树的中心2
  • $.type 怎么精确判断对象类型的 --(源码学习2)
  • (2022 CVPR) Unbiased Teacher v2
  • (arch)linux 转换文件编码格式
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (windows2012共享文件夹和防火墙设置
  • (二)windows配置JDK环境
  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • (附源码)计算机毕业设计SSM智慧停车系统
  • (图)IntelliTrace Tools 跟踪云端程序
  • (原創) 如何優化ThinkPad X61開機速度? (NB) (ThinkPad) (X61) (OS) (Windows)
  • (转)Android学习笔记 --- android任务栈和启动模式
  • (转)ORM
  • ***linux下安装xampp,XAMPP目录结构(阿里云安装xampp)
  • .java 9 找不到符号_java找不到符号
  • .Net CoreRabbitMQ消息存储可靠机制
  • .NET 设计模式—简单工厂(Simple Factory Pattern)
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • .Net转前端开发-启航篇,如何定制博客园主题
  • .vollhavhelp-V-XXXXXXXX勒索病毒的最新威胁:如何恢复您的数据?
  • [2013AAA]On a fractional nonlinear hyperbolic equation arising from relative theory
  • [Android学习笔记]ScrollView的使用
  • [Angularjs]asp.net mvc+angularjs+web api单页应用
  • [Asp.net mvc]国际化
  • [boost]使用boost::function和boost::bind产生的down机一例
  • [ccc3.0][数字钥匙] UWB配置和使用(二)
  • [codeforces]Checkpoints
  • [Firefly-Linux] RK3568修改控制台DEBUG为普通串口UART
  • [Hive] 常见函数
  • [HNOI2010]BUS 公交线路