准备工作
微信公众号(订阅号||服务号)、备案过的域名(JS接口安全域名)、服务器(也可以用花生壳代替)
获取签名流程
-
通过公众号的appid&&appSecret获取accessToken
-
利用第一步的accessToken生成jsapi_ticket
-
再通过nonceStr(随机字符串)、jsapi_ticket、 timestamp(时间戳)、 url这四个字段组成的URL键值对格式的字符串作sha1加密(加密完就是我们想要的signature)
公众号配置
-
获取appid&&appSecret备用(开发=》基本配置)
-
设置获取access_token接口的IP白名单(此IP为跑wx_jssdk.js服务的服务器IP),如果未设置会报(invalid ip 122.234.183.166 ipv6 ::ffff:122.234.183.166, not in whitelist hint: [QgOCG.wgE-cCvqFa])错误(开发=》基本配置=》IP白名单)
-
设置JS接口安全域名(设置=》公众号设置=》功能设置=》JS接口安全域名), 将文件
MP_verify_ua74c9a3LTB08ARW.txt
上传此域名下的网站根目录,如果设置错误会报(invalid url domain)
nodejs代码实现
wx_jssdk.js
JAVASCRIPT
const Koa = require('koa');
const router = require("koa-router")();
const fs = require("fs");
const path = require("path");
const moment = require("moment");
const request = require("request");
var crypto = require("crypto");
const appid = "your appid";
const appSecret = "your appSecret";
const cors = require('koa2-cors'); //跨域处理
const app = new Koa();
app.use(
cors({
origin: function (ctx) { //设置允许来自指定域名请求
if (ctx.url === '/test') {
return '*'; // 允许来自所有域名请求
}
return '*'; //只允许http://localhost:8080这个域名的请求
},
maxAge: 5, //指定本次预检请求的有效期,单位为秒。
credentials: true, //是否允许发送Cookie
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], //设置所允许的HTTP请求方法
allowHeaders: ['Content-Type', 'Authorization', 'Accept'], //设置服务器支持的所有头信息字段
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'] //设置获取其他自定义字段
})
);
router.prefix('/jssdk')
const filePath = path.join(__dirname, "/wxconfig.json");
router.get("/wxconfig", async (ctx, next) => {
const req = ctx.request.url;
let nowUrl = decodeURIComponent(req.split('url=')[1]);
console.log(nowUrl)
// 定义两个函数返回Promise对象,用来组成串行,并最终获取到jsapi_ticket后最终处理成签名。
// 获取accessToken
const getToken = function () {
let p1 = new Promise((reslove, reject) => {
request(
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + appSecret,
function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body); // 注意返回的数据是一个纯字符串,要格式化处理
let token = JSON.parse(body).access_token;
if (token !== "") {
reslove(getJsapi(token));
}
}
}
);
});
return p1;
};
// 获取jsapi_ticket
const getJsapi = function (token) {
let p2 = new Promise((reslove, reject) => {
request(
"https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" +
token +
"&type=jsapi",
function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body); // 注意返回的数据是一个纯字符串,要格式化处理
// 存储当前 ticket
const ticketData = {
jsapi_ticket: "",
time: moment().format("YYYY/MM/DD HH:mm:ss")
};
if (JSON.parse(body).errcode === 0) {
// 如果成功获取到
ticketData.jsapi_ticket = JSON.parse(body).ticket;
/* 这里是将在这个同级目录下创建一个json文件用来存储jsapi_ticket,
和请求时间,用于下次接口被调用的过期校验。*/
fs.writeFile(filePath, JSON.stringify(ticketData), function (err) {
if (err) console.error(err);
console.log("写入ticketData的json文件成功!");
});
reslove(JSON.parse(body).ticket);
} else {
fs.writeFile(filePath, JSON.stringify(ticketData), function (err) {
if (err) console.error(err);
console.log("写入ticketData的json文件失败!");
});
}
}
}
);
});
return p2
.then(result => {
console.log(result);
// 在这里返回签名生成函数的结果给前台
let sendData = getSignature(nowUrl, result);
ctx.status = 200;
ctx.body = sendData;
})
.catch(err => {
console.log(err);
});
};
/* 这里是先判断存储json文件是否存在,若不存在或者文件存在但已过期,
就调用上方的串行函数,直接返回生成的签名给前台。若文件存在没过期,
直接使用json文件中的jsapi_ticket生成签名返回给前台使用。*/
if (fs.existsSync(filePath)) {
console.log("文件路径存在");
// 先读取
const jsapiData = JSON.parse(fs.readFileSync(filePath));
console.log(jsapiData);
// 先判断时间是否过期,若不过期传key,过期不传key
let t1 = jsapiData.time; // 数据,必须是2018/12-/01 12:09:04这种格式,否则Date对象无法转换
let dateBegin = new Date(t1); // 转化为Date对象的形式
let dateEnd = new Date(); //当前时间数据
let dateDiff = dateEnd.getTime() - dateBegin.getTime(); //时间差的毫秒数
// console.log(Math.floor(dateDiff / 1000))
if (Math.floor(dateDiff / 1000) > 7198) {
// 缓存时间超过有效期(过期)
sendData = await getToken();
} else {
// 不过期,调用签名生成函数生成结果直接ctx返回给前台
let signaData = await getSignature(nowUrl, jsapiData.jsapi_ticket);
ctx.status = 200;
ctx.body = signaData;
}
} else {
console.log("文件路径不存在");
sendData = await getToken();
}
});
// 生成签名函数
/* 签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1 = value1 & key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。 */
const getSignature = function (nowUrl, key) {
let noncestr = Math.random()
.toString(36)
.substr(2); // 随机字符串
let timestamp = moment().unix(); // 获取时间戳,数值类型
let jsapi_ticket = `jsapi_ticket=${key}&noncestr=${noncestr}×tamp=${timestamp}&url=${nowUrl}`;
// console.log(jsapi_ticket)
jsapi_ticket = getSha1(jsapi_ticket);
return {
appid: appid,
noncestr: noncestr,
timestamp: timestamp,
signature: jsapi_ticket
};
};
/**
* @sha1加密模块 (加密固定,不可逆)
* @param str string 要加密的字符串
* @retrun string 加密后的字符串
* */
const getSha1 = function (str) {
var sha1 = crypto.createHash("sha1"); //定义加密方式:md5不可逆,此处的md5可以换成任意hash加密的方法名称;
sha1.update(str);
var res = sha1.digest("hex"); //加密后的值d
return res;
};
app.use(router.routes()); /*启动路由*/
app.use(router.allowedMethods());
/*
* router.allowedMethods()作用: 这是官方文档的推荐用法,我们可以
* 看到 router.allowedMethods()用在了路由匹配 router.routes()之后,所以在当所有
* 路由中间件最后调用.此时根据 ctx.status 设置 response 响应头
*
*/
app.listen(3003, () => {
console.log('正在监听端口3003');
});
// module.exports = router;
package.json
JSON{
"name": "node-service",
"devDependencies": {},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^8.0.8",
"koa2-cors": "^2.0.6",
"moment": "^2.24.0",
"request": "^2.88.2"
}
}
部署服务器
服务器安装NODE 、PM2,
将项目上传至服务器,
执行npm install
,
进程守护pm2 start node wx_jssdk.js
校验
微信文档:js-sdk说明文档
在线验证签名:微信 JS 接口签名校验工具
划重点
-
获取access_token接口时要在 公众号后台=》开发=》基本配置=》IP白名单 里添加访问的IP
-
一定要记得设置JS接口安全域名
-
服务器上跑node服务最好用进程守护,否则进程意外中断不再会重启