无法登录的用户

0

“有用户在手机端认证失败。”

ins项目的微信群里的客户又遇到了新的问题。

“不像是网络问题,感觉是后端服务的问题。”

“用其他手机试试呢?”大鹏眉头皱了一下。

自从ins项目上线以后,团队其他成员都纷纷下了项目,只留下他这个项目经理留在一线解决问题。登录这块总是出现问题,上次就出现过一次,不过上次是机房网络原因,而这次貌似并不是。

“她用我的手机是可以登录的。”客户说。

“看来这个问题跟设备有关。”大鹏想。

这时客户发来了报错的手机截图,可以看到屏幕中间有一个提示框,上面显示“认证失败”4个字。

“志豪,帮忙看看什么情况下会出现这个错误。”大鹏呼唤了开发志豪。志豪是ins项目的前端开发,登录功能就是他实现的。

“这个错误是我们报出来的,应该是没有认证通过。”志豪已经上了新的项目,不过依然抽空支持着。“我们的前端登录组件会拿到办公App给我们的参数data和token,然后发送到认证服务进行认证,认证失败了就会报这个错。”

ins项目的手机端应用是一个Web应用。用户登录办公App后点击ins的图标,办公App就会启动WebView,打开ins手机端的URL,并在URL上带上data和token参数。data包含了用户信息,token用于对data的校验。这个URL对应的就是上文提到的前端登录组件,这个组件会把data和token发送给后端的认证服务做认证,认证服务来解析data获取用户信息并校验token。如果这一步出错了就会返回认证失败响应,而前端就会提示“认证失败”。

┌─────┐  /login?data=xxx&token=yyy   ┌───────┐  /auth?data=xxx&token=yyy   ┌──────┐
│ App │ ───────────────────────────> │ Login │ ──────────────────────────> │ Auth │
└─────┘                              └───────┘                             └──────┘

“认证服务什么情况下会返回错误呢?”大鹏追问道。

“这个要看认证服务的日志了,看看到底哪里出了问题。”志豪回答道。现在掌握的信息太少,还无法作出判断。

“下午要去机房看看了。”大鹏喃喃道。

1

在机房里大鹏看到的认证服务的日志。认证服务的日志显示,AuthService.convertHexToByte方法报错了。token应该是一段类似于34ac的十六进制的字符串,但是认证服务拿到的token却是M5开头的,这明显不是十六进制,所以在验证的时候报错了。

“看起来是有些办公App的token格式不对。”志豪猜测。

“应该和设备有关系,跟人无关。同一个人使用自己的设备就不能登录,而使用别人的手机就可以登录。”大鹏补充道。

“不同设备之间会有什么区别呢?”志豪问道。“是不是版本问题?让他们把办公App都升级到最新版本呢?”

“不能登录的设备确认是最新版本。不是版本的问题。”大鹏回答道。

“我们需要更多输入,需要熟悉办公App认证逻辑的人。”志豪提出需要外部支持。

大鹏把隔壁项目的后端TL大宝拉进了群。“大宝,ins项目移动端应用有的用户用别人的手机就可以登录,但是用自己的手机却无法登录。”隔壁项目也有移动端,也和办公App进行了集成。“你能想到大概是什么原因吗?”大鹏在微信群里贴出了convertHexToByte方法的代码。

“我这边后端确实有这个代码。”大宝看到了代码,“不过我们没有遇到无法登录的问题。”

问了一圈但没有人遇到类似的问题,所以很可能是ins项目自身的问题。大鹏又回到了刚才的推测:不同客户端的token格式不对,既然这样,是不是把token的验证这个步骤去掉,用户就可以正常登录了?

“既然验证token的时候报错了,那我去问问客户,是不是可以把token的校验逻辑去掉。去掉以后,虽然有一定安全问题,但应该可以解决用户不能登录的问题。”大鹏在微信群里说道。

“这样不好吧。”志豪说。“问题的原因并没有找到,为什么认证服务拿到的token不是预期的十六进制字符串的原因还不清楚,所以去掉token的校验并不一定就可以登录了。而且就算能登录,还会带来安全性问题,并不是一个正确的方法。”

经过一番讨论,大鹏觉得志豪说的有道理,打消了去掉校验的想法。不过问题仍然没有解决,所以他们商量了一下,决定问一下ins项目的TL张伟。张伟现在已经上了别的项目,项目刚启动,他比较忙,晚上才有时间,所以他们约在了晚上8点开个视频会议。

