明天这篇文章给大家讲一个追究Bug的故事和过程。集体始终认为:事出反常必有妖,程序中的Bug也是如此

心愿通过这个Bug的排查故事,大家不仅可能学到一系列的知识点,同时也能学会如何解决问题,如何更加业余的做事。而解决问题的形式及思维比单纯的技术更加重要。

Let's go!

故事的起因

刚接手新团队新我的项目没多久,在公布一个零碎时,共事友善的揭示:公布xx零碎时,在测试环境要正文掉一行代码,上线公布时再放开正文。

听此友善揭示,一惊:这又是什么黑科技啊?!在我的教训里,还没有什么零碎须要这样解决,暗下决心要排查此问题。

终于抽出工夫,周五折腾了多半天,没解决掉,周末还心里惦记着,于是加班也搞定这个问题。

Bug的存在及操作

我的项目是基于JSP的,没有做前后端拆散。在JSP页面中引入了一个公共的head.jsp,该文件内有这样一行代码和正文:

<!-- 解决线上HTTPS浏览器转圈的问题,测试环境要正文掉上面的一句话 --><meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />

共事友善揭示的就是正文上的操作,测试环境正文掉(不然无法访问),生产环境须要放开,不然也无法访问(转圈圈啊)。据正文阐明,大略晓得是用来解决HTTPS相干的问题。

那么,是什么起因导致了要这样操作?有没有更简略的操作?大家只是在这么做,没人寻找问题的本源,也没人能出答案,只能本人去寻找了。

HTTPS中的HTTP申请

先来看看配置META元素是干什么用的。

其中http-equiv指定的“Content-Security-Policy”就"网页平安政策",缩写CSP,罕用来避免XSS攻打。

通常的应用办法就是在HTML中通过meta标签来进行定义:

<meta http-equiv="content-security-policy" content="策略"><meta http-equiv="content-security-policy-report-only" content="策略">

其中,在content中能够指定波及平安的各类限度策略。

我的项目中应用的upgrade-insecure-requests便是限度策略之一,作用是:主动将网页上所有加载内部资源的HTTP链接换成HTTPS协定

此刻略微明确了一点,原来最后写这行代码是想将HTTP申请强制转换成HTTPS申请啊。

但失常状况来说,只有在Nginx或SLB中配置了HTTP转HTTPS便不会呈现这类问题,而零碎中是有对应的配置的。

于是,在线上另起一个服务试验了一下,正文掉这段代码,局部性能还真的在转圈圈,诚不欺我!

为什么HTTPS中不容许HTTP申请

查看浏览器中的申请,发现转圈圈原来是如下谬误引起的:

Mixed Content: The page at 'https://example.com' was loaded over HTTPS, but requested an insecure stylesheet 'http://example.com/xxx'. This request has been blocked; the content must be served over HTTPS.

其中,Mixed Content即混合内容。所谓的混合内容通常呈现在以下状况:初始的HTML的内容是通过HTTPS加载的,但其余资源(比方,css款式、js、图片等)则通过不平安的HTTP申请加载。此时,同一个页面,同时应用了HTTP和HTTPS的内容,而HTTP协定会升高整个页面的安全性。

因而,古代浏览器会针对HTTPS中的HTTP申请进行正告,阻断申请,并抛出上述异样信息。

当初,问题的起因根本明确了:HTTPS申请中呈现了HTTP申请。

那么,解决方案有几种:

  • 计划一:在HTML中增加meta标签,强制将HTTP申请转换成HTTPS申请。这也是下面的应用形式,但这种形式的弊病也很显著,在没有应用HTTPS的测试环境,须要手动的正文掉。否则,也无奈失常拜访。
  • 计划二:通过Nginx或SLB的配置,将HTTP申请转换成HTTPS申请。
  • 计划三:最笨的办法,找到我的项目中存在HTTP申请的问题,一一修复。

初步革新,略显功效

目前应用的第一种计划很显然不符合要求,而第二种计划曾经配置了,但局部页面仍旧不起效。那么,还有其余计划吗?

通过大量排查,发现导致不起效的起因是:我的项目中大量应用了redirect形式的跳转。

