手把手实现第三方社交登录方式微信登录

1,867 阅读5分钟

为什么要使用第三方社交登录吗?

  • 社交登录

    • 含义:访问某个网站的用户可以通过社交媒体账号登录

    • 优点

      • 可以利用社会化媒体的影响增加网站和用户粘性,提高留存
      • 借助分享、裂变等方式对网站进行推广,提高知名度 AARRR中的留存和传播
      • 同时增加网站的流量,使用量,提高网站产品的转换率
    • 类型

      • 腾信微信
      • 腾讯QQ
      • 阿里支付宝
      • 微博
      • .....
  • 微信登录

    • 场景二维码登录
    • Oauth2.0授权登录

特别注意: 订阅号不能实现微信登录服务,服务号才可以实现,但是微信认证也要300大洋,我的钱,呜呜呜。

微信登录业务逻辑梳理-系统时序图

  • 时序图

    • 介绍微信登录从接入微信服务器node后端开发整个链路
    • 整个链路比较复杂,当前先宏观上了解整个流程,但是我会详细地讲解每个流程逻辑
    • 如果有部分细节不理解没关系,在微信登录实战开发时结合时序图都会一一掌握
  • 时序图的组成元素

    • 对象

      • 扮演的角色,大矩形表示
    • 生命线

      • 对象存活 的事件对象下的虚线
    • 控制焦点

      • 在这个时间段执行响应的操作,小矩形标识
    • 消息

      • 发送消息:实线箭头
      • 返回消息:虚线箭头
      • 处理消息:自身调用,处理数据

image.png

深入理解微信服务器的接口授权接入

使用花生壳,生成的外网地址固定,不用频繁更换

image.png

场景二维码的接入:developers.weixin.qq.com/doc/offiacc…

  • 通过access_token获取ticket

  • 拼接二维码url

    • 微信提供的部分url拼接上获取的ticket,将此url和ticket返回客户端

      https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET
      
  • 用户扫码事件:developers.weixin.qq.com/doc/offiacc…

    • 扫码之后的操作看时序图流程

    • 微信回调node服务器接口发送的XML格式消息

    • 返回给微信服务器的消息

image.png

微信服务器回调本地接口【对称加密】接入验证

微信服务器回调本地接口接入验证

  • 创建本地回调的接口

    http://127.0.0.1:8081/api/wx_login/v1/callback
    

 

  • 内网穿透实现外网访问本地接口

    • 花生壳配置本地地址

image.png

测试访问

https://474y3966p9.goho.co/api/wx_login/v1/callback

image.png

  • 接收微信服务器发送的参数对称加密验证

    • developers.weixin.qq.com/doc/offiacc…

    • 对称加密逻辑开发验证

      // sha1加密安装
      yarn add sha1@1.1.1
      
      const SecretTool = require('../utils/SecretTool')
      
      
      const WxLoginService = {
        wechat_insert: (signature, timestamp, nonce, echostr) => {
          // 服务器的token
          let token = 'testxdclass'
          // 将token、timestamp、nonce三个参数进行字典序排序,拼接成一个字符串,进行sha1加密
          let str = SecretTool.sha1([token, timestamp, nonce].sort().join(''))
          // 获得加密后的字符串可与signature对比,验证标识该请求来源于微信服务器
          if (str === signature) {
            // 确认此次GET请求来自微信服务器,原样返回echostr参数内容,则接入生效
            return echostr
          }
        },
      }
      
      
      module.exports = WxLoginService
      
  • 微信公众平台配置

    • 基本配置

image.png

  • 创建获取二维码接口

    http://127.0.0.1:8081/api/wx_login/v1/login
    

 

  • 请求微信服务器获取 access_token

    const appId = 'wx5beac15ca207c40c';
    const appSecret = '8189e5f14346ccaa3bd5f6909f31a362';
    const accessTokenPc = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
    const getAccess_token = () => {
      return axios({
        method: 'get',
        url: accessTokenPc,
      });
    };
    

 

  • 请求微信服务器获取 ticket

    const ticketReq = (token) => {
      return axios({
        method: 'post',
        url: `https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${token}`,
        data: {
          expire_seconds: 60 * 2, // 二维码有效时间
          action_name: 'QR_SCENE',
          action_info: { scene: { scene_id: 123 } },
        },
      });
    };
    

     

  • 获取二维码方法封装

    const qrUrl = 'https://mp.weixin.qq.com/cgi-bin/showqrcode';
    const wechatLogin = {
      // 获取微信登录二维码
      getQR: async () => {
        let token = (await getAccess_token()).data.access_token;
        console.log((await getAccess_token()))
        console.log('token:', token)
        let ticket = (await ticketReq(token)).data.ticket;
        return { qrcodeUrl: `${qrUrl}?ticket=${ticket}`, ticket: ticket };
      }
    }
    

     

  • 将 ticket作为key值,{ isScan : false } 转成 json 存入Redis缓存

    let key = `wechat:ticket:${ticket}`
    redisConfig.set(key, JSON.stringify({ isScan: 'no' }), 120)
    

     

  • 返回二维码url+ticket

