小程序原始id校验(小程序用户UnionID的获取及登录状态维护)
气得我啃键盘
赶在下班前把我们的鲲豆荚小程序第一版提交了审核,趁现在还在有脑回路的状态下记下踩的坑和与之对应的解决策略。
之前的文章我们提过,微信生态体系下,同一个用户在不同的小程序中的OpenID都是不同的。因为我们需要识别出使用微信登录和小程序登录是同一个人,就不能使用openId了,微信也提供了这么一个标识:UnionID。官方对UnionID的机制说明如下:
如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,UnionID是相同的。
爱之初体验
我们希望不止能识别用户的唯一性,还希望用户可以把基础开放数据授权给我们:头像和昵称。微信提供了一个wx.login()方法,按照以往第三方登录开发经验,调用这个方法就可以完成授权登录了,可是这个只能返回一个5分钟时效的code,需要传回自己服务器调用微信接口auth.code2Session来获取OpenID和UnionID。好吧,我们按照步骤先实践一遍,当时源码是这样的:
<!-- wxml --> <button open-type="getUserInfo" bindgetuserinfo="login">登录</button>
js:
Page({ login(e) { wx.login({ success: res => { getApp().query({ api: 'user/thirdparty/mini:login', param: { code: res.code } }); } }); } })
服务器端接口 user/thirdparty/mini:
<?php return [ /** * 第三方登录,返回token * @param string $code 登录code * @return string token */ 'login' => function($code) { $api = 'jscode2session'; $params = [ 'appid' => '<<APP ID>>', 'secret' => '<<APP SECRET>>', 'js_code' => $code, 'grant_type' => 'authorization_code' ]; // 获取session_key $response = file_get_contents('https://api.weixin.qq.com/sns/'.$api.'?'.http_build_query($params)); $response = json_decode($response); if(isset($response->errcode)) throw new Exception($response->errmsg); print_r($response); } ];
应该是没有问题的,对不对?但是!!!实际打印出来的$response中,只有openid和session_key,就是没有我们最想要的unionid,嗯?
带着这个问题再看了不知几多遍官方文档,原来是UnionID下发是有很多条件限制的,以下贴图我很难表述:
此时我内心是五彩斑斓的,就像那打翻了吹彩虹屁的糖罐,实话不怕告诉你,胡里花哨东西太多,我都看不懂啊!不管怎么样,选项太多,只好祭出排除大法:首先排除4,5,6三个方案,因为我们没有使用支付和云服务,第2和第3有点坑爹的意思,必须首先关注公众号才能获取UnionID?这个操作局限性太大,难道小程序的入口仅仅只有关注绑定的公众号才能进入吗?
什么乱七八糟的规则,很生气,生了一天的闷气!那就只剩第1条路去走一走了!
从解密数据中获取UnionID
在open-type="getUserInfo"的button组件中,我们绑定了getUserInfo事件回调给login方法,e.detail.encryptedData有我们所需要的所有数据,但是encryptedData中的数据经过了加密,需要传回服务器解密,解密需要用到加密算法初始向量e.detail.iv。我们把login方法稍作修改,增加两个参数:
Page({ login(e) { wx.login({ success: res => { getApp().query({ api: 'user/thirdparty/mini:login', param: { code: res.code, encrypted: e.detail.encryptedData, iv: e.detail.iv } }); } }); } })
服务器端在做解密校验之前,需要下载小程序的解密库,下载地址:https://res.wx.qq.com/wxdoc/dist/assets/media/aes-sample.eae1f364.zip。本例使用的是世界上最好的语言,校验代码如下:
<?php return [ /** * 第三方登录,返回token * @param string $code 登录code * @param string $encrypted 通过getUserInfo获取的encryptedData * @param string $iv 加密初始向量 * @return string token */ 'login' => function($code, $encrypted, $iv) { $api = 'jscode2session'; $params = [ 'appid' => '<<APP ID>>', 'secret' => '<<APP SECRET>>', 'js_code' => $code, 'grant_type' => 'authorization_code' ]; // 获取session_key $response = file_get_contents('https://api.weixin.qq.com/sns/'.$api.'?'.http_build_query($params)); $response = json_decode($response); if(isset($response->errcode)) throw new Exception($response->errmsg); require '/path/to/wxBizDataCrypt.php'; // 引入小程序解密类库 $pc = new WXBizDataCrypt('<<APP ID>>', $response->session_key); $errCode = $pc->decryptData($encrypted, $iv, $user); if(0 != $errCode) throw new Exception('登录失败,请重试'); print_r($user); } ];
不出意外的话,能顺利打印出的$user信息如下:
后续服务器端因数据库和登录实现各异,仅提供思路:既然我们获取到了unionId,应该将这个unionId和数据库用户进行比对,如果没有则作为新用户插入,接着需要颁发一个登录凭证token返回给小程序,小程序将这个token保存到本地,再后续发起需要登录凭证的API请求时带上。
实测并不完美的方案
在我们实际测试中,点击登录按钮后会经常出现“登录失败,请重试”的提示,接连再点又能成功登录。做程序这行呢,有bug并不可怕,怕就怕时而正常时而癫狂,同样的代码,同样的操作,为什么会结出不同样的果?为什么要这么秀?
咆哮帝上身
就是说在解密的时候出错了,调试后发现返回的是-41003,对照error code说明,是“aes解密失败”的意思。但为什么大部分情况下又可以成功解密?难道是算法有问题?我用的是官方解密类库啊,这个应该不会吧,那就是获取的session_key有问题?可这个session_key也是我拿着code从微信服务器返回的啊,都是微信给的,这个锅我不要背。
谁还不是个宝宝
再仔细翻看官方文档对session_key的说明,原来是有个时效性:
“最短机制”,“session_key有效期不告诉你”,“频繁使用小程序,session_key有效期越长”,天哪,这是人写吗,这么模棱两可的话都写在官方文档里,一头雾水有没有?好吧,按照第3条说的,在每次调用wx.login()前,先调用wx.checkSession,但每次都是成功的,从来就没有出现fail的情况,所以问题依旧。一度陷入不知所措的地步,小编问什么时候能交稿,我说被一个问题卡很久了,没错就是这个,整整一个下午,毫无进展。
退一步海阔天空
宝宝不开心
回过头重新思索了下整个登录流程:我们核心是需要获取到UnionID,得到后就一切好办了。经过测试,第一次通过授权登录是不会出现解密失败的情况,那何不在用户第一次登录时记录下UnionID,在后续登录直接回传给服务器完成二次登录,这样无需经过解密环节,也就不会出现因session_key古怪的失效机制引起的问题。
我们在app.js中加入一个方法:id(),用来获取和设置UnionID:
App({ /** * 获取/设置用户小程序内的unionId * @param string unionId */ id(value = null) { return value ? wx.setStorageSync('id', value) : wx.getStorageSync('id'); } });
再次修改login()方法:
Page({ login(e) { let unionId = getApp().id(); if(unionId) { // 已缓存过用户唯一识别信息 getApp().query({ api: 'user/thirdparty/mini:relogin', param: { unionId: unionId } }) } else { // 首次授权登录 wx.login({ success: res => { getApp().query({ api: 'user/thirdparty/mini:login', param: { code: res.code, encrypted: e.detail.encryptedData, iv: e.detail.iv } }).then(data => getApp().id(data.unionId)); } }); } } })
我们在首次授权登录(用户移除了小程序后再次进入需要重新登录授权的也算首次,因为unionId也一并被清除了)后,服务器端会返回一个unionId字段,我们使用getApp().id()保存到小程序客户端,这样下次用户再次登录时会调用新的接口relogin:
<?php // user/thirdparty/mini return [ 'relogin' => function($unionId) { // @todo 检查当前token是否未被替换掉,设置登录状态,返回新颁发的token } ];
注意这里为了安全起见,需要将自身最后一次token(即使过期但仍未被替换)带上去服务器检查,以此为基础才能将颁发新的token。这样做是为了防止恶意用户拿到了别人的UnionId直接调用本接口进行身份伪造。但毕竟这不是最优解决方案,微信登录流程优化上本应该能做到更好,说不定哪天就优化了呢……也说不定。
满脸高兴
给你代码往期回顾:
给你代码:leetcode题目加小技巧
给你代码:小程序内容滚动与导航栏自动高亮联动
给你代码:小程序引入icon的三种方式
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com