Nodejs获取微信签名并使用JSSDK

4,096 阅读15分钟

上一篇我们讲了基本的准备工作,接下来,进入实战,由于楼主我并没有备案过的域名(穷,没钱,没办法哈),还好, 一直通不过签名验证,微信比较人性化,提供测试号,可以测大部分的接口,并且设置JS接口安全域名,没有限制,可以写任何地址,哪怕是localhost:9999也是可以的。

1、接口测试号申请

由于用户体验和安全性方面的考虑,微信公众号的注册有一定门槛,某些高级接口的权限需要微信认证后才可以获取。

所以,为了帮助开发者快速了解和上手微信公众号开发,熟悉各个接口的调用,微信推出了微信公众帐号测试号,通过手机微信扫描二维码即可获得测试号,在这个测试号里面可以模拟各种操作,比如分享啥的,很容易通过验证。

微信JSSDK开发文档

进入微信公众帐号测试号申请系统

2、获取 access_token 访问令牌

access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token`)

https请求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数说明

参数 是否必须 说明
grant_type 获取access_token填写client_credential
appid 第三方用户唯一凭证
secret 第三方用户唯一凭证密钥,即appsecret

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

{"access_token":"ACCESS_TOKEN","expires_in":7200}

实战项目代码:

获取 access_token

config/index.json:

api/accessToken.js

// 获取 access_token
const config = require('../config/index.json'); // 配置数据
const axios = require('axios'); // 请求api
const CircularJSON = require('circular-json');

// (设置 | 获取)缓存方法
const cache = require('../utils/cache');

module.exports = getAccessToken = (res) => {

  const fetchUrl = `${config.getAccessToken}?grant_type=client_credential&appid=${config.appid}&secret=${config.appsecret}`;
  // console.log(fetchUrl, config);

  // 获取缓存
  cache.getCache('access_token', function (cacheValue) {
    // 缓存存在
    if (cacheValue) {
      const result = CircularJSON.stringify({
        access_token: cacheValue,
        from: 'cache'
      });
      res.send(result);
    } else {
      // 调取微信api
      axios.get(fetchUrl).then(response => {
        let json = CircularJSON.stringify(response.data);
				res.send(json);
        // 设置缓存
        if (response.data.access_token) {
          cache.setCache('access_token', response.data.access_token)
        }
      }).catch(err => {
        console.log('axios occurs ', err);
      });
    }
  });

};

这里用的 axios请求微信api,获取 access_token;

由于access_token 只有7200秒有效时间,并且限制一天最多调2000 次,所以中控服务器最好作缓存,这里使用的 node-cache,做了access_token的缓存,并且删除的缓存的时间也设置的是 7200s,这样在 access_token失效的时候,node缓存也会被删除。

utils/cache.js

// node-cache 保存和获取缓存

const NodeCache = require("node-cache");
const myCache = new NodeCache({
  stdTTL: 7200, // 缓存过期时间
  checkperiod: 120 // 定期检查时间
});


// 设置缓存
var setCache = function (key, value) {
  // 设置缓存
  myCache.set(key, value, function (err, success) {
    if (!err && success) {
      console.log(key + "保存成功", value);
    }
  });
};

// 获取缓存
var getCache = function (key, callback) {
  // 读取缓存
  myCache.get(key, function (err, value) {
    if (!err) {
      if (value) {
        console.log(`存在于缓存中${key}=${value}`);
        callback(value);
      } else {
        console.log(`${key} not found in node-cache`);
        callback();
      }
    } else {
      console.log('get ' + key + ' cache occurs error =', err);
    }
  });
};



module.exports = {
  setCache,
  getCache
}

node-cache只能存活于当前进程里面,如果当前node命令被重启,将会重新去请求微信服务器,所以不太适合。

这里其实最好存在 redis数据库里,

路由设置:

app.js

const express = require('express');
const api = require('./api');
const path = require('path');
const app = express();

// accessToken 获取token
app.get('/getAccessToken', (req, res) => {
  api.accessToken(res);
});

....

结果:

这是第一次请求,access_token从微信服务器获取最初的数据。

接下来是第二次请求,access_token将会缓存中读取。

3、获取 jsapi_ticket临时票据

生成签名之前必须先了解一下jsapi_ticketjsapi_ticket是公众号用于调用微信JS接口临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的api调用次数非常有限,频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket

用上一步拿到的access_token采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):<https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi>

成功返回如下JSON:

{
"errcode":0,
"errmsg":"ok",
"ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd841ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUWvsdshFKA",
"expires_in":7200
}

获得jsapi_ticket之后,就可以生成JS-SDK权限验证的签名了。

回到之前说的那个Nodejs + Express项目中:

api/jsapiTicket.js

// 通过 access_token 获取 jsapi_ticket 临时票据
const axios = require('axios'); // 请求api
const CircularJSON = require('circular-json');
const config = require('../config/index.json');
const cache = require('../utils/cache');


module.exports = get_jsapi_ticket = (access_token, res) => {

  const fetchUrl = config.getJsapiTicket + access_token;
  console.log('>>>>', fetchUrl)
  // 判断是否存在于缓存中
  const cacheName = "jsapi_ticket";
  cache.getCache(cacheName, function (cacheValue) {
    if (cacheValue) {
      const result = CircularJSON.stringify({
        ticket: cacheValue,
        from: 'cache'
      });
      res.send(result);
    } else {
      // 调取微信api
      axios.get(fetchUrl).then(response => {
        let json = CircularJSON.stringify(response.data);
        // promise
        res.send(json);
        // 设置缓存
        if (response.data.ticket) {
          cache.setCache(cacheName, response.data.ticket)
        }
      }).catch(err => {
        // console.log('axios occurs ', err);
      });
    }
  });

}

路由设置:

const express = require('express');
const api = require('./api');
const path = require('path');
const app = express();
//express请求别的路由中间件
require('run-middleware')(app);

// 获取 jsapi_ticket 临时票据
app.get('/getTicket', (req, res) => {
  app.runMiddleware('/getAccessToken', function (code, body, headers) {
    const result = JSON.parse(body);
    console.log('User token:', result.access_token);
    api.jsapiTicket(result.access_token, res);
  })
});

....

这里比较特殊的地方,是用 run-middleware 这个 npm package,从一个路由去直接请求另外一个路由的数据。

这样避免重复很多逻辑。我们直接请求路由获取上一步的 access_token;

废话不多说,运行一下:

第一次,是从微信服务器获取 ticket

第二次,从缓存中:

至此,我们获取到了 jsapi_ticker;

获得jsapi_ticket之后,就可以生成JS-SDK权限验证的签名了

4、增加签名算法获取微信签名

签名算法

签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1sha1加密,字段名和字段值都采用原始值,不进行URL 转义。

说明:

noncestr随机字符串,一般自己生成

jsapi_ticket 从微信服务器或者自己的缓存中

timestamp时间戳自己生成

url当前页面的url,一定要动态获取,千万不要 hardcode

然后再按照字典排序,进行排序 jsapi_ticket&noncestr&timestamp&url

最后sha1 加密

signature=sha1(string1)。 示例:

noncestr=Wm3WZYTPz0wzccnW
jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3
timestamp=1414587457
url=http://mp.weixin.qq.com?params=value

步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1

jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg&noncestr=Wm3WZYTPz0wzccnW&timestamp=1414587457&url=http://mp.weixin.qq.com?params=value

步骤2. 对string1进行sha1签名,得到signature

0f9de62fce790f9a083d5c99e95740ceb90c27ed

注意事项

1.签名用的noncestrtimestamp必须与wx.config中的nonceStrtimestamp相同。

2.签名用的url必须是调用JS接口页面的完整URL

3.出于安全考虑,开发者必须在服务器端实现签名的逻辑。

如出现invalid signature 等错误详见附录常见错误及解决办法

代码如下:

/**
 * 获取签名
 * @returns:
 * 1. appId 必填,公众号的唯一标识
 * 2. timestamp 必填,生成签名的时间戳
 * 3. nonceStr 必填,生成签名的随机串
 * 4. signature 必填,签名
 */
const crypto = require('crypto');
const config = require('../config/index.json');

// sha1加密
function sha1(str) {
  let shasum = crypto.createHash("sha1")
  shasum.update(str)
  str = shasum.digest("hex")
  return str
}

/**
 * 生成签名的时间戳
 * @return {字符串}
 */
function createTimestamp() {
  return parseInt(new Date().getTime() / 1000) + ''
}

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

/**
 * 对参数对象进行字典排序
 * @param  {对象} args 签名所需参数对象
 * @return {字符串}    排序后生成字符串
 */
function raw(args) {
  var keys = Object.keys(args)
  keys = keys.sort()
  var newArgs = {}
  keys.forEach(function (key) {
    newArgs[key.toLowerCase()] = args[key]
  })

  var string = ''
  for (var k in newArgs) {
    string += '&' + k + '=' + newArgs[k]
  }
  string = string.substr(1)
  return string
}


module.exports = getSign = (params, res) => {

  /**
   * 签名算法
   * 签名生成规则如下:
   * 参与签名的字段包括noncestr( 随机字符串),
   * 有效的jsapi_ticket, timestamp( 时间戳),
   * url( 当前网页的URL, 不包含# 及其后面部分)。
   * 对所有待签名参数按照字段名的ASCII 码从小到大排序( 字典序) 后,
   *  使用URL键值对的格式( 即key1 = value1 & key2 = value2…) 拼接成字符串string1。
   * 这里需要注意的是所有参数名均为小写字符。 对string1作sha1加密, 字段名和字段值都采用原始值, 不进行URL 转义。
   */
  var ret = {
    jsapi_ticket: params.ticket,
    nonceStr: createNonceStr(),
    timestamp: createTimestamp(),
    url: params.url
  };
  console.log(params, ret);
  var string = raw(ret)
  ret.signature = sha1(string)
  ret.appId = config.appid;
  console.log('ret', ret)
  res.send(ret);
}

路由设置:

const express = require('express');
const api = require('./api');
const path = require('path');
const app = express();
//express请求别的路由中间件
require('run-middleware')(app);

//获取签名
app.get('/sign', (req, res) => {
  const params = {};
  console.log(req.query)
  params.url = req.query.url;
  /***
   * runMiddleware 请求别的 endPoint 获取 jsapi_ticket
   */
  app.runMiddleware('/getTicket', function (code, body, headers) {
    const result = JSON.parse(body);
    console.log('User ticket:', result.ticket);
    params.ticket = result.ticket;
    api.getSign(params, res);
  });

});

....

postman 请求如下:

这样我就获取了 签名 等一系列数据。

5、JSSDK 使用

微信JS-SDK说明文档

步骤一:绑定域名

先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。

备注:登录后可在“开发者中心”查看对应的接口权限。

步骤二:引入JS文件

在需要调用JS接口的页面引入如下JS文件,(支持https):res.wx.qq.com/open/js/jwe…

如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:res2.wx.qq.com/open/js/jwe… (支持https)。

备注:支持使用 AMD/CMD 标准模块加载方法加载

步骤三:通过config接口注入权限验证配置

所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)

wx.config({
    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    appId: '', // 必填,公众号的唯一标识
    timestamp: , // 必填,生成签名的时间戳
    nonceStr: '', // 必填,生成签名的随机串
    signature: '',// 必填,签名
    jsApiList: [] // 必填,需要使用的JS接口列表
});

步骤四:通过ready接口处理成功验证

wx.ready(function(){
    // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});

实战代码如下:

// promise
const getSignPromise = new Promise((resolve, reject) => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', location.origin + '/sign?url=' + location.href, true);
  xhr.send();
  xhr.onload = () => {
    if (xhr.readyState === xhr.DONE) {
      if (xhr.status === 200) {
        const result = JSON.parse(xhr.response);
        console.log(result);
        resolve(result);
      }
    }
  }
});

// 分享
getSignPromise.then(res => {
  getWeShare(res);
});

/***
 * 微信分享
 */
const getWeShare = (params) => {
  wx.config({
    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    appId: params.appId, // 必填,公众号的唯一标识
    timestamp: params.timestamp, // 必填,生成签名的时间戳
    nonceStr: params.nonceStr, // 必填,生成签名的随机串
    signature: params.signature, // 必填,签名
    jsApiList: [
      'checkJsApi',
      'onMenuShareTimeline',
      'onMenuShareAppMessage',
      'onMenuShareQQ',
      'onMenuShareWeibo',
      'hideMenuItems',
      'chooseImage',
      'updateAppMessageShareData',
      'scanQRCode'
    ] // 必填,需要使用的JS接口列表
  });

  wx.ready(function () { //需在用户可能点击分享按钮前就先调用
    const data = {
      title: '测试JSSDK', // 分享标题
      desc: '后端端口签名测试', // 分享描述
      link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
      imgUrl: 'http://www.***.cf/img/share.JPG', // 分享图标
      success: function () {
        // 设置成功
      }
    }
    wx.onMenuShareTimeline(data);
    wx.onMenuShareAppMessage(data);
  });
}

// 打开相册
document.getElementById('chooseImage').addEventListener('click', function (params) {
  wx.chooseImage({
    count: 1, // 默认9
    sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
    sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
    success: function (res) {
      var localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
      console.log(localIds);
    }
  });
  wx.scanQRCode({
    needResult: 0, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
    scanType: ["qrCode", "barCode"], // 可以指定扫二维码还是一维码,默认二者都有
    success: function (res) {
      var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
    }
  });
})

页面写好之后, 我们可以在 微信开发者工具里面看到:

我们也可以打开测试公众号,在里面调试,否则,我们没有权限调取 js 接口

至此,已经算是成功开发了,其他接口不再做尝试。

6、附录

调用config接口的时候传入参数 debug: true 可以开启debug模式,页面会alert出错误信息。以下为常见错误及解决方法:

1.invalid url domain当前页面所在域名与使用的appid没有绑定,请确认正确填写绑定的域名,仅支持80(http)443(https)两个端口,因此不需要填写端口号(一个appid可以绑定三个有效域名,见 ]目录1.1.1)。

2.·invalid signature签名错误。建议按如下顺序检查:

1.确认签名算法正确,可用mp.weixin.qq.com/debug/cgi-b… 页面工具进行校验。

2.确认confignonceStr(js中驼峰标准大写S), timestamp与用以签名中的对应noncestr, timestamp一致。

3.确认url是页面完整的url(请在当前页面alert(location.href.split('#')[0])确认),包括'http(s)://'部分,以及'?'后面的GET参数部分,但不包括'#'hash后面的部分。

4.确认 config 中的 appid 与用来获取 jsapi_ticket 的 appid 一致。

5.确保一定缓存access_tokenjsapi_ticket

6.确保你获取用来签名的url是动态获取的,动态页面可参见实例代码中php的实现方式。如果是html的静态页面在前端通过ajax将url传到后台签名,前端需要用js获取当前页面除去'#'hash部分的链接(可用location.href.split('#')[0]获取,而且需要encodeURIComponent),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。

3.the permission value is offline verifying这个错误是因为config没有正确执行,或者是调用的JSAPI没有传入config的jsApiList参数中。建议按如下顺序检查:

1.确认config正确通过。

2.如果是在页面加载好时就调用了JSAPI,则必须写在wx.ready的回调中。

3.确认config的jsApiList参数包含了这个JSAPI。

4.permission denied该公众号没有权限使用这个JSAPI,或者是调用的JSAPI没有传入config的jsApiList参数中(部分接口需要认证之后才能使用)。

5.function not exist当前客户端版本不支持该接口,请升级到新版体验。

6.为什么6.0.1版本config:ok,但是6.0.2版本之后不ok(因为6.0.2版本之前没有做权限验证,所以config都是ok,但这并不意味着你config中的签名是OK的,请在6.0.2检验是否生成正确的签名以保证config在高版本中也ok。)

7.在iOS和Android都无法分享(请确认公众号已经认证,只有认证的公众号才具有分享相关接口权限,如果确实已经认证,则要检查监听接口是否在wx.ready回调函数中触发)

8.服务上线之后无法获取jsapi_ticket,自己测试时没问题。(因为access_token和jsapi_ticket必须要在自己的服务器缓存,否则上线后会触发频率限制。请确保一定对token和ticket做缓存以减少2次服务器请求,不仅可以避免触发频率限制,还加快你们自己的服务速度。目前为了方便测试提供了1w的获取量,超过阀值后,服务将不再可用,请确保在服务上线前一定全局缓存access_token和jsapi_ticket,两者有效期均为7200秒,否则一旦上线触发频率限制,服务将不再可用)。

9.uploadImage怎么传多图(目前只支持一次上传一张,多张图片需等前一张图片上传之后再调用该接口)

10.没法对本地选择的图片进行预览(chooseImage接口本身就支持预览,不需要额外支持)

11.通过a链接(例如先通过微信授权登录)跳转到b链接,invalid signature签名失败(后台生成签名的链接为使用jssdk的当前链接,也就是跳转后的b链接,请不要用微信登录的授权链接进行签名计算,后台签名的url一定是使用jssdk的当前页面的完整url除去'#'部分)

12.出现config:fail错误(这是由于传入的config参数不全导致,请确保传入正确的appId、timestamp、nonceStr、signature和需要使用的jsApiList)

13.如何把jsapi上传到微信的多媒体资源下载到自己的服务器(请参见文档中uploadVoice和uploadImage接口的备注说明)

14.Android通过jssdk上传到微信服务器,第三方再从微信下载到自己的服务器,会出现杂音(微信团队已经修复此问题,目前后台已优化上线)

15.绑定父级域名,是否其子域名也是可用的(是的,合法的子域名在绑定父域名之后是完全支持的)

16.在iOS微信6.1版本中,分享的图片外链不显示,只能显示公众号页面内链的图片或者微信服务器的图片,已在6.2中修复

17.是否需要对低版本自己做兼容(jssdk都是兼容低版本的,不需要第三方自己额外做更多工作,但有的接口是6.0.2新引入的,只有新版才可调用)

18.该公众号支付签名无效,无法发起该笔交易(请确保你使用的jweixin.js是官方线上版本,不仅可以减少用户流量,还有可能对某些bug进行修复,拷贝到第三方服务器中使用,官方将不对其出现的任何问题提供保障,具体支付签名算法可参考 JSSDK微信支付一栏)

19.目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题已在Android6.2中修复

20.uploadImage在chooseImage的回调中有时候Android会不执行,Android6.2会解决此问题,若需支持低版本可以把调用uploadImage放在setTimeout中延迟100ms解决

21.require subscribe错误说明你没有订阅该测试号,该错误仅测试号会出现

22.getLocation返回的坐标在openLocation有偏差,因为getLocation返回的是gps坐标,openLocation打开的腾讯地图为火星坐标,需要第三方自己做转换,6.2版本开始已经支持直接获取火星坐标

23.查看公众号(未添加): "menuItem:addContact"不显示,目前仅有从公众号传播出去的链接才能显示,来源必须是公众号

24.ICP备案数据同步有一天延迟,所以请在第二日绑定