小程序原始id校验(小程序用户UnionID的获取及登录状态维护)

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(1)

气得我啃键盘

赶在下班前把我们的鲲豆荚小程序第一版提交了审核,趁现在还在有脑回路的状态下记下踩的坑和与之对应的解决策略。

之前的文章我们提过,微信生态体系下,同一个用户在不同的小程序中的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,嗯?


小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(2)

带着这个问题再看了不知几多遍官方文档,原来是UnionID下发是有很多条件限制的,以下贴图我很难表述:

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(3)

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(4)

此时我内心是五彩斑斓的,就像那打翻了吹彩虹屁的糖罐,实话不怕告诉你,胡里花哨东西太多,我都看不懂啊!不管怎么样,选项太多,只好祭出排除大法:首先排除4,5,6三个方案,因为我们没有使用支付和云服务,第2和第3有点坑爹的意思,必须首先关注公众号才能获取UnionID?这个操作局限性太大,难道小程序的入口仅仅只有关注绑定的公众号才能进入吗?


小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(5)

什么乱七八糟的规则,很生气,生了一天的闷气!那就只剩第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信息如下:

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(6)

后续服务器端因数据库和登录实现各异,仅提供思路:既然我们获取到了unionId,应该将这个unionId和数据库用户进行比对,如果没有则作为新用户插入,接着需要颁发一个登录凭证token返回给小程序,小程序将这个token保存到本地,再后续发起需要登录凭证的API请求时带上。

实测并不完美的方案

在我们实际测试中,点击登录按钮后会经常出现“登录失败,请重试”的提示,接连再点又能成功登录。做程序这行呢,有bug并不可怕,怕就怕时而正常时而癫狂,同样的代码,同样的操作,为什么会结出不同样的果?为什么要这么秀?

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(7)

咆哮帝上身

就是说在解密的时候出错了,调试后发现返回的是-41003,对照error code说明,是“aes解密失败”的意思。但为什么大部分情况下又可以成功解密?难道是算法有问题?我用的是官方解密类库啊,这个应该不会吧,那就是获取的session_key有问题?可这个session_key也是我拿着code从微信服务器返回的啊,都是微信给的,这个锅我不要背。

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(8)

谁还不是个宝宝

再仔细翻看官方文档对session_key的说明,原来是有个时效性:

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(9)

“最短机制”,“session_key有效期不告诉你”,“频繁使用小程序,session_key有效期越长”,天哪,这是人写吗,这么模棱两可的话都写在官方文档里,一头雾水有没有?好吧,按照第3条说的,在每次调用wx.login()前,先调用wx.checkSession,但每次都是成功的,从来就没有出现fail的情况,所以问题依旧。一度陷入不知所措的地步,小编问什么时候能交稿,我说被一个问题卡很久了,没错就是这个,整整一个下午,毫无进展。

退一步海阔天空

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(10)

宝宝不开心

回过头重新思索了下整个登录流程:我们核心是需要获取到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直接调用本接口进行身份伪造。但毕竟这不是最优解决方案,微信登录流程优化上本应该能做到更好,说不定哪天就优化了呢……也说不定。

小程序原始id校验(小程序用户UnionID的获取及登录状态维护)(11)

满脸高兴


给你代码往期回顾:

给你代码:leetcode题目加小技巧

给你代码:小程序内容滚动与导航栏自动高亮联动

给你代码:小程序引入icon的三种方式

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页