最近做的项目中需要使用到微信支付,从而涉及到拉取微信授权,因为测试环境域名切换的问题,导致测试环境上的微信支付是不可用的,想着之前生产环境一直是ok的,于是抱着这种心理,直接发布生产环境做验证,结果就悲剧了······
下面着重讲解一下这次事故的整个解决过程和思路。
【bug现象描述】
扫描二维码进入程序首页,此时会拉起用户微信授权,以获取用户信息,正常情况下,新用户会弹框请求同意授权,然后成功获取授权。而现状是获取授权失败,导致页面不断重复闪屏刷新去调用授权接口。
【问题定位过程】
-
初次定位:
因为近期公司刚做过域名切换,现在处于新旧两个域名同时使用的阶段,第一反应是不是公众号后台配置的域名地址有问题,于是去检查公众号后台的配置,发现确实没有新域名。配置上新域名后再次测试,发现问题还是存在,说明引起问题的原因与域名配置无关。
-
再探究竟:
通过查看服务器日志记录,发现导致页面重复闪屏刷新原因是因为后台没有获取到HttpServletRequest中的cookie,该cookie中保存的信息为用户信息token。以该cookie为切入点反向推进,发现cookie的写入点是在某个filter中,然后查看该filter打印的关键日志,未果。
-
详细探究:
- 因为该系统filter众多,而且由于历史原因,导致其中绝大部分的filter日志关键字全部是一样的,完全没法区分当前接口到底进的是哪个filter,于是对日志关键字做了下调整,加上了filter的类名用作区分;
- 再次测试时发现某台服务器上调用微信报了连接超时,经排查发现这台机器是后面扩容时新增加的机器,未开通外网访问权限,调整安全组后访问外网正常。同时由于做了负载均衡,多台服务器查看日志不便,在这里将流量全部调整到一台机器上。
- 接着测试时候,找到了该filter的入口日志,但逻辑处理部分的代码未能执行,继续深挖原因。阅读祖传代码,发现这个filter里面有个校验URI是否需要拦截的方法,如下:
/**
* 验证URI是否需要拦截
* @param request
* @return
*/
public static Boolean verifyUri(HttpServletRequest request) {
String uri = request.getRequestURI();
String uris = PropertiesUtil.getPropertyValues("wechat.filter_uri_list", "");
if (uris.indexOf(uri + ",") != -1) {
return true;
}
return false;
}
这里使用了一个配置项,读取配置项里的内容得到一个uris的字符串,然后取当前调用的uri进行比对,如果该uri存在于uris中,则调用微信api获取网页授权access_token;如果该uri不在uris中,则filter直接放行。
乍一看,这逻辑没毛病啊,然后我去看了一下生产环境配置中心该配置项的值,哦豁,豁然开朗茅塞顿开醍醐灌顶······原来都是(uris.indexOf(uri + ",") != -1)这个骚操作在搞事情!加上这个","之后,那么要求你的uris这个字符串的格式必须是"/a/b/c,/a/b/d,/a/b/e,"这种,而配置中心上配置的却是"/a/b/c,/a/b/d,/a/b/e"······
4.改配置项,再次测试,正常拉起授权,微信支付ok。
【反思总结】
- 排查问题时要由表象倒推深层次根本原因,及时查看日志;
- 平常编码时一定要注意编码规范,不同接口日志关键字要差异化,方便后续调试和排查生产问题;
- 服务器扩容时要注意网络访问权限和文件访问权限等问题;
- 校验配置项中是否包含某段字符串,最好是使用转化为list然后再进行比较的这种方式,一一举例说明优缺点,如下:
方法一:
private static boolean verifyUri1(String uri) {
String uris = "/a/b/c,/a/b/d,/a/b/e";
if (StringUtils.contains(uris, uri)) {
return true;
}
return false;
}
优点:编码简单,几乎不需要任何思考
缺点:方法粗暴,如果有类似于“/a/b”这样的接口出现,会被误杀
方法二:
private static boolean verifyUri2(String uri) {
String uris = "/a/b/c,/a/b/d,/a/b/e,";
if (uris.indexOf(uri + ",") != -1) {
return true;
}
return false;
}
优点:解决了方法一中可能误判的问题
缺点:不能区分大小写(自行补充contains方法和indexOf方法的区别),配置项看着有点奇怪,不能一目了然的理解(拼接逗号的问题)
方法三(推荐):
private static boolean verifyUri3(String uri) {
String uris = "/a/b/c,/a/b/d,/a/b/e";
String[] array = uris.split(",");
List<String> list = new ArrayList<>(array.length);
Collections.addAll(list, array);
for (String s : list) {
if (StringUtils.equals(uri, s)) {
return true;
}
}
return false;
}
优点:代码结构清晰易懂,不存在方法一中的问题
缺点:大批量数据处理时效率较低(字符串——>数组——>List——>for循环)
【延伸拓展】
经过此次排查问题的过程,把整个对接微信授权的流程又梳理了一遍,大致流程如下(详细对接方案参考微信开放文档):
- 第一步:用户同意授权,获取code
- 第二步:通过code换取网页授权access_token
- 第三步:刷新access_token(如果需要)
- 第四步:拉取用户信息(需scope为 snsapi_userinfo)
- 附:检验授权凭证(access_token)是否有效