小程序中神秘的用户数据
在上一篇文章中,我教了你如何手工登录和验证小程序,并介绍了如何登录和验证小程序。然后可以使用上面提到的微信提供的jscode2session接口来交换通用小程序的用户ID,小程序还提供了一个getUserInfo的API来获取用户数据,其中也可以包含当前的用户ID openid。本文详细介绍了如何在小程序中获取用户数据和进行数据完整性检查。
API介绍
wx.getUserInfo是一个获取用户信息的API接口。以下是相应的参数字段:
lang 指定返回用户信息的语言,有三个值:lang
zh_CN简体中文zh_TW繁体中文en English,默认为en
timeout
超时指定API调用的超时。实际上,getUserInfoAPI的底层是发起http请求获取用户相关数据的客户端,这些数据被封装后返回给applet,后面会详细介绍。
withCredentials
withCredentials这个字段是一个布尔值,它决定了小程序返回的数据在调用API时是否会有登录状态信息。如果留空,此字段的默认值为真。
然后,应用编程接口返回以下结果:
EncryptedData是包括敏感数据在内的完整用户信息的加密数据,涉及用户的openid和unionid等。然后数据加密算法是AES-128-CBC块对称加解密算法,后面我们会详细分析。如果该字段的值为false,则不会返回上述两个字段:encryptedData,iv。
Iv是上述解密算法的算法初始向量。后面我们还会详细介绍。
RawData是一个对象字符串,包含用户的一些公开数据,即:昵称(微信昵称)、省份(省份)、语言(微信客户端设置的语言类型)、性别(用户性别)、国家(国家)、城市(城市)、avatarUrl(微信头像地址)。
签名为了保证数据的有效性和安全性,小程序对明文数据进行签名。该值由sha1(rawData session_key)计算,sha1是一个加密哈希函数,比md5哈希函数更能抵抗攻击。
UserInfo字段是一个对象,是用户的开放数据,与rawData显示的内容一致,只是rawData将对象序列化为字符串作为返回值。
API之http请求
我之前跟你说过,在客户端调用getUserInfoAPI的时候,微信客户端会向微信服务器发送请求。我们可以从微信开发者工具中的http请求包中看到,发送了一个像https://servicewechat.com/wxa-dev-logic/jsoperatewxdata这样的http请求。
请求体携带几个重要参数,包括数据、grant_type等。数据字段是一个JSON字符串,其字段api_name的值为“webapi_userinfo”。grant_type字段也对应于值“webapi_userinfo”。
响应主体返回一个JSON对象,首先是一个baseresponse字段,其中包含接口调用的返回代码errcode和调用结果errmsg。该对象还返回一个数据字段,该字段对应于一个JSON字符串,该字符串包含通过调用API获得的所有用户数据信息。在开发人员工具中,我们还可以看到返回了一个debug_info字段,其中也包含了用户的数据,只是这里的数据还返回了用户的openid和用户的session_key登录凭证。
通常,我们可以通过在开发工具中抓取包来调试一些信息的有效性,包括用户的session_key和openid。
AES-128-CBC 加密算法
正如我们上面所说,encryptedData,通过applet中的API获取的完整用户信息,需要通过AES-128-CBC算法进行加密解密。我们先来了解一下AES-128-CBC是什么:
AES被称为高级加密标准,
是美国国家标准与技术研究院(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 05PKCS#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百科)
但是需要明确说明的是,这里API返回的iv是解密算法对应的初始化向量,而非加密算法对应的初始化向量。所以大家肯定也就猜到了,CBC模式解密时第一个密码块也是需要和初始化向量进行异或运算的。如下图(图片来自wiki百科):
在小程序里,这里加密和解密的密码器为我们上一篇文章所获取到的经过base64编码的session_key。
小程序中的应用
那么在前面我们大致了解了小程序中是如何对用户数据进行加密的之后,我们就一起以nodejs为例来看看如何在服务端对用户数据进行解密,以及解密后的数据完整性校验:
在util.js文件中,定义了两个方法:
decryptByAES方法是利用服务端在登录时通过微信提供的jscode2session接口拿到的session_key和调用wx.getUserInfo后将返回的iv初始化向量来解密encryptedData。
encryptedBySha1方法是通过sha1哈希算法来加密session_key生成小程序应用自身的用户登录态标识,保证session_key的安全性。
// util.jsconst 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.jsconst { 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时间戳,来校验数据的时效性。
版权声明:小程序中神秘的用户数据是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。