微信扫码登录,vue node mysql,出不来你打我

1,364 阅读3分钟

中篇 , 生成带参数的二维码,加扫码后的状态

不好意思久等了,两天没更新,看了有兄弟收藏的很高兴,那么继续往下面走

上篇中如果照着做下去的话一定都连接上公众号了

image.png

微信服务器会发送 两种类型消息   给开发者
        1.get
            验证服务器的有效性
        2.post
            微信服务器会将用户发送的数据以post转发到开发者

之后就是要去获取二维码了

 获取二维码最重要的就是需要access-token,那么我们去获取它,这里可能需要redis
 

首先在utils/wechat/文件夹中新建一个helper.js,负责提供公众号配置(用于下面创建wechat对象)和get/set access_token的两个方法。

const { WXMP } = require('../../config')
const { redis } = require('../dbHelper')
const config = {
    MP: {
        appID: WXMP.appID,
        appSecret: WXMP.appSecret,
        token: WXMP.token,
        getAccessToken: async () => {
            let token = await redis.get('access_token')
            return token
        },
        saveAccessToken: async (data = {}) => {
            await redis.set('access_token', data.access_token ,'EX', data.expires_in)
        }
    }
}
module.exports = {
    ...config
}

这上面两个引用

第一个是一开始的config文件

第二个是redis文件,下面去redis文件在utils/dbHelp.js文件,这时候就需要有一个redis的软件了

const Redis = require('ioredis')
const { CACHE }  = require('../config')

/* ////////////// Redis ////////////// */

const redis = new Redis({
  host: CACHE.host,
  port: CACHE.port
})

module.exports = {
  redis
}

在utils/wechat/文件夹中新建一个wxmp.js,定义一个Wechat类

const got = require('got')

const wxGot = got.extend({
  baseUrl: 'https://api.weixin.qq.com/cgi-bin/',
  json: true,
  hooks: {
    afterResponse: [
      res => res.statusCode === 200 ? JSON.parse(res.body) : null
    ]
  }
})
const api = {
    accessToken: 'token?grant_type=client_credential',
    user: {
      info: 'user/info?',
    },
    QRCodeTicket: 'qrcode/create?',
    QRCode: 'showqrcode?'
  }

  class Wechat {
    constructor (opts) {
      this.opts = Object.assign({}, opts)
      this.appID = opts.appID
      this.appSecret = opts.appSecret
      this.getAccessToken = opts.getAccessToken
      this.saveAccessToken = opts.saveAccessToken
      this.getTicket = opts.getTicket
      this.saveTicket = opts.saveTicket
  
      this.fetchAccessToken(true)
    }
  
    async fetchAccessToken (init = false) {
      let token = await this.getAccessToken()
  
      if (!token) {
        token = await this.updateAccessToken()
        await this.saveAccessToken(token)
        token = token.access_token
      }
      return token
    }
  
    async updateAccessToken () {
      const url = api.accessToken + '&appid=' + this.appID + '&secret=' + this.appSecret
      return await wxGot(url)
    }
  
    async handle (operation, ...args) {
      const token = await this.fetchAccessToken()
      if (!token) return {}
  
      const options = this[operation](token, ...args)
      console.log(options,'optionsoptions')
      let res = await wxGot(options)
      console.log(res)
  
      return res
    }
  
    getUserInfo (token, openID, lang) {
      const url = `${api.user.info}access_token=${token}&openid=${openID}&lang=${lang || 'zh_CN'}`
  
      return { url: url }
    }
  
    getQRCodeTicket (token, sceneStr, timeout) {
      console.log(token,sceneStr,timeout,'70707070')
      return {
        url: `${api.QRCodeTicket}access_token=${token}`,
        method: 'post',
        body: {
          "expire_seconds": timeout || 60,
          "action_name": "QR_STR_SCENE", // 临时二维码
          "action_info": {
            "scene": {
              "scene_str": sceneStr
            }
          }
        }
      }
    }
  }
module.exports = Wechat

之后回到刚才的/routes/index.js,增加代码(新增获取二维码的路由)

const { createQRCodeMB } = require('../controllers/wechat')
module.exports = function(app){
    app.get('/aa',(req,res)=>{
        res.send('aaaaaaaaaaaaaaaa')
    })

    // 获取二维码
    app.get('/qrcode',createQRCodeMB)
  
}

然后回到/controllers/wechat.js里面去写代码

const config = require('../config')
const sha1 = require('sha1')
// json web token
const {
    fmtNormalXML,
    streamToBuffer,
    createTimestamp,
    getUserDateAsync,
    parseXMLAsync,
    fromatMessage
} = require('../utils/mUtils')
const { tmpl } = require('../utils/wechat/tmpl')
const qr = require('../vendor/qr')
const fs = require('fs')
const pathResolve = require('path').resolve

const { redis } = require('../utils/dbHelper')
const Wechat = require('../utils/wechat/wxmp')
const MPConfig = require('../utils/wechat/helper').MP
const MP = new Wechat(MPConfig)


