前言
为了方便小程序应用使用微信登录态进行授权登录,微信小程序提供了登录授权的开放接口。乍一看文档,感觉文档上讲的非常有道理,但是实现起来又真的是摸不着头脑,不知道如何管理和维护登录态。本文就来手把手的教会大家在业务里如何接入和维护微信登录态。
小程序登录鉴权流程
这里官方文档上的流程图已经足够清晰,我们直接就该图展开详述和补充。
首先大家看到这张图,肯定会注意到小程序进行通信交互的不止是小程序前端和我们自己的服务端,微信第三方服务端也参与其中,那么微信服务端在其中扮演着怎样的角色呢?我们一起来串一遍登录鉴权的流程就明白了。
1. 调用wx.login
生成code
wx.login()
这个API
的作用就是为当前用户生成一个临时的登录凭证,这个临时登录凭证的有效期只有五分钟。我们拿到这个登录凭证后就可以进行下一步操作:获取openid
和session_key
wx.login({
success: function(loginRes) {
if (loginRes.code) {
// example: 081LXytJ1Xq1Y40sg3uJ1FWntJ1LXyth
}
}
});
2. 获取openid
和session_key
我们先来介绍下openid
,用过公众号的童鞋应该对这个标识都不陌生了,在公众平台里,用来标识每个用户在订阅号、服务号、小程序这三种不同应用的唯一标识,也就是说每个用户在每个应用的openid
都是不一致的,所以在小程序里,我们可以用openid
来标识用户的唯一性。
openid
与unionid
的区别:
- 一个用户可以有多个
openid
,一个用户只有一个unionid
- 一个微信开放平台下面可以有多个应用,用户在每个应用中有唯一的
openid
,用户在一个开放平台下面有唯一的unionid
,用户在不同应用中openid
是不同的如果要多个小程序识别同一用户,可以把多个小程序绑定在微信开放平台帐号下,用
unionid
来判断
那么session_key
是用来干嘛的呢?有了用户标识,我们就需要让该用户进行登录,那么session_key
就保证了当前用户进行会话操作的有效性,这个session_key
是微信服务端给我们派发的。也就是说,我们可以用这个标识来间接地维护我们小程序用户的登录态,那么这个session_key
是怎么拿到的呢?我们需要在自己的服务端请求微信提供的第三方接口https://api.weixin.qq.com/sns/jscode2session
,这个接口需要带上四个参数字段:
参数 | 值 |
---|---|
appid | 小程序的appid |
secret | 小程序的secret |
js_code | 前面调用wx.login 派发的code |
grant_type | 'authorization_code' |
从这几个参数,我们可以看出,要请求这个接口必须先调用wx.login()
来获取到用户当前会话的code
。那么为什么我们要在服务端来请求这个接口呢?其实是出于安全性的考量,如果我们在前端通过request
调用此接口,就不可避免的需要将我们小程序的appid
和小程序的secret
暴露在外部,同时也将微信服务端下发的session_key
暴露给“有心之人”,这就给我们的业务安全带来极大的风险。除了需要在服务端进行session_key
的获取,我们还需要注意两点:
session_key
和微信派发的code
是一一对应的,同一code
只能换取一次session_key
。每次调用wx.login()
,都会下发一个新的code
和对应的session_key
,为了保证用户体验和登录态的有效性,开发者需要清楚用户需要重新登录时才去调用wx.login()
session_key
是有时效性的,即便是不调用wx.login
,session_key
也会过期,过期时间跟用户使用小程序的频率成正相关,但具体的时间长短开发者和用户都是获取不到的
function getSessionKey (code, appid, appSecret) {
var opt = {
method: 'GET',
url: 'https://api.weixin.qq.com/sns/jscode2session',
params: {
appid: appid,
secret: appSecret,
js_code: code,
grant_type: 'authorization_code'
}
};
return http(opt).then(function (response) {
var data = response.data;
if (!data.openid || !data.session_key || data.errcode) {
return {
result: -2,
errmsg: data.errmsg || '返回数据字段不完整'
}
} else {
return data
}
});
}
上面说到session_key
是小程序与微信服务端的登录态(会话密钥),如果想获取敏感数据和开放数据,可以调用wx.getUserInfo获得开放数据,但是没有登录获取解密密钥session_key
是无法解密敏感数据的,所以开放数据中,没有登录的话,我们只能获得用户昵称、头像、城市、性别、国籍等,并没有可以标识用户的唯一id
。
登录后我们就可以获取到加密数据encryptedData
,但加密数据encryptedData
需要使用用户的session_key
作为秘钥进行解密才能得到原始完整数据。为了防止返回的数据被人篡改伪造,开发者可以将 signature、rawData
发送到开发者服务器进行校验。服务器利用用户对应的 session_key
使用相同的算法计算出签名 signature2
,比对 signature
与 signature2
即可校验数据的完整性。返回结果主要包括:
参数 类型 说明
userInfo OBJECT 用户信息对象,不包含 openid 等敏感信息
rawData String 不包括敏感信息的原始数据字符串,用于计算签名。
signature String 使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息,参考文档 signature。
encryptedData String 包括敏感数据在内的完整用户信息的加密数据,详细见加密数据解密算法
iv String 加密算法的初始向量,详细见加密数据解密算法
//调用成功后返回结果示例
{
encryptedData:"6exNeBvACX+EpzLNF2vYRhM0Z1tNZnBOYP0Qh6jQ4Ofv+69qY/RGVAB34aj4f364mkjAAD5pgDDJ41hGkkr/IaHJGioI3EhGSEQyrAfE1mPovV9s6Prg4rKedfBUerG4jEgXbS0GeERMtJ15tqEGz7qzcA49c1D1obZTuneImZq1vB1lXo3oUbnmK7LUAxQcgCQA==",
errMsg:"getUserInfo:ok",
iv:"X6PuFoueBpfgyjV9l9D6A==",
rawData:"{"nickName":"xxx","gender":1,"language":"zh_CN","city":"","province":"","country":"Iceland","avatarUrl":"https://wx.qlogo.cn/mmsen/vi_s/0daYVYsscPNxEvALzBTsJarXwUmlxjZJZMHPM6NNLegH4wLRcRlsA1x4bsJg/132"}",
signature:"c32wba325164258b9a332b6fdsdew2e520081348",
userInfo:
{
avatarUrl:"https://wx.qlogo.cn/mmsen/vi_s/0daYVYsscPNxEvALzBTsJarXwUmlxjZJZMHPM6NNLegH4wLRcRlsA1x4bsJg/132",
city:"",
country:"Iceland",
gender:1,
language:"zh_CN",
nickName:"xxx",
province:""
}
}
敏感数据获取(加密数据
encryptedData
签名signature
)对于
encryptedData
,开发者如需要获取敏感数据,需要对接口返回的加密数据( encryptedData )进行对称解密。
也就是需要在开发者服务器上利用session_key
解密获得的encryptedData
,其结构如下:
{
"openId": "OPENID",
"nickName": "NICKNAME",
"gender": GENDER,
"city": "CITY",
"province": "PROVINCE",
"country": "COUNTRY",
"avatarUrl": "AVATARURL",
"unionId": "UNIONID",
"watermark": //数据水印
{
"appid":"APPID", //敏感数据归属appid,开发者可校验此参数与自身appid是否一致
"timestamp":TIMESTAMP //敏感数据获取的时间戳, 开发者可以用于数据时效性校验
}
}
会话密钥 session_key 有效性
开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。
- wx.login 调用时,用户的 session_key 可能会被更新而致使旧 session_key 失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用 wx.login,并非每次调用都导致 session_key 刷新)。开发者应该在明确需要重新登录时才调用 wx.login,及时通过 auth.code2Session 接口更新服务器存储的 session_key。
- 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
- 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。
- 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。
3. 生成3rd_session
前面说过通过session_key
来“间接”地维护登录态,所谓间接,也就是我们需要自己维护用户的登录态信息,这里也是考虑到安全性因素,如果直接使用微信服务端派发的session_key
来作为业务方的登录态使用,会被“有心之人”用来获取用户的敏感信息,比如wx.getUserInfo()
这个接口呢,就需要session_key
来配合解密微信用户的敏感信息。
那么我们如果生成自己的登录态标识呢,这里可以使用几种常见的不可逆的哈希算法,比如md5、sha1
等,因为openid
是用户的唯一标识,根据它生成后的登录态标识(这里我们统称为'skey'
)返回给前端,并在前端维护这份登录态标识(一般是存入storage
)。而在服务端呢,我们会把生成的skey
存在用户对应的数据表中,前端通过传递skey
来存取用户的信息。
可以看到这里我们使用了sha1
算法来生成了一个skey
:
const crypto = require('crypto');
return getSessionKey(code, appid, secret)
.then(resData => {
// 选择加密算法生成自己的登录态标识
const { session_key } = resData;
const skey = encryptSha1(session_key);
});
function encryptSha1(data) {
return crypto.createHash('sha1').update(data, 'utf8').digest('hex')
}
4. checkSession
前面我们将skey
存入前端的storage
里,每次进行用户数据请求时会带上skey
,那么如果此时session_key
过期呢?所以我们需要调用到wx.checkSession()
这个API
来校验当前session_key
是否已经过期,这个API
并不需要传入任何有关session_key
的信息参数,而是微信小程序自己去调自己的服务来查询用户最近一次生成的session_key
是否过期。如果当前session_key
过期,就让用户来重新登录,更新session_key
,并将最新的skey
存入用户数据表中。
checkSession
这个步骤呢,我们一般是放在小程序启动时就校验登录态的逻辑处,这里贴个校验登录态的流程图:
下面代码即校验登录态的简单流程:
let loginFlag = wx.getStorageSync('skey');
if (loginFlag) {
// 检查 session_key 是否过期
wx.checkSession({
// session_key 有效(未过期)
success: function() {
// 业务逻辑处理
},
// session_key 过期
fail: function() {
// session_key过期,重新登录
doLogin();
}
});
) else {
// 无skey,作为首次登录
doLogin();
}
在实际项目中,我们只需要在小程序启动和调用需要用到session_key
的API
时使用wx.checkSeesion
校验session_key
是否有效,因为wx.checkSeesion
非常耗时,大约200ms
,所以不能每次接口调用前都使用wx.checkSession
检查
小程序手机号登录鉴权流程
思路:将微信用户与公司一账通用户建立映射关系,自定义登录态与手机号、unionid
或者openid
相关联
时序图如下:
参考:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。