开发微信小程序时,接入小程序的受权登陆能够快速实现用户注册登陆的步骤,是快速创建用户体系的重要一步。这篇文章将介绍 python + sanic + 微信小程序实现用户快速注册登陆全栈方案。javascript
微信小程序登陆时序图以下:html
这个流程分为两大部分:java
下面咱们先看一下小程序提供的 API。python
在这个受权登陆的过程当中,用到的 API 以下:git
wx.chekSession
是可选的,这里并无用到。github
调用此接口能够获取登陆凭证(code),以用来换取用户登陆态信息,包括用户的惟一标识(openid) 及本次登陆的 会话密钥(session_key)。算法
若是接口调用成功,返回结果以下:数据库
参数名 | 类型 | 说明 |
---|---|---|
errMsg | String | 调用结果 |
code | String | 用户容许登陆后,回调内容会带上 code(有效期五分钟),开发者须要将 code 发送到开发者服务器后台,使用code 换取 session_key api,将 code 换成 openid 和 session_key |
开发者服务器使用登陆凭证 code 获取 session_key 和 openid。其中 session_key 是对用户数据进行加密签名的密钥。为了自身应用安全,session_key 不该该在网络上传输。因此这一步应该在服务器端实现。json
此接口用来获取用户信息。小程序
当
withCredentials
为 true 时,要求此前有调用过 wx.login 且登陆态还没有过时,此时返回的数据会包含 encryptedData, iv 等敏感信息;当 withCredentials 为 false 时,不要求有登陆态,返回的数据不包含 encryptedData, iv 等敏感信息。
接口success 时返回参数以下:
参数名 | 类型 | 说明 |
---|---|---|
userInfo | OBJECT | 用户信息对象,不包含 openid 等敏感信息 |
rawData | String | 不包括敏感信息的原始数据字符串,用于计算签名。 |
signature | String | 使用 sha1( rawData + sessionkey ) 获得字符串,用于校验用户信息,参考文档 signature。 |
encryptedData | String | 包括敏感数据在内的完整用户信息的加密数据,详细见加密数据解密算法 |
iv | String | 加密算法的初始向量,详细见加密数据解密算法 |
encryptedData
解密后为如下 json 结构,详见加密数据解密算法
{
"openId": "OPENID",
"nickName": "NICKNAME",
"gender": GENDER,
"city": "CITY",
"province": "PROVINCE",
"country": "COUNTRY",
"avatarUrl": "AVATARURL",
"unionId": "UNIONID",
"watermark":
{
"appid":"APPID",
"timestamp":TIMESTAMP
}
}复制代码
因为解密 encryptedData 须要 session_key 和 iv 因此,在给服务器端发送受权验证的过程当中须要将 code、encryptedData 和 iv 一块儿发送。
服务器端受权须要提供两个 API:
开始受权时,小程序调用此 API 尝试换取jwt,若是用户未注册返回401,若是用户发送参数错误,返回403。
接口 获取 jwt 成功时返回参数以下:
参数名 | 类型 | 说明 |
---|---|---|
account_id | string | 当前受权用户的用户 ID |
access_token | string | jwt(登陆流程中的第三方 session_key |
token_type | string | token 类型(固定Bearer) |
小程序受权后应该先调用此接口,若是结果是用户未注册,则应该调用新用户注册的接口先注册新用户,注册成功后再调用此接口换取 jwt。
注册新用户时,服务器端须要存储当前用户的 openid,因此和受权接口同样,请求时须要的参数为 code、encryptedData 和 iv。
注册成功后,将返回用户的 ID 和注册时间。此时,应该再次调用获取 token 的接口去换取第三方 token,以用来下次登陆。
接口定义好以后,来看下先后端总体的受权登陆流程。
这个流程须要注意的是,在 C 步(使用 code 换取 session )以后咱们获得 session_key,而后须要用 session_key 解密获得用户数据。
而后使用 openid 判断用户是否已经注册,若是用户已经注册,生成 jwt 返回给小程序。
若是用户未注册返回401, 提示用户未注册。
jwt(3rd_session)
用于第三方服务器和小程序之间作登陆态校验,为了保证安全性,jwt 应该知足:
- 足够长。建议有 2^128 组合
- 避免使用 srand(当前时间),而后 rand() 的方法,而是采用操做系统提供的真正随机数机制。
- 设置必定的有效时间,
固然,在小程序中也可使用手机号登陆,不过这是另外一个功能了,就不在这里叙述了。
说了这么多,接下来看代码吧。
代码逻辑为:
为了简便,这里在小程序 启动的时候就请求受权。代码实现以下。
//app.js
var config = require('./config.js')
App({
onLaunch: function() {
//调用API从本地缓存中获取数据
var jwt = wx.getStorageSync('jwt');
var that = this;
if (!jwt.access_token){ //检查 jwt 是否存在 若是不存在调用登陆
that.login();
} else {
console.log(jwt.account_id);
}
},
login: function() {
// 登陆部分代码
var that = this;
wx.login({
// 调用 login 获取 code
success: function(res) {
var code = res.code;
wx.getUserInfo({
// 调用 getUserInfo 获取 encryptedData 和 iv
success: function(res) {
// success
that.globalData.userInfo = res.userInfo;
var encryptedData = res.encryptedData || 'encry';
var iv = res.iv || 'iv';
console.log(config.basic_token);
wx.request({ // 发送请求 获取 jwt
url: config.host + '/auth/oauth/token?code=' + code,
header: {
Authorization: config.basic_token
},
data: {
username: encryptedData,
password: iv,
grant_type: "password",
auth_approach: 'wxapp',
},
method: "POST",
success: function(res) {
if (res.statusCode === 201) {
// 获得 jwt 后存储到 storage,
wx.showToast({
title: '登陆成功',
icon: 'success'
});
wx.setStorage({
key: "jwt",
data: res.data
});
that.globalData.access_token = res.data.access_token;
that.globalData.account_id = res.data.sub;
} else if (res.statusCode === 401){
// 若是没有注册调用注册接口
that.register();
} else {
// 提示错误信息
wx.showToast({
title: res.data.text,
icon: 'success',
duration: 2000
});
}
},
fail: function(res) {
console.log('request token fail');
}
})
},
fail: function() {
// fail
},
complete: function() {
// complete
}
})
}
})
},
register: function() {
// 注册代码
var that = this;
wx.login({ // 调用登陆接口获取 code
success: function(res) {
var code = res.code;
wx.getUserInfo({
// 调用 getUserInfo 获取 encryptedData 和 iv
success: function(res) {
// success
that.globalData.userInfo = res.userInfo;
var encryptedData = res.encryptedData || 'encry';
var iv = res.iv || 'iv';
console.log(iv);
wx.request({ // 请求注册用户接口
url: config.host + '/auth/accounts/wxapp',
header: {
Authorization: config.basic_token
},
data: {
username: encryptedData,
password: iv,
code: code,
},
method: "POST",
success: function(res) {
if (res.statusCode === 201) {
wx.showToast({
title: '注册成功',
icon: 'success'
});
that.login();
} else if (res.statusCode === 400) {
wx.showToast({
title: '用户已注册',
icon: 'success'
});
that.login();
} else if (res.statusCode === 403) {
wx.showToast({
title: res.data.text,
icon: 'success'
});
}
console.log(res.statusCode);
console.log('request token success');
},
fail: function(res) {
console.log('request token fail');
}
})
},
fail: function() {
// fail
},
complete: function() {
// complete
}
})
}
})
},
get_user_info: function(jwt) {
wx.request({
url: config.host + '/auth/accounts/self',
header: {
Authorization: jwt.token_type + ' ' + jwt.access_token
},
method: "GET",
success: function (res) {
if (res.statusCode === 201) {
wx.showToast({
title: '已注册',
icon: 'success'
});
} else if (res.statusCode === 401 || res.statusCode === 403) {
wx.showToast({
title: '未注册',
icon: 'error'
});
}
console.log(res.statusCode);
console.log('request token success');
},
fail: function (res) {
console.log('request token fail');
}
})
},
globalData: {
userInfo: null
}
})复制代码
服务端使用 sanic
框架 + swagger_py_codegen
生成 rest-api。
数据库使用 MongoDB,python-weixin
实现了登陆过程当中 code 换取 session_key 以及 encryptedData 解密的功能,因此使用python-weixin 做为 python 微信 sdk 使用。
为了过滤无效请求,服务器端要求用户在获取 token 或受权时在 header 中带上
Authorization
信息。Authorization
在登陆前使用的是 Basic 验证(格式 (Basic hashkey) 注 hashkey为client_id + client_secret 作BASE64处理),只是用来校验请求的客户端是否合法。不过Basic 基本等同于明文,并不能用它来进行严格的受权验证。jwt 原理及使用参见 理解JWT(JSON Web Token)认证及实践
使用 swagger 生成代码结构以下:
因为代码太长,这里只放获取 jwt 的逻辑:
def get_wxapp_userinfo(encrypted_data, iv, code):
from weixin.lib.wxcrypt import WXBizDataCrypt
from weixin import WXAPPAPI
from weixin.oauth2 import OAuth2AuthExchangeError
appid = Config.WXAPP_ID
secret = Config.WXAPP_SECRET
api = WXAPPAPI(appid=appid, app_secret=secret)
try:
# 使用 code 换取 session key
session_info = api.exchange_code_for_session_key(code=code)
except OAuth2AuthExchangeError as e:
raise Unauthorized(e.code, e.description)
session_key = session_info.get('session_key')
crypt = WXBizDataCrypt(appid, session_key)
# 解密获得 用户信息
user_info = crypt.decrypt(encrypted_data, iv)
return user_info
def verify_wxapp(encrypted_data, iv, code):
user_info = get_wxapp_userinfo(encrypted_data, iv, code)
# 获取 openid
openid = user_info.get('openId', None)
if openid:
auth = Account.get_by_wxapp(openid)
if not auth:
raise Unauthorized('wxapp_not_registered')
return auth
raise Unauthorized('invalid_wxapp_code')
def create_token(request):
# verify basic token
approach = request.json.get('auth_approach')
username = request.json['username']
password = request.json['password']
if approach == 'password':
account = verify_password(username, password)
elif approach == 'wxapp':
account = verify_wxapp(username, password, request.args.get('code'))
if not account:
return False, {}
payload = {
"iss": Config.ISS,
"iat": int(time.time()),
"exp": int(time.time()) + 86400 * 7,
"aud": Config.AUDIENCE,
"sub": str(account['_id']),
"nickname": account['nickname'],
"scopes": ['open']
}
token = jwt.encode(payload, 'secret', algorithm='HS256')
# 因为 account 中 _id 是一个 object 须要转化成字符串
return True, {'access_token': token, 'account_id': str(account['_id'])}复制代码
具体代码能够在 Metis:https://github.com/gusibi/Metis 查看。
Note
: 若是试用代码,请先设定 oauth2_client,使用本身的配置。不要将私密配置信息提交到 github。
最后,感谢女友支持。
欢迎关注(April_Louisa) | 请我喝芬达 |
---|---|
![]() |
![]() |