掌握流数据+xml数据处理接受微信服务器请求参数

  • 创建用户扫码时微信回调接口的开发 POST请求

    // controller 控制层
    wechat_message: (req, res) => {
      let handleRes = WxLoginService.wechat_message(req)
      res.send(handleRes)
    }
    
    // service 数据层
    wechat_message: (req, res) => {
      return '成功'
    }
    
  • 处理微信回调的参数

    • 封装数据处理工具类
    /**
     * @param {*} getXMLStr 以流的形式接收微信服务器发过来的数据
     * @param {*} getJsData 通过工具解析xml数据转换成对象
     * @param {*} getObjData 优化数据
     */
    var { parseString } = require('xml2js');
    
    
    // 微信XML数据类型处理
    class WxDataTool {
      static getXMLStr(req) {
        return new Promise((resolve, reject) => {
          let data = '';
          req.on('data', (msg) => {
            data += msg.toString();
          });
          req.on('end', () => {
            resolve(data);
          });
        });
      }
      
      static getJsData(data) {
        return new Promise((resolve, reject) => {
          parseString(data, (err, result) => {
            if (err) {
              reject('error');
            } else {
              resolve(result);
            }
          });
        });
      }
    
    
      static getObjData(obj) {
        let tempObj = {};
        if (obj && typeof obj === 'object') {
          // 循环对象,提取数据
          for (let key in obj) {
            let value = obj[key];
            if (value && value.length > 0) {
              tempObj[key] = value[0];
            }
          }
          return tempObj;
        } else {
          return tempObj;
        }
      }
    };
    
    
    module.exports = WxDataTool
    

根据微信回调参数生成权限token+redis缓存状态+xml数据返回

  • 根据openid进行注册/登录

    // 根据openid判断是否注册
    let openidRes = await DB.Account.findAll({ where: { openid }, raw: true })
    // 随机获取头像、用户名
    let head_img = RandomTool.randomAvatar()
    let username = RandomTool.randomName()
    // 无注册则增加个用户数据
    let user = null
    if (openidRes.length === 0) {
      let resData = await DB.Account.create({ username, head_img, openid })
      user = { head_img, username, id: resData.toJSON().id }
    } else {
      user = {
        head_img: openidRes.head_img,
        username: openidRes.username,
        id: openidRes.id
      }
    }
    
  • 生成token

    let token = SecretTool.jwtSign(user, '168h')
    
  • 更新redis缓存中的扫码状态+token

    let key = `wechat:ticket:${ticket}`
    const existsKey = await redisConfig.exists(key)
    if (existsKey) {
      redisConfig.set(key, JSON.stringify({ isScan: 'yes', token }), 120)
    }
    
  • 返回微信xml格式数据

    let content = ''
    if (msgObj.MsgType == 'event') {
      // 扫码
      if (msgObj.Event == 'SCAN') {
        content = '欢迎回来,讲师微信:xdclass6'
      } else if (msgObj.Event == 'subscribe') {
        // 订阅
        content = '感谢关注,讲师微信:xdclass6'
      }
      // 根据来时的信息格式,重组返回。(注意中间不能有空格)
      let msgStr = `<xml>
        <ToUserName><![CDATA[${lastData.FromUserName}]]></ToUserName>
        <FromUserName><![CDATA[${lastData.ToUserName}]]></FromUserName>
        <CreateTime>${Date.now()}</CreateTime>
        <MsgType><![CDATA[text]]></MsgType>
        <Content><![CDATA[${content}]]></Content>
      </xml>`
      return msgStr
    }
    

客户端怎么知道用户何时扫码?开发轮询接口

image.png

  • 创建轮询接口

    http://127.0.0.1:8081/api/wx_login/v1/check_scan
    
    
    请求方法:get
    请求参数:ticket
    

 

  • 根据ticket参数查询redis缓存

    let { ticket } = req.query
    let key = `wechat:ticket:${ticket}`
    let redisData = JSON.parse(await redisConfig.get(key))
    

 

  • 判断是否存在对应key

    if (redisData && redisData.isScan == 'yes') {
      let token = redisData.token
      return JsonData.buildSuccessAndData(`Bearer ${token}`)
    } else {
      return JsonData.buildResult(CodeEnum.WECHAT_WAIT_SCAN)
    }
    

源代码:

里面都有我每次的commit记录,想要源码的同学可以看下面的链接~

[前端] github.com/mohaixiao/c…

[后端] github.com/mohaixiao/c…