上一篇文章手把手教会你小程序登录鉴权介绍了小程序如何进行登录鉴权,那么一般小程序的用户标识可以使用上文所述微信提供的jscode2session
接口来换取,小程序还提供了一个getUserInfo
的API来获取用户数据,这个用户数据里面也可以包含当前的用户标识openid。本文就如何获取小程序中的用户数据及数据完整性校验等内容来展开详述
wx.getUserInfo
是用来获取用户信息的API接口,下面是对应的参数字段:
字段 | 类型 | 是否必填 |
---|---|---|
withCredentials | Boolean | 否 |
lang | String | 否 |
timeout | Number | 否 |
success | Function | 否 |
fail | Function | 否 |
complete | Function | 否 |
lang 指定返回用户信息的语言,有三个值:
timeout 指定API调用的超时时间, getUserInfo
API其实底层也是客户端发起一个http请求,来获取到用户的相关数据,经过封装后返回给小程序端,后面会给大家详细介绍。
withCredentials 这个字段是一个布尔类型的值,决定了在调用API时小程序返回的数据里是否带上登录态信息,不填的话默认该字段的值为true
那么此时API返回的结果为:
字段 | 类型 | 描述 |
---|---|---|
encryptedData | String | 加密后的用户数据 |
iv | String | 解密算法向量 |
rawData | String | 用户开放数据 |
signature | String | 签名 |
userInfo | Object | 用户开放数据 |
如果该字段的值为false
,就不会返回上面这两个字段:encryptedData
, iv
。
openid
及unionid
等。那么数据加密采用的算法为AES-128-CBC
分组对称加解密算法,后面我们对这个加密算法进行详细分析。nickName(微信昵称)
、province(所属省份)
、language(微信客户端内设置的语言类型)
、gender(用户性别)
、country(所在国家)
、city(所在城市)
、avatarUrl(微信头像地址)
sha1(rawData + session_key)
计算后的值,sha1
则是一种密码的哈希函数,相比于md5
哈希函数来说抗攻击性更强。前面给大家讲到在客户端内调用getUserInfo
API时,微信客户端会向微信服务端发送一条请求,在微信开发者工具里通过 http请求抓包可以看到,发出了一条https://servicewechat.com/wxa-dev-logic/jsoperatewxdata
这样的http请求。
请求体里携带了几个重要的参数,包括data
, grant_type
等,data字段是一个JSON字符串,里面有一个字段api_name
,其值为'webapi_userinfo'。而grant_type字段也对应了一个值“webapi_userinfo”。
响应体返回了一个JSON对象,首先是一个baseresponse
字段,里面包含了接口调用的返回码errcode
和调用结果errmsg
。该对象还返回了一个data
字段,这个data字段对应了一个JSON字符串,里面就是通过调用API拿到的所有用户数据信息。在开发者工具内,我们还可以看到返回了一个debug_info
字段,这个里面同样包含了用户的数据data
,只不过这里的data
还返回了用户的openid,同时还返回了用户的session_key登录态凭据。
一般我们可以在开发者工具内通过抓包,来调试一些信息的有效性,包括用户的session_key
和openid
。
上面我们说过,在小程序里通过API获取到的用户完整信息encryptedData
,是需要通过AES-128-CBC
算法来加解密的。首先我们先来了解什么是AES-128-CBC
:
AES 全称为 Advanced Encryption Standard,是美国国家标准与技术研究院(NIST)在2001年建立了电子数据的加密规范,它是一种分组加密标准,每个加密块大小为128位,允许的密钥长度为128、192和256位。
分组加密有五种模式,分别是
ECB(Electronic Codebook Book) 电码本模式
CBC(Cipher Block Chaining) 密码分组链接模式
CTR(Counter) 计算器模式
CFB(Cipher FeedBack) 密码反馈模式
OFB(Output FeedBack) 输出反馈模式
这里我们主要来看AES-128-CBC
的分组加密算法,即用同一组key进行明文和密文的转换,以128bit为一组,128bit也就是16byte,那么明文的每16字节为一组就对应了加密后的16字节的密文。如果最后剩余的明文不够16字节时,就需要进行填充了,通常会采用PKCS#7
(PKCS#5仅支持填充8字节的数据块,而PKCS#7支持1-255之间的字节块)来进行填充。
如果最后剩余的明文为13个字节,也就是缺少了3个字节才能为一组,那么这个时候就需要填充3个字节的0x03:
明文数据: 05 05 05 05 05 05 05 05 05 05 05 05 05
PKCS#7填充: 05 05 05 05 05 05 05 05 05 05 05 05 05 03 03 03
若明文正好是16个字节的整数倍,最后要再加入一个16字节0x10的组再进行加密。
因此,我们发现PKCS#7填充的两个特点:
我们再来一起看明文加密的过程,CBC模式对于每个待加密的密码块在加密前会先与前一个密码块的密文进行异或运算,然后将得到的结果再通过加密器加密,其中第一个密码块会与我们前文所述的iv初始化向量
的数据块进行异或运算。如下图(图片来自wiki百科):
1
但是需要明确说明的是,这里API返回的iv
是解密算法对应的初始化向量,而非加密算法对应的初始化向量。所以大家肯定也就猜到了,CBC模式解密时第一个密码块也是需要和初始化向量进行异或运算的。如下图(图片来自wiki百科):
2
在小程序里,这里加密和解密的密码器为我们上一篇文章所获取到的经过base64编码的session_key
。
那么在前面我们大致了解了小程序中是如何对用户数据进行加密的之后,我们就一起以nodejs为例来看看如何在服务端对用户数据进行解密,以及解密后的数据完整性校验:
在util.js文件中,定义了两个方法:
decryptByAES
方法是利用服务端在登录时通过微信提供的jscode2session
接口拿到的session_key
和调用wx.getUserInfo后将返回的iv
初始化向量来解密encryptedData
。
encryptedBySha1
方法是通过sha1
哈希算法来加密session_key生成小程序应用自身的用户登录态标识,保证session_key的安全性。
// util.js
const crypto = require('crypto');
module.exports = {
decryptByAES: function (encrypted, key, iv) {
encrypted = new Buffer(encrypted, 'base64');
key = new Buffer(key, 'base64');
iv = new Buffer(iv, 'base64');
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv)
let decrypted = decipher.update(encrypted, 'base64', 'utf8')
decrypted += decipher.final('utf8');
return decrypted
},
encryptBySha1: function (data) {
return crypto.createHash('sha1').update(data, 'utf8').digest('hex')
}
};
在auth.js文件中,调用了上篇文章里的getSessionKey
方法,获取用户的openid
和session_key
,拿到这两者后,对加密的用户数据进行解密操作,同时将解密后的用户数据及用户的session_key和skey存入数据表中。
这里需要注意到一点:如果当前小程序绑定了开放平台的移动应用或网站应用,或公众平台的公众号等,那么encryptedData
还会多返回一个unionId
的字段,这个unionId可在小程序和其他已绑定的平台之间区分用户的唯一性,也就是说同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。一般,我们可以用unionId来打通小程序和其他应用之间的用户登录态。
// auth.js
const { decryptByAES, encryptBySha1 } = require('../util');
return getSessionKey(code, appid, secret)
.then(resData => {
// 选择加密算法生成自己的登录态标识
const { session_key } = resData;
const skey = encryptBySha1(session_key);
let decryptedData = JSON.parse(decryptByAES(encryptedData, session_key, iv));
// 存入用户数据表中
return saveUserInfo({
userInfo: decryptedData,
session_key,
skey
})
})
.catch(err => {
return {
result: -10003,
errmsg: JSON.stringify(err)
}
})
当我们通过解密拿到用户的完整数据后,可以对拿到的数据进行数据的完整性和有效性校验,防止用户数据被恶意篡改。这里说明如何进行相关的数据校验:
有效性校验:在前面我们介绍到,当withCredentials
设置为true时,返回的数据还会带上一个signature
的字段,其值是sha1(rawData + session_key)
的结果,开发者可以将所拿到的signature,在自己服务端使用相同的sha1算法算出对应的signature2,即
signature2 = encryptedBySha1(rawData + session_key);
通过对比signature与signature2是否一致,来确定用户数据的完整性。
完整性校验:在前面拿到的encryptedData
并进行相关解密操作后,会看到用户数据的object对象里存在一个watermark
的字段,官方称之为数据水印,这个字段结构为:
"watermark": {
"appid":"APPID",
"timestamp":TIMESTAMP
}
这里开发同学可以校验watermark内的appid和自身appid是否一致,以及watermark内的数据获取的timestamp时间戳,来校验数据的时效性。
那么上面就是小程序中如何对用户数据进行加解密操作,以及如何对用户数据进行相关处理和校验的介绍,请大家多多指教!
腾讯IVWEB团队的工程化解决方案feflow已经开源:Github主页:feflow 如果对您的团队或者项目有帮助,请给个Star支持一下哈~
参考文章: