开发微信小程序时,接入小程序的授权登录可以快速实现用户注册登录的步骤,是快速建立用户体系的重要一步。这篇文章将介绍 python + sanic + 微信小程序实现用户快速注册登录全栈方案。

微信小程序登录时序图如下:

这个流程分为两大部分:

  1. 小程序使用 wx.login() API 获取 code,调用 wx.getUserInfo() API 获取 encryptedData 和 iv,然后将这三个信息发送给第三方服务器。

  2. 第三方服务器获取到 code、encryptedData和 iv 后,使用 code 换取 session_key,然后将 session_key 利用 encryptedData 和 iv 解密在服务端获取用户信息。根据用户信息返回 jwt 数据,完成登录。

下面我们先看一下小程序提供的 API。

小程序登录 API

在这个授权登录的过程中,用到的 API 如下:

  • wx.login

  • wx.getUserInfo

wx.chekSession 是可选的,这里并没有用到。

wx.login(OBJECT)

调用此接口可以获取登录凭证(code),以用来换取用户登录态信息,包括用户的唯一标识(openid) 及本次登录的 会话密钥(session_key)。

如果接口调用成功,返回结果如下:

参数名 类型 说明
errMsg String 调用结果
code String 用户允许登录后,回调内容会带上 code(有效期五分钟),开发者需要将 code 发送到开发者服务器后台,使用code 换取 session_key api,将 code 换成 openid 和 session_key

code 换取 session_key

开发者服务器使用登录凭证 code 获取 session_key 和 openid。其中 session_key 是对用户数据进行加密签名的密钥。为了自身应用安全,session_key 不应该在网络上传输。所以这一步应该在服务器端实现。

wx.getUserInfo

此接口用来获取用户信息。

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:

  1. /oauth/token 通过小程序提供的验证信息获取服务器自己的 token

  2. /accounts/wxapp 如果登录用户是未注册用户,使用此接口注册为新用户。

换取第三方 token(/oauth/token)

开始授权时,小程序调用此 API 尝试换取jwt,如果用户未注册返回401,如果用户发送参数错误,返回403。

接口 获取 jwt 成功时返回参数如下:

参数名 类型 说明
account_id string 当前授权用户的用户 ID
access_token string jwt(登录流程中的第三方 session_key
token_type string token 类型(固定Bearer)

小程序授权后应该先调用此接口,如果结果是用户未注册,则应该调用新用户注册的接口先注册新用户,注册成功后再调用此接口换取 jwt。

新用户注册(/accounts/wxapp)

注册新用户时,服务器端需要存储当前用户的 openid,所以和授权接口一样,请求时需要的参数为 code、encryptedData 和 iv。

注册成功后,将返回用户的 ID 和注册时间。此时,应该再次调用获取 token 的接口去换取第三方 token,以用来下次登录。

实现流程

接口定义好之后,来看下前后端整体的授权登录流程。

这个流程需要注意的是,在 C 步(使用 code 换取 session )之后我们得到 session_key,然后需要用 session_key 解密得到用户数据。

然后使用 openid 判断用户是否已经注册,如果用户已经注册,生成 jwt 返回给小程序。
如果用户未注册返回401, 提示用户未注册。

jwt(3rd_session) 用于第三方服务器和小程序之间做登录态校验,为了保证安全性,jwt 应该满足:

  1. 足够长。建议有 2^128 组合

  2. 避免使用 srand(当前时间),然后 rand() 的方法,而是采用操作系统提供的真正随机数机制。

  3. 设置一定的有效时间,

当然,在小程序中也可以使用手机号登录,不过这是另一个功能了,就不在这里叙述了。

代码实现

说了这么多,接下来看代码吧。

小程序端代码

代码逻辑为:

  1. 用户在小程序授权

  2. 小程序将授权消息发送到服务器,服务器检查用户是否已经注册,如果注册返回 jwt,如果没注册提示用户未注册,然后小程序重新请求注册接口,注册用户,注册成功后重复这一步。

为了简便,这里在小程序 启动的时候就请求授权。代码实现如下。

//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) 请我喝芬达

微信小程序开发:python+sanic 实现小程序登录注册的更多相关文章

  1. 微信小程序开发教程 #043 - 在小程序开发中使用 npm

    本文介绍了如何在微信小程序开发中使用 npm 中包的功能,大大提高微信小程序的开发效率,同时也是微信小程序系列教程的视频版更新. 微信小程序在发布之初没有对 npm 的支持功能,这也是目前很多前端开发 ...

  2. 微信小程序--关于加快小程序开发的几个小建议

    加快小程序开发的几个小建议 1.使用 app.json创建页面 ​ 按照我们平常的开发习惯,创建一个新的页面,一般都会先创建文件夹,再创建对应page的形式,创建完成后,app.json中会自动注册该 ...

  3. Win32 程序开发:创建一个应用程序窗口

    一.创建一个应用程序窗口 代码如下: // 头文件 #include <windows.h> // 全局变量 WCHAR g_lpszClassName[] = L"CLASSN ...

  4. 微信小程序开发公测,小程序账号申请办法攻略

    11月3号晚上 10 点,微信公众平台发布公告,宣布微信小程序正式开放公测.此次小程序公测允许开发者将产品提交至微信公众平台审核,但是暂时不支持发布,也就是说普通消费者若想体验小程序,还需要等待一段时 ...

  5. 微信小程序开发——前端如何区分小程序运行环境

    前言: 之前用vue做h5项目,对于接口请求,都是根据前端访问域名来判断运行环境,然后自动适配对应的服务器地址的.这样的好处就是在开发.测试及发布上线全程都不需要手动去改接口请求地址,只要提前配置好就 ...

  6. 微信小程序开发——打开另一个小程序

    微信小程序打开另一个小程序,有两种方法:1.超链接:2.点击按钮. 全局配置: 跳转到其他小程序,需要在当前小程序全局配置中配置需要跳转的小程序列表,代码如下: App.json { ... &quo ...

  7. 微信公众号开发系统入门教程(公众号注册、开发环境搭建、access_token管理、Demo实现、natapp外网穿透)

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/a1786223749/article/ ...

  8. 微信小程序开发入门学习(2):小程序的布局

    概述 小程序的布局采用了和Css3中相同的 flex(弹性布局)方式,使用方法也类似(只是属性名不同而已). 水平排列 默认是从左向右水平依次放置组件,从上到下依次放置组件. 任何可视组件都需要使用样 ...

  9. 微信小程序开发-rem转换rpx小工具

    实现原理: 对样式进行格式化,然后根据 “rem” 进行拆分,这样就会拆分成一个数组 [str1,str2,str3...,str6], 除了最后一个元素,前边的元素都会以 “rem” 样式的数值结尾 ...

随机推荐

  1. 发现Spring事务的一个实锤bug,官方还拒不承认?你来评评理...

    你好呀,我是歪歪. 事情是这样的,上周我正在全神贯注的摸鱼,然后有个小伙伴给我发来微信消息,提出了自己关于事务的一个疑问,并配上两段代码: 先说结论:我认为这是 Spring 事务的一个 bug.但是 ...

  2. FoveaBox:细节差别,另一种DenseBox+FPN的Anchor-free方案 | IEEE TIP 2020

    作为与FCOS和FSAF同期的Anchor-free论文,FoveaBox在整体结构上也是基于DenseBox加FPN的策略,主要差别在于FoveaBox只使用目标中心区域进行预测且回归预测的是归一化 ...

  3. FaE:基于符号知识的适应性和可解释的神经记忆

    原创作者 | 朱林 论文解读: Facts as Experts: Adaptable and Interpretable Neural Memory over Symbolic Knowledge ...

  4. 基于消息队列(RabbitMQ)实现延迟任务

    一.序言 延迟任务应用广泛,延迟任务典型应用场景有订单超时自动取消:支付回调重试.其中订单超时取消具有幂等性属性,无需考虑重复消费问题:支付回调重试需要考虑重复消费问题. 延迟任务具有如下特点:在未来 ...

  5. Tableau绘制K线图、布林线、圆环图、雷达图

    Tableau绘制K线图.布林线.圆环图.雷达图 本文首发于博客冰山一树Sankey,去博客浏览效果更好.直接右上角搜索该标题即可 一. K线图 1.1 导入数据源 1.2 拖拽字段 将[日期]托到列 ...

  6. 从零开始Pytorch-YOLOv3【笔记】(一)配置文件解读

    前言 这是github上的一个项目YOLO_v3_tutorial_from_scratch,它还有相应的blog对其详细的解读.机器之心翻译了他的tutorial:从零开始PyTorch项目:YOL ...

  7. CDN网络科普小文(小说版)

    引言 作为公司 cdn 小组的一名小码仔,我为写一篇 cdn 的科普文章准备了好一段时间(大概有一个多月没有更新我的社交账号了). 在我刚进入公司,培训完,进入小组,了解到我们做的是 cdn 相关的工 ...

  8. Netty学习(四)FastThreadLocal

    FastThreadLocal 前面介绍过 JDK 的 ThreadLocal , 使用不当的话容易造成内存泄漏最终导致OOM, 并且也有一些地方设计的不够好(相对于接下来要介绍的 FastThrea ...

  9. webug4.0 打靶笔记-02【完结】

    webug4.0打靶笔记-02 3. 延时注入(时间盲注) 3.1 访问靶场 3.2 寻找注入点 貌似一样的注入点: ?id=1' --+ 3.3 判断输出位置 同前两关一样的位置,时间盲注应该不是这 ...

  10. 2.2 C++STL string容器详解

    文章目录 引言 2.2.1 string的特性 2.2.2 string用法理论 2.2.2.1 string构造函数 2.2.2.2 string赋值操作 2.2.2.3 string取值操作 2. ...