module.exports = () => {
    return async (req, res, next) => {
        const token = config.WXMP.token
        const {
            signature,
            echostr,
            timestamp,
            nonce
        } = req.query
        // console.log(req.query)
        const str = [token, timestamp, nonce].sort().join('')
        const signVerified = sha1(str)
        // console.log(signVerified)
        // console.log(req.query,'353535353535353535')

        /* 微信服务器会发送 两种类型消息   给开发者
            1.get
                验证服务器的有效性
            2.post
                微信服务器会将用户发送的数据以post转发到开发者
            console.log('0--------------')
         */
        if (req.method === 'GET') {
            if (signVerified === signature) {
                res.send(echostr)
            } else {
                res.end('err')
            }
        } 
        else if (req.method === 'POST') {
            // 验证 消息是否来自微信服务器
            if (signVerified !== signature) {
                //消息不是微信服务器
                res.end('error')
            }
         
            console.log('--------------')
            // 接收请求体中的数据 , 流式数据
            const xmlData = await getUserDateAsync(req)
         
            // 解析xml
            const jsonData = await parseXMLAsync(xmlData)
            const message = fmtNormalXML(jsonData.xml)
            console.log(message,'解析结果')

            const msgType = message.MsgType
            const msgEvent = message.Event
            const userID = message.FromUserName
            let eventKey = message.EventKey
            let body = null
            if (msgType === 'event') {
                switch (msgEvent) {
                    // 关注&取关
                    case 'subscribe':
                    case 'unsubscribe':
                        body = await subscribe(message)
                        break
                    // 关注后扫码
                    case 'SCAN':
                        body = '扫码成功'
                        break
                }

                if (!!eventKey) {
                    // 有场景值(扫了我们生成的二维码)
                    let user = await MP.handle('getUserInfo', userID)
                    console.log(user)
                    let userInfo = `${user.openid},${user.nickname},${user.sex == 0 ? '男' : '女'}, ${user.province}${user.city}`
                    if (eventKey.slice(0, 8) === 'qrscene_') {
                        // 扫码并关注
                        // 关注就创建帐号的话可以在这里把用户信息写入数据库完成用户注册
                        eventKey = eventKey.slice(8)
                        console.log(userInfo + '扫码并关注了公众号')
                        obj = user

                    } else {
                        // 已关注
                        console.log(userInfo + '扫码进入了公众号')
                        obj = user
                    }
                    // 更新扫码记录,供浏览器扫码状态轮询
                    await redis.pipeline()
                               .hset(eventKey, 'unionID', user.unionid || '') // 仅unionid机制下有效
                               .hset(eventKey, 'openID', user.openid)
                               .exec()
                }
                res.send(tmpl(body || req.body, message))
            }
          
          
            
            // // 如果开发者服务器没有返回响应给微信服务器 ,微信服务器会发送三次请求过来
            // res.end('')


            // console.log(req,query)

        } else {
            res.end('err')
        }
    }
}
async function subscribe (message) {
    let userID = message.FromUserName
    if (message.Event === 'subscribe') {
        obj = message
        return  obj && '感谢您的关注'
    } else {
        // 用户取消关注后我们不能再通过微信的接口拿到用户信息,
        // 如果要记录用户信息,需要从我们自己的用户记录里获取该信息。
        // 所以建议创建用户时除了unionid,最好把openid也保存起来。
        console.log(userID + '取关了')
    }
}

const templetData = fs.readFileSync(pathResolve(__dirname, '../vendor/qrcode-templet.html'))

// 创建二维码
async function createQRCodeMB (req,res1,next) {
    let userID = req.query.userID
    let type = +req.query.type
    let errno = 0
    let responseDate = {}
    let id = createTimestamp()
    let res = await MP.handle('getQRCodeTicket', id)
    if (res === null) errno = 1
    else {
        responseDate = {
            expiresIn: res.expire_seconds,
            id
        }
        let imgBuffer = await streamToBuffer(qr.image(res.url))
        let imgSrc = imgBuffer.toString('base64')
        if (type === 1) {
            // 返回图片
             res1.send(`<img src="data:image/png;base64,${imgSrc}" />`)
           
        } else if (type === 2) {
            // 返回一个自带查询状态和跳转的网页
            let templetValue = `
                <script>var imgSrc='${imgSrc}',id='${responseDate.id}',
                timeout=${responseDate.expiresIn},width=200,height=200</script>`
                res1.send(templetValue + templetData.toString('utf-8'))
           
        } else {
            // 返回图片内容
            responseDate.imgSrc = imgSrc
        }
     }
    if (!req.body) {
        req.body = {
            errno,
            ...responseDate
        }
    }
}
module.exports.createQRCodeMB = createQRCodeMB

这个时候可能就报错了 ,因为上面引入了很多东西 ,现在按上面的一步一步去走

先去创建/utils/mUtils.js这里是封装的一些方法

