NestJS 轻松实现小程序扫码登录

282 阅读3分钟

概要

好多时候在系统开发中懒得实现用户模块的登录/注册功能。直接使用第三方授权用户数据作为数据基础,以达到快速创建用户和登录操作。对用户来说可以更快捷的使用系统,对开发者来说可以直接免掉一部分重复工作,双赢!

开始

实现之前,请确认有上线的微信小程序不上线没法生成小程序二维码),记录好你小程序的SerectKey

功能实现需要以下几个接口,请先研读说明:

一般情况上面几个接口够用了,只需要用户的 openid / unionid / 小程序码

1.创建项目

基础创建一个nestjs 项目,具体就不说了(Documentation | NestJS - A progressive Node.js framework

$ npm i -g @nestjs/cli
$ nest new easy_wechat_auth

2.初始化模块

$ nest g mo auth
$ nest g co auth --no-spec
$ nest g s auth --no-spec

3.引入依赖

开发该功能,我们需要用到以下几个依赖:

$ npm i --save @nestjs/axios axios
$ npm install @nestjs/cache-manager cache-manager

使用缓存的时候,你喜欢用redis 需要多加依赖方可使用

3.1 系统引入依赖 app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';

// 依赖
import { CacheModule } from '@nestjs/cache-manager';
import { HttpModule } from '@nestjs/axios';

@Module({
  imports: [
    CacheModule.register({
      isGlobal: true,
    }),
    HttpModule.register({
      timeout: 60e3,
    }),
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

4.接口实现

在开发小程序接口之前,需要获取接口调用凭据:

  ......
  
  private async getAccessToken() {
    const APPID_CACHE_KEY = `${this.appId}:token`;
    // 读取缓存
    let accessToken: string | null =
      await this.cacheManager.get(APPID_CACHE_KEY);
    if (!accessToken) {
      // 获取access_token GET https://api.weixin.qq.com/cgi-bin/token
      const { status, data } = await this.httpService
        .get('https://api.weixin.qq.com/cgi-bin/token', {
          params: {
            grant_type: 'client_credential',
            appid: this.appId,
            secret: this.secret,
          },
        })
        .toPromise();
      if (status === 200 && data?.access_token) {
        await this.cacheManager.set(
          APPID_CACHE_KEY,
          data.access_token,
          (data.expires_in - 60) * 1e3,
        );
      }
      // 抛出异常
      if (data?.errcode) {
        throw new BadRequestException(`[${data?.errcode}] ${data?.errmsg}`);
      }
      accessToken = data?.access_token;
    }
    this.logger.debug('accessToken', accessToken);
    return accessToken;
  }
  
  ......

实现整个扫码流程需要以下几个接口

  • 获取小程序二维码 getQrCode
    • Controller
  ......
  
  @Get('getQrCode')
  async getQrCode() {
    return await this.authService.getQrCode();
  }
  
  ......
    • Service
  
  ......

  /**
   * 获取小程序二维码
   * @returns
   */
  async getQrCode() {
    const APPID_CACHE_KEY = `${this.appId}:scene`;
    // 构建二维码数据
    const accessToken = await this.getAccessToken();
    // POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN

    const { headers, data } = await this.httpService
      .post(
        `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`,
        {
          page: '',
          scene: 'a=1',
        },
        {
          responseType: 'arraybuffer',
        },
      )
      .toPromise();
    // 截取请求响应类型,是错误还是成功
    const contentType = headers['content-type'];
    if (contentType.includes('application/json')) {
      throw new BadRequestException(`[${data?.errcode}] ${data?.errmsg}`);
    } else {
      const scene = uuidv4();
      // 记录缓存等待扫码
      await this.cacheManager.set(
        APPID_CACHE_KEY + `${scene}`,
        {
          status: 0, // 0 等待 1 扫码 2 登录
        },
        5 * 60 * 1e3,
      );
      this.logger.debug('scene', scene);
      return {
        scene,
        qrcode: `data:image/png;base64,${Buffer.from(data as string).toString('base64')}`,
      };
    }
  }
  
  ......
  • 检查二维码状态 getCodeStatus/:scene
    • Controller

  ......
  @Get('getCodeStatus/:scene')
  async getCodeStatus(@Param('scene') scene: string) {
    return await this.authService.getCodeStatus(scene);
  }
  
  ......
    • Service
  ......
  
  /**
   * 获取码状态
   * @param scene
   * @returns
   */
  async getCodeStatus(scene: string) {
    const APPID_CACHE_KEY = `${this.appId}:scene`;
    // 这里不多做了,直接读取缓存数据吧
    return await this.cacheManager.get(APPID_CACHE_KEY + `${scene}`);
  }
  
  ......

  • 扫码确认登录操作 appletLogin/:scene
    • Controller
  ......
  
  @Post('appletLogin/:scene')
  async appletLogin(@Param('scene') scene: string, @Body() data: any) {
    return await this.authService.appletLogin(scene, data);
  }
  
  ......
    • Service
  ......
  
  /**
   *
   * @param scene
   * @param { code }
   * @returns
   */
  async appletLogin(scene: string, { code }) {
    // GET https://api.weixin.qq.com/sns/jscode2session
    const accessToken = await this.getAccessToken();
    const { data } = await this.httpService
      .get(
        `https://api.weixin.qq.com/sns/jscode2session?access_token=${accessToken}`,
        {
          params: {
            appid: this.appId,
            secret: this.secret,
            js_code: code,
            grant_type: 'authorization_code',
          },
        },
      )
      .toPromise();
    // 获取缓存
    const APPID_CACHE_KEY = `${this.appId}:scene`;
    const qrStatus = await this.getCodeStatus(scene);
    if (qrStatus) {
      // TODO 这里如果是有用户模块,需要进行用户的比对处理增加或者修改操作,并生成好登录token数据
      // 更新扫码状态,标记登录成功
      await this.cacheManager.set(
        APPID_CACHE_KEY + `${scene}`,
        {
          status: 2,
          // TODO 写入token等数据
          ...data,
        },
        5 * 60 * 1e3,
      );
    }
    return data;
  }

  ......

5.扫码操作

生成好小程序码之后,小程序侧扫码进入需要获取二维码携带的登录凭据 scene

Page({
  onLoad (query) {
    // scene 需要使用 decodeURIComponent 才能获取到生成二维码时传入的 scene
    const scene = decodeURIComponent(query.scene)
  }
})

image.png

通过以上onload读取到登录的 scene 凭据,在登录确认时携带 scene 进行登录

  • 登录按钮
    <button bind:tap="onAuthTap">一键登录</button>
  • 登录触发事件
    onAuthTap() {
      // 缓存读取scene
      const scene = wx.getStorageSync('scene') || Date.now()
      //微信登录coded
      wx.login({
        success: ({ code }) => {
          console.log('code', code);
          // 提交登录操作
          wx.request({
            method: 'POST',
            url: `http://127.0.0.1:3000/auth/appletLogin/${scene}`,
            data: {
              code,
            },
            success: (res) => {
              // TODO 检查登录结果和写入token等数据
              console.log('success', res);
            }
          })
        }
      })
    }

image.png

最后

下面是我简单描绘的一个图,比较简单的概括了下流程

image.png

比较基础的功能实现,主要是了解下具体的实现流程

各端的能力:

端点能力
Web获取二维码信息及其状态确认登录
小程序扫小程序码提取scene确认登录操作
Api提供小程序码接口和确认scene登录状态

简要源码地址:xuzuxing/easy_nest_wechat_qrcode (github.com)