52

无法登陆的用户

 7 years ago
source link: https://insights.thoughtworks.cn/can-not-log-in/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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上。”志豪说。“以后的人也许就可以避开这个坑了。”

(完)

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK