一个以为不会有问题的问题 2022年9月25日 1129 代码厨子 你以为不会有问题的地方,往往是最危险的地方,反复用过N次的代码,都会出问题。出问题的原因很多,你防住了内因,更要注意外因的变化。 # 突发事件 这本是一个宁静的周末早晨,昨天答应闺女早上陪她骑车去看牙医,所以早早地就和她愉快地出门了。 不是我迷信,每逢重要事情,只要我在家就不会有问题,一旦出来,问题也就来了,今天当然也不例外。 活动从早上八点半开始,大约十五分钟后,客户给我发信息,说貌似有问题,用户数据进不来。 不可能,这绝不可能!这是我的第一反应,但是本着这么多年被打脸的经验,我还是认真地检查起来。果然,数据感觉是非常不正常的,只有少部分用户数据可以进入,绝大多数数据被挡在了门外。 这不科学啊! 我和闺女匆匆在外面吃完早餐,扔下了半杯咖啡就急速返回了,一路上闺女跟着我的自行车骑得满头大汗,就这样我还经常停下来等等她。一路上我就在想到底哪个环节出问题才会导致这个原因,思来想去,基本上定位在用户身份问题上。 # 紧急检查 打开电脑,从最简单的开始检查,无果。客户催的紧,没办法,只能采用紧急对症治疗方法,去掉了一些验证,让项目能够先正常运行,我再来仔细的检查原因。 经过紧急止血后,到家五分钟内,项目在用户侧恢复正常,能够正常回收数据,但是我知道,现在只是临时止血,并没有从根本上解决问题。于是,就有了下面的复盘经历。 项目大概是这样的流程:用户进入APP后,先判断是否获取了微信的openid,如果没有,就进行微信登录流程,登录完成后,操作关键业务后台会对用户进行记录,并进行去重数据。 我首先从提交数据进行查询,发现数据的确是从这个接口无法提交,我仔细检查了代码和数据结构,唯一可疑的就是用户这个信息,从代码上看,只有JWT出问题时候,才会无法进入数据。于是我反复测试,我的微信完全没有问题。 然后就推理到了生成JWT的程序,也就是login接口,这个接口其貌不扬,而且是反复使用的代码,按理说没有问题,怎么看也很难判断出问题所在,于是,我做了一个重要的动作,去查看微信的官方接口文档,就是这里:[微信网页开发接口文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html "微信网页开发接口文档"),有一段大字赫然在列: > 微信网页授权能力调整公告 # 问题定位 我晕了,又调整规则了,然后仔细的看了我的前后端代码,发现了问题。 前端获取js_code的代码: ```javascript function() { let appid = this.WX_Appid; let re_url = encodeURIComponent(this.Url + this.$route.fullPath); let goto_url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' + appid + '&redirect_uri=' + re_url + '&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect'; window.location.replace(goto_url); } ``` 不难看出,这是拼接了跳转代码,去获取js_code,看起来也算正常,再看看后端代码: ```python class WxLoginViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """ 获取微信openID来登录 """ serializer_class = WxLoginSerializer def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) token = cache.get('token') if not token: t_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code" % ( settings.CLOUD_CONFIG["wx_mp_appid"], settings.CLOUD_CONFIG["wx_mp_secret"], serializer.validated_data["code"]) token_res = requests.get(t_url) token = json.loads(token_res.text) if "access_token" in token: cache.set('token', token, timeout=3600) u_url = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN" % (token["access_token"], token["openid"]) wx_info_res = requests.get(u_url) wx_info = json.loads(wx_info_res.text.encode("raw_unicode_escape")) else: raise serializers.ValidationError("小故障,过会重试!") if "openid" in wx_info: try: user = User.objects.get(username=wx_info["openid"]) user.first_name = wx_info["nickname"] user.save() except BaseException: user = User.objects.create(**{"username": wx_info["openid"], "password": wx_info["openid"], "first_name": wx_info["nickname"]}) payload = jwt_payload_handler(user) return Response({"token": jwt_encode_handler(payload), "user_id": user.id, "wx_info": wx_info}, status=status.HTTP_201_CREATED) return Response({"detail": '有点晕了,过会重试!'}, status=status.HTTP_400_BAD_REQUEST) ``` 看起来也是其貌不扬,而且多个项目用过,然后仔细的对比后发现,的确问题就在这里。 # 解决方案 前端获取的js_code的类型是snsapi_userinfo,这次调整的就是关于snsapi_userinfo的方式,直接跳转是不行了,必须通过用户主动点击才能获取,snsapi_userinfo和snsapi_base方式结果的不同主要在于能否获取用户的昵称等信息,前者可以,但是需要用户主动发起,后者不能,但是可以无感登录。 看样子问题就在这了,我用的是snsapi_userinfo方式,但是是静默方式,所以如果是知青没有授权过公众号或者不是开发者的情况下,就有可能被拒绝,而后端我用了try函数,导致并不会崩塌,但是无法登录,最终导致无法进行业务。 还有一个错误就是,我以为的token是微信返回的token信息,其实登录是不用走token方式的,我还傻乎乎地保存了cache,真是一错再错。 既然问题找到了,那么我的调整方案是这样的: 首先,前端把snsapi_userinfo改为snsapi_base,其他都不变。 后端修改后的代码如下: ```python class WxLoginViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """ 获取微信openID来登录 """ serializer_class = WxLoginSerializer def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) try: t_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code" % ( settings.CLOUD_CONFIG["wx_mp_appid"], settings.CLOUD_CONFIG["wx_mp_secret"], serializer.validated_data["code"]) wx_info = requests.get(t_url).json() except: raise serializers.ValidationError("小故障,过会重试!") if "openid" in wx_info: try: user = User.objects.get(username=wx_info["openid"]) user.save() except BaseException: user = User.objects.create(**{"username": wx_info["openid"], "password": wx_info["openid"]}) payload = jwt_payload_handler(user) return Response({"token": jwt_encode_handler(payload), "user_id": user.id, "wx_info": wx_info}, status=status.HTTP_201_CREATED) return Response({"detail": '有点晕了,过会重试!'}, status=status.HTTP_400_BAD_REQUEST) ``` 经过测试,问题解决了,昵称也不要了,只要openid,这样就静默方式采集了。 # 经验总结 至此,问题解决,但是考虑到用户项目正在跑,我也不敢轻易再更新代码了,总结本篇文章,给自己提个醒,还是那句话,小心驶得万年船。至于教训,我总结了如下几点,与君共勉: 1. 千万不要相信什么东西是一层不变的,做代码就是要严谨到极致,任何一个错误,电脑都不会放过,因为电脑是没有情商的玩意。 2. 测试环节非常非常重要,特别是用户多样性的测试,必须要做。 3. 重要活动一定要值守电脑,这个事情一定要定计划执行,小心小心再小心。 4. 解决问题要果断,该止血止血,绝不拖泥带水。