likes
comments
collection
share

图解&实现JS-SDK鉴权流程

作者站长头像
站长
· 阅读数 9

JS-SDK鉴权流程

图解&实现JS-SDK鉴权流程

简介

全栈实现JS-SDK鉴权流程,前端使用vue的技术栈,后端使用Express和mongodb,实现access_token的缓存与更新。

整体流程

图解&实现JS-SDK鉴权流程

公众号配置

服务器配置

图解&实现JS-SDK鉴权流程

IP白名单

图解&实现JS-SDK鉴权流程

JS接口安全域名

图解&实现JS-SDK鉴权流程

注意事项: 这里配置的域名等需要公网可访问,可以使用内网穿透工具,请看相关工具

前端

核心代码

<html>

<head>
  <title>微信JS-SDK调用Demo</title>
  <link rel="stylesheet" href="/stylesheets/style.css">
  <link rel="stylesheet" href="/stylesheets/base.css">
  <script src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>
  <script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
  <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
  <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
</head>

<body>
  <div id="app">
    <h1>微信JS-SDK调用Demo {{ wxReady }}</h1>
    <p @click="scanCode(0)">扫描二维码--微信处理</p>
    <p @click="scanCode(1)">扫描二维码--自行处理</p>
    <div v-if="scanResult">scanResult: {{ scanResult }}</div>
  </div>
</body>
<script>
  var vConsole = new window.VConsole();
  new Vue({
    el: '#app',
    data: () => {
      return {
        result: {},
        scanResult: '',
        wxReady: ''
      }
    },
    mounted() {
      this.wxconfig();
    },
    methods: {
      wxconfig() {
        console.log('获取 wxconfig', window.wx);
        // let url = encodeURIComponent(location.href.split('#')[0])
        // 改地址为使用花生壳将本地服务做的映射
        const _this = this
        let url = 'https://2z42z99186.imdo.co/'
        axios.get(`https://2z42z99186.imdo.co/jsapi?url=${url}`).then((result) => {
          console.log('jsapi接口数据', result.data);
          this.result = result.data
          wx.config({
            debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
            ...result.data,
            jsApiList: [
              'scanQRCode'
            ] // 必填,需要使用的JS接口列表
          });
          wx.ready(function () {
            _this.wxReady = 'wx.ready'
          });

          wx.error(function (a, b) {
            _this.wxReady = 'wx.error'
          });

        }).catch(err => {
          console.log('err', err);
        })
      },
      scanCode(type) {
        const that = this
        wx.scanQRCode({
          needResult: type, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
          scanType: ["qrCode", "barCode"], // 可以指定扫二维码还是一维码,默认二者都有
          success: function (res) {
            // 当needResult 为 1 时,扫码返回的结果
            that.scanResult = res.resultStr
          }
        });
      }
    },
  })
</script>

</html>

后端

核心代码

鉴权接口

router.get('/jsapi', async function (req, res) {
  let url = decodeURIComponent(req.query.url);
  let conf = await sign(url);
  res.send(conf);
})

处理签名

require('dotenv').config();
const axios = require('axios')
const ticketModel = require('../db/models/ticketModel')
const crypto = require('crypto');

appid = process.env.APPID
secret = process.env.SECRET

// 生成随机字符串
function createNonceStr() {
    return Math.random().toString(36).substr(2, 15);
}

// 生成时间戳
function createTimestamp() {
    return parseInt(new Date().getTime() / 1000) + '';
}

// 获取 access_token
async function getAccessToken() {
    let tokenUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`
    let tokenTata = await axios.get(tokenUrl);
    console.log('tokenTata', tokenTata.data);
    return tokenTata.data?.access_token
}

// 获取 ticket
async function handleTicKetData(accessToken) {
    let ticketUrl = `https://api.weixin.qq.com/cgi-bin/ticket/handleTicKet?access_token=${accessToken}&type=jsapi`;
    let ticketData = await axios.get(ticketUrl);
    console.log('ticketData', ticketData.data);
    return ticketData.data?.ticket
}

