前端开发者利用Strapi实现API自由 - 之微信小程序登录
背景
作为一名前端,要想自己一个人撸一个网站、APP或者小程序,如果没有后端合作还是有难度的。之前在微信生态可以用 微信云开发 CMS输出api,基本能实现自给自足,自己一个人能完成一些简单功能的项目,可这玩意积累了一定的用户量以后,几个月前开始收费啦!好吧,本着能白嫖就白嫖的原则,另寻他路吧。
正在思前想后不断纠结中,了解到了 Strapi - Open source Node.js Headless CMS 🚀 ,相信不少朋友用过,他上手方便、功能丰富、部署简单,非常吸引人,果断与微信云开发决裂,投入到strapi的怀抱。不过我今天要说的不是如何使用、入门stiapi,我要说的是如何将 strapi的用户管理系统和微信小程序的登录 结合起来,让我们在开发小程序的时候方便使用strapi的用户系统进行权限控制。
另,快速入门可以先看这里 Quick Start Guide - Strapi Developer Docs!
strapi 的用户系统
strapi 提供了基础的用户管理功能,用户可以进行注册、登录、修改密码等操作,不同版本的用户注册、登录调用方法可能有细微差别,现在以4.5.3版本为例进行演示,更为详细的文档请参考这里:Users & Permissions - Strapi Developer Docs。
用户注册
当我们正确安装、启动了 strapi 后就可以通过接口进行注册操作,如下:
# post
{{STRAPI_BACKEND_URL}}/api/auth/local/register
# body
{
"username":"tester",
"email":"tester@test.com",
"password":"testPassword"
}
# response
{
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNjcwNzI1NDQ3LCJleHAiOjE2NzMzMTc0NDd9.2fsTqlE2xdCwd9JVk9NQ7WK6i0V--nhpYV-r5EVmqhk",
"user": {
"id": 3,
"username": "tester",
# ......
}
}
注册成功以后,进入后台管理界面,就可以看到刚刚注册的用户:
一个用户注册的过程就结束了。
用户登录
登录过程如下:
# post
{{STRAPI_BACKEND_URL}}/api/auth/local
# body
{
"identifier":"tester@test.com",
"password":"testPassword"
}
# response
{
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNjcwNzI2Njg1LCJleHAiOjE2NzMzMTg2ODV9.EFL9akvV65tmu9PqHp4lQHhnTvx33fXiKN_geYbJfIg",
"user": {
"id": 3,
"username": "tester",
# ......
}
}
拿到token
后就可以存起来,用于后续的业务。
这里有一点要注意的是,登录用的identifier
可以是我们注册时的 username
,也可以是email
,其他无特殊之处。
Token 的使用
上文返回的 jwt token 可以用于受限资源的访问。在日常业务中,有些数据需要用户登录以后才可以访问,可以在后台的角色权限管理里对其进行配置,任何没有 token 的请求都被视为公共角色。
现在新建一个模型 Post,并以它为例。(新建的过程请参考 Quick Start Guide)
在用户权限里配置只有已授权的用户可以访问,如下:
这时,对于一个 header 里未带 token 的请求,将会返回一个 403
ForbiddenError,如下:
# get
{{STRAPI_BACKEND_URL}}/api/posts
# response
{
"data": null,
"error": {
"status": 403,
"name": "ForbiddenError",
"message": "Forbidden",
"details": {}
}
}
如果将正确的 token
添加到 header 里,就可以正常取得数据:
# get
{{STRAPI_BACKEND_URL}}/api/posts
# header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNjcyMDM3MTY3LCJleHAiOjE2NzQ2MjkxNjd9.-9aVMgPlxUNq0c1i3EEbOuFAFvEme2IrTnzU_6xTjG0
# response
{
"data": [
...
],
"meta": {
...
}
}
微信小程序登录
说完了 strapi 的注册、登录过程,再看看 小程序登录 。根据官方文档的解释,是利用 wx.login() 获取临时登录凭证code,然后将code传递给开发者服务器,在开发者服务器调用微信的 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识 UnionID,开发者服务器拿到用户的标识以后,可以根据自己的业务需要对用户进行权限管理。
微信小程序官网的登录流程时序图如下:
通常来说,对于一个前端来说,我们能做的只有第一步: wx.login() 获取 code,通过 wx.request()发送 code。
后续的开发者服务器部分,如何通过 code 换取 OpenId 等业务操作,如果作为 纯前端 我们是做不了的,需要后端支持,不过既然要使用 strapi 自己开发一个完整的服务,可以利用 strapi 的插件系统,将微信登录与 strapi 用户系统结合,完成一个完整的微信小程序用户登录业务,并对其进行权限控制。
下面介绍具体做法。
将微信小程序登录与strapi用户系统结合
前面提到,微信小程序登录就是用 wx.login() 获取临时登录凭证 code,再用 wx.request()发送给后端,后端如何处理这个 code,如何处理登录逻辑,做为前端是无法控制的。
但是现在作为前端的我们,自己开发一个完整的CMS系统,没有后端可以依靠,只能自己来!
以 strapi 为基础,完成微信小程序的用户登录,具体的做法主要有以下几步:
- 基于 strapi 系统,生成一个 strapi plugin
- 利用这个 plugin 接收来自 wx.request() 发送过来的 code
- 调用微信 auth.code2Session 接口换取用户唯一标识 OpenID 等用户信息
- 将用户信息存到 strapi 用户表里,返回 jwt token 及用户信息
微信小程序拿到后端返回的 token 及用户信息以后,就可以存起来用于以后的业务中,下面分步详细说明:
1. 基于 strapi 系统,生成一个 strapi plugin
strapi 提供以命令行方式初始化一个 plugin ,切换到 strapi 根目录,执行命令
npm run strapi generate
# 执行命令后会让你选择要生成的模板类型,选择 plugin 即可
> strapi "generate"
? Strapi Generators (Use arrow keys)
> api - Generate a basic API
controller - Generate a controller for an API
content-type - Generate a content type for an API
plugin - Generate a basic plugin
policy - Generate a policy for an API
middleware - Generate a middleware for an API
service - Generate a service for an API
# 然后按提示输入 plugin name: wx-login
# 再选择语言 javascript
# 回车后文件就生成完成了
如果是第一次开发 plugin,需要在目录下新建文件 ./config/plugins.js
,然后将以下内容写到 plugins.js 中:
module.exports = {
// ...
'wx-login': {
enabled: true,
resolve: './src/plugins/wx-login'
},
// ...
}
再打开src\plugins\wx-login\server\routes\index.js
,在 config 里加上 auth: false
。
然后启动项目(npm run develop
)进行测试:
# get
{{STRAPI_BACKEND_URL}}/wx-login/
# response
Welcome to Strapi 🚀
如果能看到如上的返回,那么 wx-login 插件就初始化好了。
2. 利用这个 plugin 接收来自 wx.request() 发送过来的 code
接下来要做的是在微信小程序端,利用 wx.login() 获取临时登录凭证 code,然后传递给后端。
微信小程序
index.wxml
<!--pages/account/index.wxml-->
<button bindtap="login">授权登录</button>
微信小程序
index.js
// pages/account/index.js
Page({
// 其他业务代码 ...
login() {
wx.login({
success: res => {
// 发送res.code到后台换取openId,sessionKey,unionId
wx.request({
url: 'http://localhost:1337/wx-login/',
method: "post",
data: {
code: res.code
},
success(res) {
console.log('wx.request res', res)
}
})
}
})
},
// 其他业务代码 ...
})
服务端接收 code
strapi wx-login 插件
src\plugins\wx-login\server\routes\index.js
// 修改 route,接受来自前端的 post 请求
module.exports = [
{
method: 'POST',
path: '/',
handler: 'myController.index',
config: {
auth: false,
policies: [],
},
},
];
src\plugins\wx-login\server\controllers\my-controller.js
// 接收 post 过来的 code,传递给 service 处理
'use strict';
module.exports = ({ strapi }) => ({
async index(ctx) {
ctx.body = await strapi
.plugin('wx-login')
.service('myService')
.login(ctx.request.body.code);
},
});
src\plugins\wx-login\server\services\my-service.js
// service 接收到 code 后,调用微信调用 auth.code2Session 接口换取用户唯一标识 OpenID等用户信息
'use strict';
const axios = require("axios")
module.exports = ({ strapi }) => ({
getWelcomeMessage() {
return 'Welcome to Strapi 🚀';
},
login(code) {
return new Promise(async (resolve, reject) => {
try {
// 这里是主要的处理逻辑
} catch (error) {
return reject({ error: true, message: error });
}
})
},
});
到目前为止,已经按着 strapi plugin 的代码格式,成功的处理了来自前端的请求,并将 code 传递到 service,接下来将具体看如何调用微信的服务,获取用户标识。
3. 调用微信 auth.code2Session 接口换取用户唯一标识 OpenID等用户信息
打开微信小程序管理后台,进入:开发管理 -> 开发设置,找到 AppID
、AppSecret
,将其复制到代码中。再结合前端传过来的 code 进行登录凭证校验。
src\plugins\wx-login\server\services\my-service.js
let app_id = 'Your AppID'
let app_secret = 'Your AppSecret'
// 这里的 code 是前端通过 wx.login() 获取临时登录凭证
let resData = await axios.get(`https://api.weixin.qq.com/sns/jscode2session?appid=${app_id}&secret=${app_secret}&js_code=${code}&grant_type=authorization_code`)
返回的数据示例:
{
session_key: '0PTbmNDSOmdkJJqAIaNNVw==',
openid: 'oFHxc5TV5VKscIudqlmfx9JpK4d4'
}
如果能正确返回 openid
,说明验证成功,认为这个用户是一个有效用户,返回登录信息。
4. 将用户信息存到 strapi 用户表里,返回 jwt token 及用户信息
前面已经完成了微信小程序登录的整个过程,现在要把这个用户记录到 strapi 的 user 表里。 需要在 user 表里新建一个 openid 字段,将 auth.code2Session 接口获取的 openid 存到表里,做为用户的唯一标识。
来到后面管理界面, 切换到 User 模型管理 (PLUGINS -> Content-Type Builder -> COLLECTION TYPES -> User),添加 openid 字段,并且保存。
NAME | TYPE |
---|---|
openid | Text |
添加完成后,下面看 my-service.js
的完整代码。
src\plugins\wx-login\server\services\my-service.js
'use strict';
const axios = require("axios")
module.exports = ({ strapi }) => ({
getWelcomeMessage() {
return 'Welcome to Strapi 🚀';
},
// 生成一个随机的密码
makeRandomPassword(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
},
login(code) {
return new Promise(async (resolve, reject) => {
try {
let app_id = 'Your AppID'
let app_secret = 'Your AppSecret'
// 调用 auth.code2Session 进行用户登录验证
let resData = await axios.get(`https://api.weixin.qq.com/sns/jscode2session?appid=${app_id}&secret=${app_secret}&js_code=${code}&grant_type=authorization_code`)
if (resData.status !== 200) {
return reject({ error: true, message: "Error occur when request to wechat api" });
}
// 验证失败,返回错误信息
if (!resData.data.openid) {
return reject({ error: true, message: resData.data });
}
// 登录验证通过,查询 strapi 用户表中是否存在该用户
const { openid } = resData.data;
const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: { openid } });
// 如果该用户不存在(第一次登录),将用户信息插入到用户表中
if (!user) {
let randomPass = this.makeRandomPassword(10);
let password = await strapi.service("admin::auth").hashPassword(randomPass);
let newUser = await strapi.db.query('plugin::users-permissions.user').create({
data: {
password,
openid,
confirmed: true,
blocked: false,
role: 1,
provider: "local"
}
})
// 返回登录信息
return resolve({
token: strapi.plugin('users-permissions').service('jwt').issue({ id: newUser.id }),
user: strapi.service('admin::user').sanitizeUser(newUser),
})
}
// 如果用户已经存在于 user 表中,直接返回用户登录信息
resolve({
token: strapi.plugin('users-permissions').service('jwt').issue({ id: user.id }),
user: strapi.service('admin::user').sanitizeUser(user),
})
} catch (error) {
return reject({ error: true, message: error });
}
})
},
});
至此,微信小程序的整个登录过程就结束了,前端通过 wx.login() 可以直接拿到 token
,前端通过返回的 token
可以访问 strapi 权限管理体系内的受限资源。
更详细的代码看这里: wfzong/strapi-wechat-miniprogram-auth (github.com)
strapi 插件 WeChat MiniProgram Auth 的使用
作为单个项目来说,这么做是没问题的,可以直接在代码写任何逻辑,但其实这个登录是个通用过程,所以在前段时间做项目的时候,就把它写成了一个 strapi plugin 插件,主要有两点优化:
- 可以在后台管理 appid 和 app_secret,不用把它们直接写在代码里。
- 一键安装使用,几乎不用写任何代码。
并且将它发布到了 strapi 的插件市场里,官方也收录了:Wechat Miniprogram Auth | Strapi Market
使用相对简单,只需要npm install strapi-wechat-miniprogram-auth
安装一下,再配置一下 config/plugins.js
,再给 user 表加两个字段就可以使用。
更详细的使用说明,可以看 strapi-wechat-miniprogram-auth - npm (npmjs.com) 的 README.md。
总结
微信小程序的登录过程相对比较简单,其核心是通过 code 拿到 openid,这个 openid 是用户在微信系统的唯一标识,将它存到 strapi 的 user 表里,也就成为了用户在 strapi 系统里的唯一标识,这样就将微信小程序的用户和strapi用户关联起来,然后再按着 strapi 的 token 生成规则,生成 token 返回给用户,用户拿到 token 后就完成了整个登录过程。
至于是否在微信小程序端获取用户信息、获取用户手机号等业务,可以根据自己的业务需要做处理,然后决定把它存在什么位置。