这个问题引起了志豪的好奇心,登录功能也是反复测试过的,怎么会一上线就遇到了问题呢?为了搞清楚原因,也为了项目顺利验收,志豪决定晚上留下来研究下这个问题。

大鹏也利用这段时间又研究一下日志。他发现认证服务收到的token貌似由两部分组成,前半部分由M5开头,显然不是十六进制,但后半部分是十六进制字符串,两部分之间由一个+符号连接。

“看来后半部分才是正确的token。”他把这个猜测告诉了志豪,“认证服务收到请求的时候token已经错了。”

“嗯,看来是这样。”志豪说道。“而且这个加号貌似有问题。”

2

晚上大鹏来到办公室,和志豪一起跟张伟开了视频会议。张伟把登录的流程完整的说明了一遍,就匆匆下线了。志豪依据张伟的讲述画出了完整的时序图:

​ 可以看到前端登录组件和认证服务之间还有一个API Gateway。

既然发给认证服务的HTTP请求就是错的,那么问题应该出现在认证服务之前的前端登录组件或者API Gateway。大鹏又查看了前端登录组件的日志,日志显示在办公App调用前端登录组件的URL里,data和token是正确的。data中包含的%2B引起了大鹏的注意,%2B之前的部分就是认证服务收到的data,而%2B后面的部分和正确token一起,被当作token传给了认证服务。

“认证服务收到的错误的token,可以分成三个部分:data的%2B之后的部分,这个加号,还有正确的token。”大鹏把这个发现告诉了志豪。“感觉我们越来越接近真相了。” 志豪点点头。“现在问题已经逐渐明确,就是有个倒霉孩子把data的后半部分混入了token。”

还可以通过搜索引擎和阅读代码获取更多信息。志豪暂时想不到合适的搜索关键字,所以他选择先从代码中收集更多信息。

由于前端登录组件收到的信息是对的,而认证服务收到的信息是错的,志豪结合时序图判断问题应该只会出现在以下3个地方:

  • 前端登录组件获取参数并调用API Gateway时
  • API Gateway解析请求时
  • API Gateway调用认证服务时

因为对于前端登录组件的代码还是很有信心的,所以志豪决定从后往前排查问题。

志豪首先检查了API Gateway调用认证服务的代码:

    @GetMapping("/authentications")
    AuthInfo getAuthInfoByDataAndToken(@RequestParam("data") String data,
                                       @RequestParam("token") String token);

由于使用了Feign,代码逻辑也非常简单,看上去没什么可能会造成data的部分混token里。

接下来检查API Gateway解析请求的代码。前端登录组件拿到data和token后,会把他俩传给API Gateway去做认证。具体的方式是把data和token放到HTTP Header里:

X-User-Login: APP $data $token

API Gateway在接收到请求后,取到HTTP Header里的值,把APP前缀去掉,然后找到第一个空格,空格前的部分保存为data,后面的部分保存为token。代码如下:

    private String[] extractAndDecodeHeader(String loginHeaderValue) {
        final String dataAndToken = loginHeaderValue.substring(APP_PREFIX.length());
        int spaceIndex = dataAndToken.indexOf(" ");
        if (spaceIndex == -1) {
            throw new BadCredentialsException();
        }
        return new String[]{dataAndToken.substring(0, spaceIndex), dataAndToken.substring(spaceIndex + 1)};
    }

这段代码有测试覆盖,CI也是过的。志豪思考了一下,得出结论:当请求正确时,这部分代码也不会出现问题;但是如果请求里的data包含了空格,那么data的后半部分就会混在token里。志豪笑了笑,他感觉抓到了线索。

data是Base64编码过的字符串,而token是十六进制对应的字符串。Base64编码后内容只会包含大小写字母、数字和+/这64个字符,十六进制字符串只会包含数字和字母A-F,所以这两者都不会包含空格。

目标继续缩小到了前端登录组件里。相关的代码如下:

import URLSearchParams from 'url-search-params';

const searchParams = new URLSearchParams(search);
const [data, token] = [ searchParams.get('data'), searchParams.get('token') ];

...

return `APP ${data} ${token}`;

这里用到的url-search-params是一个npm包。这段代码分别取到data和token参数,然后用空格作为分隔符,和APP前缀拼在一起返回。

如果URLSearchParams把%2B经过URL解码成空格,那么${data} ${token}就是$data的前半部分$data的后半部分$token,所以API Gateway就会把$data的前半部分当作data$data的后半部分$token当作token传给认证服务,那么认证服务就会在校验token的时候报错,这正好和问题出现时的现象一致。而且也解释了为什么认证服务拿到的错误的token里会包含加号。