// 保存 access_token、ticket 数据
async function createTicket(access_token, ticket) {
    let time = new Date().getTime();
    await new ticketModel({
        access_token,
        token_time: time,
        ticket,
        ticket_time: time
    }).save().then(res => {
        console.log('createTicket-res', res);
    })
}

// 更新 access_token、ticket 数据
async function updateTicket(_id, access_token, ticket) {
    let time = new Date().getTime();
    await ticketModel.update({ _id }, {
        access_token,
        token_time: time,
        ticket,
        ticket_time: time
    })
}

// 处理 access_token 和 ticket
async function handleTicKet() {
    let access_token = '';
    let ticket = '';
    const ticKetData = await ticketModel.find() ?? [];
    // 判断数据库是否存储过ticket
    if (ticKetData.length > 0) {
        let t = new Date().getTime() - ticKetData[0].token_time;
        if (t > 7000000) { //是否过期
            //重新获取
            await loadData();
            console.log('过期后重新获取token', access_token)
            console.log('过期后重新获取ticket', ticket)
            // 准备更新数据
            let { _id } = ticKetData[0];
            await updateTicket(_id, access_token, ticket)
        } else {
            access_token = ticKetData[0].access_token;
            ticket = ticKetData[0].ticket;
            console.log('从数据库获取: token', access_token)
            console.log('从数据库获取: ticket', ticket)
        }
    } else {
        // 生成Ticket信息,注意这里生成的Ticket信息可能有误,如:access_token 或者 ticket 为空 。 此时,需要保证 appid、secret和公众号的相关配置正确, 如果生成的数据有误,删除数据后在重新生成 
        // 生成Ticket信息
        await loadData();
        // 第一次获取access_token,对数据库进行新增操作
        await createTicket(access_token, ticket)
    }

    // 生成Ticket信息
    async function loadData() {
        access_token = await getAccessToken()
        ticket = await handleTicKetData(access_token)
    }

    return {
        access_token,
        ticket
    }
}

// 生成签名
function generateJSSDKSignature(timestamp, nonceStr, url, jsapiTicket) {
    const rawString = `jsapi_ticket=${jsapiTicket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`;
    const signature = crypto.createHash('sha1').update(rawString).digest('hex');
    return signature;
}

// 生成wx.config的入参数
async function getWxConfigParams(url) {
    const { ticket } = await handleTicKet()
    const obj = {
        jsapi_ticket: ticket,
        nonceStr: createNonceStr(),
        timestamp: createTimestamp(),
        url
    }
    // 生成签名, 参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。
    let signature = generateJSSDKSignature(obj.timestamp, obj.nonceStr, url, ticket)
    obj.signature = signature;
    obj.appId = appid;
    return obj;
}

module.exports = {
    getWxConfigParams,
    handleTicKet
};

完整代码

gitee.com/ironc/wx-js…

常见问题

1、config:fail,invalid url domain

当前网页所使用的域名,没有在JS接口安全域名中,参考公众号配置中的 JS接口安全域名进行修改

2、config:fail,invalid signature

签名失败,注意签名的参数为 jsapi_ticket、nonceStr、timestamp、url,url为当前页面对应的域名。

提供一个签名方法(记得安装一下crypto):

// npm i crypto
function generateJSSDKSignature(timestamp, nonceStr, url, jsapiTicket) {
    const rawString = `jsapi_ticket=${jsapiTicket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`;
    const signature = crypto.createHash('sha1').update(rawString).digest('hex');
    return signature;
}

3、iOS微信浏览器中window下不存在 wx 属性

切换一个JS-SDK的版本,我使用的是 http://res.wx.qq.com/open/js/jweixin-1.6.0.js, 发现的该问题。

解决方式:

1、使用其他版本的JS链接,比如:https://res.wx.qq.com/open/js/jweixin-1.3.2.js,需要注意您所需要的SDK的功能与版本是否匹配

2、使用npm安装, npm install weixin-js-sdk , npm地址 www.npmjs.com/package/wei…

二选一即可

相关工具

1、花生壳

hsk.oray.com/download/

2、natapp

natapp.cn/

3、免费mongodb (其云服务免费)

www.mongodb.com/