// const uuidv1 = require('uuid/v1')
const crypto = require('crypto')
const {parseString} = require('xml2js')
// function uuid() {
//   return uuidv1()
// }

function MD5 (str) {
  let result = crypto.createHash('md5').update(str.toString()).digest('hex')
  return result
}

function SHA1 (str) {
  let result = crypto.createHash('sha1').update(str.toString()).digest('hex')
  return result
}

function isInteger (num, zero = false, unsigned = false) {
  num = +num
  let res = Number.isInteger(num)

  if (res && !unsigned) res = num > 0 - zero ? num : false
  return res
}

function fmtNormalXML (xml) {
  let message = {}

  if (typeof xml === 'object') {
    const keys = Object.keys(xml)

    for (let i = 0; i < keys.length; i++) {
      let item = xml[keys[i]]
      let key = keys[i]

      if (!(item instanceof Array) || item.length === 0) {
        continue
      }

      if (item.length === 1) {
        let val = item[0]

        if (typeof val === 'object') {
          message[key] = fmtNormalXML(val)
        } else {
          message[key] = (val || '').trim()
        }
      } else {
        message[key] = []

        for (let j = 0; j < item.length; j++) {
          message[key].push(fmtNormalXML(item[j]))
        }
      }
    }
  }

  return message
}

function createNonce (len = 15) {
  return Math.random().toString(36).substr(2, len)
}

function createTimestamp () {
  return parseInt(new Date().getTime() / 1000, 0) + ''
}

function streamToBuffer(stream) {  
  return new Promise((resolve, reject) => {
    let buffers = []
    stream.on('error', reject)
    stream.on('data', (data) => buffers.push(data))
    stream.on('end', () => resolve(Buffer.concat(buffers)))
  })
}

// 解析xml
function getUserDateAsync(req){
  return new Promise((reslove,reject)=>{
    let xmlData = '';
    req
      .on('data',data=>{
        // 当数据传递过来的时候
        xmlData +=data.toString()
      })
      .on('end',()=>{
        // 当数据接收完毕时,会触发
        reslove(xmlData)
      })
  })
}

//转化xml
function parseXMLAsync(xmlData){
  return new Promise((reslove,reject)=>{
    parseString(xmlData,{trim:true},(err,data)=>{
      if(!err){
        reslove(data)
      }else{
        reject('parseXMLAsync方法出了问题'+err)
      }
    })
  })
}

//获取转化xml后内容
function fromatMessage(jsData){
    let message = {}
    jsData = jsData.xml
    
    if(typeof jsData==='object'){
      for(let key in jsData){
        var value = jsData[key];
        if(Array.isArray(value) && value > 0){

          message[key] = value[0]
        }
      }
    }
    return message
}

module.exports = {
  MD5,
  SHA1,
  isInteger,
  fmtNormalXML,
  createNonce,
  createTimestamp,
  streamToBuffer,
  getUserDateAsync,
  parseXMLAsync,
  fromatMessage
}

之后在创建/utils/wechat/tmpl.js,这里是xml模板

const util = require('util')
const msgTemplet = `
<xml>
    <ToUserName><![CDATA[%s]]></ToUserName>
    <FromUserName><![CDATA[%s]]></FromUserName>
    <CreateTime>%d</CreateTime>
    <MsgType><![CDATA[%s]]></MsgType>
    $msgBody$
</xml>
`
const textMsg = `<Content><![CDATA[%s]]></Content>`
const imageMsg = `<Image><MediaId><![CDATA[%s]]></MediaId></Image>`
function tmpl(ctx,originMsg){
    let type = (ctx && ctx.type) || 'text'
    let msgTmpl = util.format(msgTemplet,
        originMsg.FromUserName,
        originMsg.ToUserName,
        Math.floor(new Date().getTime() / 1000),
        type
    )
    let body = ''
    switch (type) {
        case 'text':
            body = util.format(textMsg, ctx)
            break
    case 'image':
        break
    default:
        body = util.format(textMsg, '操作无效')
    }
    return msgTmpl.replace(/\$msgBody\$/, body)
}

module.exports ={
    tmpl
}

vendor文件夹呢 是一个封装的东西,代码太多就不贴了,回头可以私我给你们,如果实在不想要,那也可以找一下代码把那块注掉,没啥影响

再去创建utils/dbHelper.js

const Redis = require('ioredis')
const { CACHE }  = require('../config')

/* ////////////// Redis ////////////// */

const redis = new Redis({
  host: CACHE.host,
  port: CACHE.port
})

module.exports = {
  redis
}

到此,应该就不会报错了 但是这个时候就要去开启redis了

image.png 打开这个页面就成功开启了

之后就去调试一下吧

去网页打开 http://localhost:8090/qrcode?type=1   
type 可以为0 1 2 都可以试试
打开这个如果出现二维码你就成功了,如果没出来那就去app.js找找原因,哈哈

image.png

 如果做出来了 ,希望点个喜欢,做不出来私我,感谢支持,

附上 我的demo地址

http://124.221.233.32:84