如果一个参数要放到URL的query string里,那么这个参数需要经过URL编码。比如在谷歌搜索hello world,结果页的URL则是https://www.google.com/search?q=hello+world。空格会被编码成+,而+会被编码成%2B。相对的,在获取到URL后,需要经过URL解码才能拿到正确的参数。URLSearchParams就是一个可以用来进行URL解码的工具。在日志里看到一般都是URL,所以参数都是编码过的。

看上去一步步接近真相了,志豪有些兴奋。他写了一段简单的测试代码:new URLSearchParams('q=%2B').get('q')。如果结果为+,则是正确的,不会产生问题;如果结果是空格,就是错误的,就会造成无法登录的问题,就意味着原因找到了。

志豪在Node.js环境测试,结果发现返回的是+。“嗯,是正确的。”志豪自言自语道。“还有其他情况吗?对了,url-search-paramsURLSearchParams API的polyfill,所以如果浏览器原生支持URLSearchParams API,那就会使用原生的URLSearchParams API,而不是npm包。”

polyfill允许Web开发人员使用某HTML API,即使浏览器并不支持它。通常ployfill先检查浏览器是否支持了该API,如果支持了则直接使用,否则使用ployfill的实现。

“是不是在原生支持URLSearchParams API的浏览器里有问题?”志豪又打开了Chrome开发者工具的控制台面板,在里面进行了测试。结果也是+。这个结果说明,Chrome已经原生支持了URLSearchParams API,而且原生的URLSearchParams API也是正确的。

志豪摇了摇头,问题仍未确认。

3

“到底在什么情况下才会出现问题这个呢?”志豪思考着。

“这个问题跟设备有关。”大鹏也突然想到了什么。“我去问问无法登录的设备的型号。”

大鹏赶快给客户打了电话,得到的回复是,两部出问题的手机都是iPhone,而且iOS版本分别是10.3.2和10.3.3。

志豪感到眼前一亮:“莫非是iOS 10.3有问题?如果这个假设成立,那么iOS 10.3应该用的不是polyfill,所以它应该是原生支持URLSearchParams API的。”志豪想着。

志豪搜索了一下,找到了MDN的URLSearchParams文档(历史版本),发现浏览器兼容性部分里显示Safari Mobile并不支持URLSearchParams API。

“难道这个推理是错的?”逐渐清晰的真相又模糊起来。“不过还是用iOS模拟器试一下吧。”志豪打开了Xcode,发现只安装了默认的iOS 11模拟器,于是在设置里找到了iOS 10.3.1模拟器,开始下载。

趁着下载的时间,志豪测试了iOS 11,结果同样是+。“看来MDN上写错了,还想骗我。”志豪嘴角翘了起来。

经过十几分钟等待,iOS 10.3.1模拟器终于下载好了。志豪速度测试了一下。 结果是空格!

“终于把你这个倒霉孩子找出来了!”志豪情不自禁的欢呼起来。“终于找到你了。”

4

志豪不放心的又查了一下兼容性,发现在MDN中文版的URLSearchParamsW3cubeDocs赫然显示Safari Mobile从10.3开始原生支持URLSearchParams API。嗯,果然是MDN出错了。

iOS从10.3开始原生支持URLSearchParams API,但也许因为是第一次支持,这个版本有点问题,随后的iOS 11修复了这个问题。

“我刚用iOS 10.2试了一下,返回的是加号啊。”大鹏在一旁也没有闲着。

“那就对了,10.2并不原生支持URLSearchParams API,用的是polyfill,所以也没有问题。”志豪利用刚找到的真相完美的解释了这个问题。

5

不知不觉已经快11点了,志豪和大鹏准备回家,却发现6部电梯都停止了服务,两个人只好爬楼梯。

“没想到浓眉大眼的iOS也有这种坑。”志豪一边下楼一边感慨道。

“是啊,让我们好找啊。”大鹏一边喘气一边说。“话说这个问题有办法避免吗?”

“之前可能还真没办法预料到。如果URLSearchParams API文档里能说明iOS 10.3的问题就好了,但我刚才搜索了一圈,并没有发现有人在讨论这个问题。”志豪回想着。“应该做点什么,不要让它再祸害其他人了。”

“写篇博客?”大鹏提议。

“不仅如此,还应该把这个问题更新到MDN上。”志豪说。“以后的人也许就可以避开这个坑了。”

(完)


更多精彩洞见,请关注微信公众号:思特沃克

Share