前端开发者利用Strapi实现API自由 - 之微信小程序登录

4,728

背景

作为一名前端,要想自己一个人撸一个网站、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",
        # ......
    }
}

注册成功以后,进入后台管理界面,就可以看到刚刚注册的用户:

image.png

一个用户注册的过程就结束了。

用户登录

登录过程如下:

# 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

在用户权限里配置只有已授权的用户可以访问,如下:

image.png

这时,对于一个 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,开发者服务器拿到用户的标识以后,可以根据自己的业务需要对用户进行权限管理。

微信小程序官网的登录流程时序图如下:

image.png

通常来说,对于一个前端来说,我们能做的只有第一步: wx.login() 获取 code,通过 wx.request()发送 code。

后续的开发者服务器部分,如何通过 code 换取 OpenId 等业务操作,如果作为 纯前端 我们是做不了的,需要后端支持,不过既然要使用 strapi 自己开发一个完整的服务,可以利用 strapi 的插件系统,将微信登录与 strapi 用户系统结合,完成一个完整的微信小程序用户登录业务,并对其进行权限控制。

下面介绍具体做法。

将微信小程序登录与strapi用户系统结合

前面提到,微信小程序登录就是用 wx.login() 获取临时登录凭证 code,再用 wx.request()发送给后端,后端如何处理这个 code,如何处理登录逻辑,做为前端是无法控制的。

但是现在作为前端的我们,自己开发一个完整的CMS系统,没有后端可以依靠,只能自己来!

以 strapi 为基础,完成微信小程序的用户登录,具体的做法主要有以下几步:

  1. 基于 strapi 系统,生成一个 strapi plugin
  2. 利用这个 plugin 接收来自 wx.request() 发送过来的 code
  3. 调用微信 auth.code2Session 接口换取用户唯一标识 OpenID 等用户信息
  4. 将用户信息存到 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等用户信息

打开微信小程序管理后台,进入:开发管理 -> 开发设置,找到 AppIDAppSecret,将其复制到代码中。再结合前端传过来的 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 字段,并且保存。

NAMETYPE
openidText

添加完成后,下面看 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 插件,主要有两点优化:

  1. 可以在后台管理 appid 和 app_secret,不用把它们直接写在代码里。
  2. 一键安装使用,几乎不用写任何代码。

并且将它发布到了 strapi 的插件市场里,官方也收录了:Wechat Miniprogram Auth | Strapi Market

image.png

使用相对简单,只需要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 后就完成了整个登录过程。

至于是否在微信小程序端获取用户信息、获取用户手机号等业务,可以根据自己的业务需要做处理,然后决定把它存在什么位置。