如何实现微信关注公众号自动登录功能 ?

背景:最近和我的小伙伴们基于 ChatGPT 开发了一个脑图生成工具。而登陆一直用的短信验证,比较消耗资金,于是就准备接入微信登陆。

image-20230328095006004

1. 前置条件

通过审核的微信公众号

有公网IP的web服务器

核心概念:当用户扫描二维码时,微信服务会向我们配置url发起post请求。这个请求携带着用户的一些操作数据。我们可以在生成的微信公众号二维码中附带一些数据,这些数据也会一起被发送。

如果上面条件满足则可以进入下一步。

2. 微信公众号开发平台配置

2.1 配置IP白名单

配置完IP白名单才能够去获取access_token 用于访问微信服务。这个白名单应该是你公网IP地址。

image-20230328090531248

2.2 配置微信推送消息的接口

image-20230328090816206

点击修改配置

  • 服务器地址URL:当微信接受到用户向公众号发送的消息时,会通过post请求推送这个消息。值得注意的是,当首次配置时,回通过get请求去验证接口。
  • 令牌:随便填写
  • 消息加密解密密钥:随机生成
  • 消息加密方式:我这里选择明文,就用不到上 令牌和密钥了。

这里实现一下处理微信第一配置url的请求的验证逻辑,我们只要返回 echostr就可以了。

/**
 * 用来接收微信的推送消息
 * tousername: 开发者微信号,即公众号的原始ID
 * fromusername: 发送方帐号,即用户的OpenID
 * createtime: 消息创建时间,整型,表示自1970年以来的秒数
 * msgtype: 消息类型,即事件类型,这里为event
 * event: 事件类型,这里为SCAN,表示用户已经扫描了带场景值的二维码
 * eventkey: 事件KEY值,即场景值,这里为123123test
 * ticket: 二维码的ticket,可用于换取二维码图片
 */
export async function main(ctx: FunctionContext) {
  const { echostr } = ctx.query; // 从请求url中获取这个参数
  const { event, fromusername, ticket, eventkey} = ctx.body.xml; // 这个数据是微信推送的用户操作数据
  // --- 这里我们可以处理对应的逻辑,这里逻辑暂时不写。


	// 返回 echostr 就代表验证通过,微信需要返回这个数据
  return echostr;
} 

3. 生成带参数的微信二维码

3.1 获取访问微信服务的 access_token

我们需要知道 appId 和 key,

image-20230328092738270

接下来接是发起请求得到 access_token.

const res = await fetch(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${key}`);
const data = await res.json(); //这样就能够获取到,access_token

3.2 获取生成二维码的ticket

上面一步,我们已经获得了 access_token, 当然这个 token 请求次数有限,能缓存使用尽量缓存,我这里就不缓存了。

// 获取生成二维码的ticket和设置生成二维码携带的参数
async getTicket(uid: string) {
  const access_token = await this.getAccessToken();
  const TICKET_URL = `https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${access_token}`;
  const body = {
    expire_seconds: 120,	// 过期时间
    action_name: "QR_STR_SCENE", // 名称
    action_info: {
      scene: {
        scene_str: uid, // 这里就是携带的参数,通过这个参数我们可以唯一标识一个人
      }
    }
  };
  const res = await fetch(TICKET_URL, {
    method: "POST",
    body: JSON.stringify(body)
  });
  return await res.json(); // 这里我们就可以得到 ticket 和 二维码过期时间
}

3.3 通过ticket 获取二维码

<img style="width: 200px; height: 200px" src="https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${ticket}">

这里我们就能够获取到二维码。

4. 处理微信推送的消息

当用户扫描时微信会推送携带用户的openID 和我们为二维码设置的参数到我们配置的url。

import cloud from '@lafjs/cloud'
const db = cloud.database();
/**
 * 用来接收微信的推送消息
 * tousername: 开发者微信号,即公众号的原始ID
 * fromusername: 发送方帐号,即用户的OpenID
 * createtime: 消息创建时间,整型,表示自1970年以来的秒数
 * msgtype: 消息类型,即事件类型,这里为event
 * event: 事件类型,这里为SCAN,表示用户已经扫描了带场景值的二维码
 * eventkey: 事件KEY值,即场景值,这里为123123test
 * ticket: 二维码的ticket,可用于换取二维码图片
 */


/**
 * {
  xml: {
    tousername: [ 'gh_462fd94462f8' ],
    fromusername: [ 'oX2TM53cBZk229DDBJ5bR9xc8YVaw' ],
    createtime: [ '16799082520' ],
    msgtype: [ 'event' ],
    event: [ 'SCAN' ],
    eventkey: [ '123123test' ],
    ticket: [
      'gQGk7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyRXpGMmtKdkFmY0QxNDh2eHhBY0MAAgSQXiFkAwR4AAAA'
    ]
  }
}
 */

export async function main(ctx: FunctionContext) {
  const {echostr} = ctx.query;
  const { event, fromusername, ticket, eventkey} = ctx.body.xml;
  // 如果是扫码登陆的消息
  if (event[0] === "SCAN") {
    
    
    // 先通过wx_id判断是用户之前是否已经登陆过
    let { data: user } = await db.collection("user")
      .where({wx_id: fromusername[0]}).getOne();
    if (!user) {
      // 将微信数据保存到用户列表中
      const { id } = await db.collection("user").add({
        wx_id: fromusername[0],
        nickname: fromusername[0],
        phone: "",
        created_at: new Date(),
        created: Date.now(),
      });
      
      const r = await db.collection('user').where({ _id: id }).getOne()
      user = r.data
    }
    // 这里可以使用缓存在做,这里的目的为前端轮训提供查询条件。因为在生成二维码会给用户返回uid 作为后续查询登录状态的参数
    await db.collection("wx_ticket_cache")
      .where({
        uid: eventkey[0]
      }).update({
        use: true,
        wx_id: fromusername[0]
      });
  }


  return echostr;
} 

5. 处理前端轮训

import cloud from '@lafjs/cloud'

const db = cloud.database()

export async function main(ctx: FunctionContext) {  
  const { uid } = ctx.request.body;

  // 1.uid -> wx_id 这里可以是从缓存中获取
  let { data: ticketCache } = await db.collection("wx_ticket_cache").where({
    uid
  }).getOne();

  // 如果还没有扫码,use是用来判断扫码登录是否成功
  if (!ticketCache.use) {
    return {
      code: 1,
      data: {
        user: null
      }
    };
  }

  // 如果扫码后,通过wx_id获取用户信息
  const { data: user } = await db.collection("user").where({
    wx_id: ticketCache.wx_id
  }).getOne();

  // 生成token
  delete user['password'];

  // 默认 token 有效期为 365 天
  const expire = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365
  const access_token = cloud.getToken({
    uid: user._id,
    exp: expire,
  });


  return {
    code: 1,
    data: {
      access_token,
      expire,
      user
    }
  };
}