明天这篇文章给大家讲一个追究 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