@RequestMapping(value = "delete")public String delete(RedirectAttributes redirectAttributes) {        //.. do something        addMessage(redirectAttributes, "删除xxx胜利");        return "redirect:" + Global.getAdminPath() + "/list";}

redirect形式的跳转在HTTPS的环境下会重定向到HTTP协定,导致无法访问。

这也太坑了,难怪下面HTTP转HTTPS的设置都配置实现了,局部页面还不起效。

而导致这个问题的根本原因是Spring的ViewResolver对HTTP 1.0协定的兼容。

针对此问题,将其敞开即可解决,具体革新计划有两个。

计划一,将redirect改为RedirectView类来实现:

modelAndView.setView(new RedirectView(Global.getAdminPath() + "/list", true, false));

其中RedirectView的最初一个参数设置为false,就是将http10Compatible的开关敞开,不对HTTP 1.0协定进行兼容。

计划二:配置Spring的ViewResolver的redirectHttp10Compatible属性。通过这种计划,能够实现全局敞开。

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />  <property name="prefix" value="/" />  <property name="suffix" value=".jsp" />  <property name="redirectHttp10Compatible" value="false" /></bean>

因为我的项目中应用redirect较多,于是就采纳了第二种计划。批改之后,发现大部分问题都解决了。

为了避免脱漏,就多点了一些页面,居然还有漏网之鱼!

Shiro拦截器又作怪

解决了重定向导致的问题,认为高枕无忧了,后果波及到Shiro重定向的页面又呈现了相似的问题。起因很简略:某些页面的权限验证须要通过Shiro,但Shiro将HTTPS申请拦挡之后,重定向时转换成了HTTP申请。

那么,为什么视图层将redirectHttp10Compatible设置为false不起效呢?

追踪了Shiro拦截器中的代码,发现Shiro在拦截器中默认将redirectHttp10Compatible设置为true,又是一坑~

查看源码能够发现,Shiro的登录过滤器FormAuthenticationFilter的办法中调用了saveRequestAndRedirectToLogin办法:

protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {    saveRequest(request);    redirectToLogin(request, response);}// 进而调用redirectToLogin办法protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {   String loginUrl = getLoginUrl();   WebUtils.issueRedirect(request, response, loginUrl);}// 通过WebUtils.issueRedirect进行设置public static void issueRedirect(ServletRequest request, ServletResponse response, String url) throws IOException {    issueRedirect(request, response, url, (Map)null, true, true);}// 通过WebUtils.issueRedirect重载办法public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException {    RedirectView view = new RedirectView(url, contextRelative, http10Compatible);    view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response));}

通过上述代码追踪,能够看到,最终在WebUtils的issueRedirect办法中调用了两次issueRedirect,而http10Compatible参数值默认为true。

找到问题的本源,解决起来就简略了,重写FormAuthenticationFilter拦截器:

public class CustomFormAuthenticationFilter extends FormAuthenticationFilter {     @Override    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {        if (isLoginRequest(request, response)) {            if (isLoginSubmission(request, response)) {                return executeLogin(request, response);            } else {                return true;            }        } else {            saveRequestAndRedirectToLogin(request, response);            return false;        }    }        protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {        saveRequest(request);        redirectToLogin(request, response);    }        protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {        String loginUrl = getLoginUrl();        WebUtils.issueRedirect(request, response, loginUrl, null, true, false);    }}

示例中,将onAccessDenied中须要本来调用WebUtils.issueRedirect办法的http10Compatible参数改为false即可。

下面只是示例,实际上不仅包含胜利页面,还包含失败页面等,都须要从新实现一下对应的办法。最初,在shiroFilter中配置自定义的拦截器。

    <!-- 自定义的登录过滤器-->    <bean id="customFilter" class="com.senzhuang.shiro.CustomFormAuthenticationFilter" />     <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">        <property name="securityManager" ref="securityManager" />        <property name="loginUrl" value="/login.html"></property>        <property name="unauthorizedUrl" value="/refuse.html"></property>        <property name="filters">            <map>                <entry key="authc" value-ref="customFilter"/>            </map>        </property>    </bean>

通过上述的革新,对于HTTPS中的HTTP申请问题曾经失去解决了。

为了避免脱漏,又挨个点了一些页面,又发了问题了!哎,咋那么手欠呢……

LayUI的坑

原本认为解决了下面的问题,就彻底解决了,能够吃顿烧烤庆贺一下了。后果,在前端页面中又发现了相似的谬误。但此时错误信息来自拜访登录页面的门路:

http://example.com/a/login

奇了怪了,曾经登录胜利了,为什么业务操作页面还会再申请login页面呢?而且跳转过来还是HTTP申请,而不是HTTPS的申请。

查看了一下login的申请后果:

排查了相干的业务代码,登录实现之后,再也没有申请登录申请了啊,为什么会再次申请一次login呢?难道是拜访某些资源受限,导致重定向到登录页面了?

于是,查看了一下HTML调用的”Initiator“:

原来是LayUI申请对应的layer.css资源时,触发了login的登录操作。

首先想到的是Shiro中没有放开动态资源的拦挡,于是在Shiro中放开了layui的拦挡权限,但问题曾经存在。

再次排查,发现页面中没有被动引入layer.css文件,于是被动引入了layer.css文件,但问题还是存在。

没方法,只好查看layui.js,看看为什么要发动这个申请。此时,还留意到申请门路中有一个"undefinedcss"的词。

用过js的敌人都晓得,undefined是js中变量未初始化的默认值,相似Java中的null。

在layui.js中搜寻”css/“,还真找到这样一段代码:

return layui.link(o.dir + "css/" + e, t, n)

对照起来,也就是说o.dir的值为"undefined",与前面的css连接起来就变成了"undefinedcss",而这个门路并不存在,也没在Shiro中进行权限配置,默认会走到登录界面去。而这里是外部的一个异步的redirect申请,不会在页面出现,要查看浏览器的错误信息能力发现。

找到问题起因了,革新起来就简略了,将layui的link办法参数进行批改:

// 正文掉// return layui.link(o.dir + "css/" + e, t, n)// 改为return layui.link((o.dir ? o.dir:"/static/sc_layui/") +"css/"+e, t, n)

革新的基本思路是:如果o.dir有值(js中有值即为true)则应用o.dir的值;如果o.dir为undefined则采纳指定的默认值。

其中"/static/sc_layui/"为我的项目中寄存layui组件的门路。因为layui.js可能是压缩后的js,可通过搜寻”css/“或”layui.link“找到对应的代码。

重启我的项目,革除浏览器缓存,再次拜访页面,问题失去彻底解决。

能够安心吃烤串了

周末又花了半天工夫,终于把这个问题彻底解决了,当初能够安心去吃顿烤串庆贺一下了。

最初,回顾一下这个过程,看看你能从中播种到什么:

  • 呈现问题:不同环境(HTTP和HTTPS)须要手动改代码;
  • 寻找问题:为了平安,HTTPS内不容许发动HTTP申请;
  • 解决问题:两种形式敞开http10Compatible
  • Shiro问题:Shiro中默认为敞开http10Compatible,重写Filter,实现敞开操作;
  • LayUI Bug修复:LayUI代码bug,导致发动http(登录)申请。修复此Bug;

在这个过程中,如果你只是安于现状,”遵守规则“,每次上线时批改一下文件,不仅费时费力,而且不知为什么要这么做。

但如果像笔者一样,刨根问底的追踪一下,你将会学到一系列的常识:

  • HTTP申请的CSP,upgrade-insecure-requests配置;
  • HTTPS中为什么不能发动HTTP申请;
  • Spring视图解析器中配置http10Compatible
  • redirect形式视图返回的弊病;
  • Nginx中如何将HTTP申请转为HTTPS申请;
  • HTTP申请的混合内容(Mixed Content)概念及谬误;
  • HTTP 1.0、HTTP 1.1、HTTP2.0协定的区别;
  • Shiro拦截器自定义Filter;
  • Shiro拦截器过滤指定URL拜访;
  • Shiro拦截器的配置及局部源码实现;
  • LayUI的一个bug;
  • 其余排查该问题时用到或学到的技术;

这些技术你学到了吗?解决问题的思路和形式办法你学到了吗?如果本文有那么一点内容启发到你了,我不吝分享,你也不要悭吝,点个赞吧。

博主简介:《SpringBoot技术底细》技术图书作者,热爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢送关注~

技术交换:请分割博主微信号:zhuan2quan