本文继续学习下微信小程序登录相关的内容。
普通用户视角:对于每个小程序,微信都会将用户的微信ID映射出一个小程序OpenID,作为这个用户在这个小程序的唯一标识。(注意:同一微信用户在不同小程序的openid不同。)
开发者视角:对于拥有多个小程序的开发者,开发者可以注册一个微信开放平台账号,然后把所有小程序绑定在这一个开放平台下,这样的话微信还会将微信ID映射出一个UnionID,作为这个用户在整个开放平台的唯一ID。(注:该能力不单限于小程序,所有公众号、网站、移动APP,只要使用微信开放能力登录的应用,都能获取到unionid,这样就能打通各个平台的用户账号体系了。)
说明
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意事项
session_key
是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。以上是官方给出的登录时序说明,要理解上述内容,还需要学习其中涉及的很多细节,并实际编写代码运行体会。
获取用户登录凭证(code),有效期五分钟。
开发者需要在开发者服务器后台调用 code2Session,使用 code 换取 openid、unionid、session_key 等信息。
OpenID:用户在当前小程序的唯一标识。
UnionID:微信开放平台账号下的唯一标识。(若当前小程序已绑定到微信开放平台账号,没绑定的话没有)
sesion_key:本次登录的会话密钥。
wx.login({
success (res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://example.com/onLogin',
data: {
code: res.code
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
获取用户当前设置。返回值中只会出现小程序已经向用户请求过的权限。
可获取用户当前的授权状态。
属性 | 类型 | 说明 | 最低版本 |
---|---|---|---|
authSetting | AuthSetting | 用户授权结果 | |
subscriptionsSetting | SubscriptionsSetting | 用户订阅消息设置,接口参数withSubscriptions 值为true 时才会返回。 | 2.10.1 |
miniprogramAuthSetting | AuthSetting | 在插件中调用时,当前宿主小程序的用户授权结果 |
用户授权设置信息
属性 | 作用 | 接口 |
---|---|---|
boolean scope.userInfo | 是否授权用户信息 | wx.getUserInfo |
boolean scope.userLocation | 是否授权精确地理位置 | wx.getLocation, wx.chooseLocation |
boolean scope.userFuzzyLocation | 是否授权模糊地理位置 | wx.getFuzzyLocation |
boolean scope.address | 是否授权通讯地址,已取消此项授权,会默认返回true | |
boolean scope.invoiceTitle | 是否授权发票抬头,已取消此项授权,会默认返回true | |
boolean scope.invoice | 是否授权获取发票,已取消此项授权,会默认返回true | |
boolean scope.werun | 是否授权微信运动步数 | wx.getWeRunData |
boolean scope.record | 是否授权录音功能 | wx.startRecord |
boolean scope.writePhotosAlbum | 是否授权保存到相册 | wx.saveImageToPhotosAlbum, wx.saveVideoToPhotosAlbum |
boolean scope.camera | 是否授权摄像头 | camera ((camera)) 组件 |
boolean scope.bluetooth | 是否授权蓝牙 | wx.openBluetoothAdapter、wx.createBLEPeripheralServer |
boolean scope.addPhoneContact | 是否添加通讯录联系人 | wx.addPhoneContact |
boolean scope.addPhoneCalendar | 是否授权系统日历 | wx.addPhoneRepeatCalendar、wx.addPhoneCalendar |
提前向用户发起授权请求。调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口。如果用户之前已经同意授权,则不会出现弹窗,直接返回成功。
getSetting(e){
wx.getSetting({
success: (res) => {
console.log(res)
if (!res.authSetting['scope.record']){
wx.authorize({
scope: 'scope.record',
success: ()=>{
const options = {
duration: 10000,
sampleRate: 44100,
numberOfChannels: 1,
encodeBitRate: 192000,
format: 'aac',
frameSize: 50
}
wx.getRecorderManager().start(options)
}
})
}
}
})
},
授权后再调用wx.getSetting就可以看到scope.record为true了。
小程序可以通过各种前端接口获取微信提供的开放数据。考虑到开发者服务端也需要获取这些开放数据,微信提供了两种获取方式:
微信会对这些开放数据做签名和加密处理。开发者后台拿到开放数据后可以对数据进行校验签名和解密,来保证数据不被篡改。
签名校验以及数据加解密涉及用户的会话密钥 session_key。 开发者应该事先通过 code2Session 登录流程获取会话密钥 session_key 并保存在服务器。为了数据不被篡改,开发者不应该把 session_key 传到小程序客户端等服务器外的环境。
为了确保开放接口返回用户数据的安全性,微信会对明文数据进行签名。开发者可以根据业务需要对数据包进行签名校验,确保数据的完整性。
接口如果涉及敏感数据(如 openId 和 unionId),接口的明文内容将不包含这些敏感数据。开发者如需要获取敏感数据,需要对接口返回的加密数据(encryptedData) 进行对称解密。 解密算法如下:
另外,为了应用能校验数据的有效性,会在敏感数据加上数据水印( watermark )。
watermark参数说明:
参数 | 类型 | 说明 |
---|---|---|
appid | String | 敏感数据归属 appId,开发者可校验此参数与自身 appId 是否一致 |
timestamp | Int | 敏感数据获取的时间戳, 开发者可以用于数据时效性校验 |
开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。
也就是说,在小程序服务端,通过验签和解密可以获取到敏感数据(如用户信息等)。
检查登录状态是否过期。
通过 wx.login 接口获得的用户登录态拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效。具体时效逻辑由微信维护,对开发者透明。开发者只需要调用 wx.checkSession 接口检测当前用户登录态是否有效。
微信小程序的登录流程大体上涉及三个部分:小程序客户端、小程序服务端(开发者自行搭建)、微信服务端。
小程序客户端通过wx.login获取临时登录凭证code,然后通过wx.request将code发送给小程序服务端,小程序服务端再通过code2session接口获取OpenId、session_key等。到这里为止,其实微信小程序本身的登录已经完成了,后续的流程只是小程序应用根据实际需求来对用户进行管理,这个部分就看小程序应用的开发者依据项目需求实际进行开发了。
之后小程序服务端可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意事项
session_key
是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,小程序服务端不应该把会话密钥下发到小程序应用端,也不应该对外提供这个密钥。<!--pages/index/index.wxml-->
<view class="container">
<view class="userinfo">
<block wx:if="{{!hasUserInfo}}">
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
</block>
<block wx:else>
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>
<input type="nickname" placeholder="请输入昵称"/>
</view>
<button bindtap="openSetting"> 打开设置页 </button>
<button bindtap="getSetting">获取设置</button>
</view>
// pages/index/index.js
Page({
/**
* 页面的初始数据
*/
data: {
userInfo: {},
hasUserInfo: false,
canIUseGetUserProfile: false,
code:''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
wx.login({
success: (resp) => {
console.log("code:", resp)
this.setData({
code:resp.code
})
}
})
},
getSetting(e){
wx.getSetting({
success: (res) => {
console.log(res)
/*
if (!res.authSetting['scope.record']){
wx.authorize({
scope: 'scope.record',
success: ()=>{
const options = {
duration: 10000,
sampleRate: 44100,
numberOfChannels: 1,
encodeBitRate: 192000,
format: 'aac',
frameSize: 50
}
wx.getRecorderManager().start(options)
}
})
}
*/
}
})
},
openSetting(e){
wx.openSetting({
success: (res) => {
console.log(res)
}
})
},
getUserProfile(e) {
wx.getUserProfile({
desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
console.log("success:", res)
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
wx.request({
url: 'http://127.0.0.1:5000/login',
data: {
code: this.data.code,
encryptedData: res.encryptedData,
iv: res.iv
},
method: "POST",
success(res) {
console.log(res);
},
fail(res) {
console.log(res)
}
})
},
fail: (res) => {
console.log("fail:", res)
}
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})
点击“获取头像昵称”按钮后,可以从小程序服务端获取到userinfo,带watermark。
import requests
from flask import Flask, jsonify, request
from WXBizDataCrypt import WXBizDataCrypt
app = Flask(__name__)
@app.route("/login", methods = ['POST'])
def login():
data = request.get_json()
print(data)
appID = '你自己的appID' #开发者关于微信小程序的appID
appSecret = '你自己的appSecret' #开发者关于微信小程序的appSecret
code = data['code'] #前端POST过来的微信临时登录凭证code
encryptedData = data['encryptedData']
iv = data['iv']
req_params = {
'appid': appID,
'secret': appSecret,
'js_code': code,
'grant_type': 'authorization_code'
}
wx_login_api = 'https://api.weixin.qq.com/sns/jscode2session'
response_data = requests.get(wx_login_api, params=req_params) #向API发起GET请求
data2 = response_data.json()
print(data2)
if (data2['openid']):
openid = data2['openid'] #得到用户关于当前小程序的OpenID
session_key = data2['session_key'] #得到用户关于当前小程序的会话密钥session_key
pc = WXBizDataCrypt(appID, session_key) #对用户信息进行解密
userinfo = pc.decrypt(encryptedData, iv) #获得用户信息
print(userinfo)
else:
data = "get openid failed"
return jsonify(result = data)
if __name__ == '__main__':
app.run()
import base64
import json
from Crypto.Cipher import AES
class WXBizDataCrypt:
def __init__(self, appId, sessionKey):
self.appId = appId
self.sessionKey = sessionKey
def decrypt(self, encryptedData, iv):
# base64 decode
sessionKey = base64.b64decode(self.sessionKey)
encryptedData = base64.b64decode(encryptedData)
iv = base64.b64decode(iv)
cipher = AES.new(sessionKey, AES.MODE_CBC, iv)
decrypted = json.loads(self._unpad(cipher.decrypt(encryptedData)))
if decrypted['watermark']['appid'] != self.appId:
raise Exception('Invalid Buffer')
return decrypted
def _unpad(self, s):
return s[:-ord(s[len(s)-1:])]