uni-app+uniCloud开发微信公众号H5网页如何使用云函数计算jssdk的签名,以及invalid signature 和 realUrl的问题

前天突然接到一个小活儿,客户的要求很简单,但要求快速上线
这时候uniCloud的优势就出来了,不用考虑服务器的事儿,直接走前端托管即可
但中间遇到的坑是真的多
下边一个个来说

首先,uni-app本身更倾向于小程序类的开发,关于公众号网页,尤其是jssdk这块几乎没有任何封装;
其次,uniCloud后端的接口也是小程序相关的,想做公众号jssdk的签名,得自己写接口(反正我搜了一圈没有,这也应该算是首发了, )

好了,客户的要求很简单,
需求1是要能获取用的头像和昵称
需求2是要能实现卡片式分享

so,
需求1很简单,uniCloud的userCenter已经封装了公众号登录,做好配置即可直接调用

但这里有个坑点:
uni-app的h5配置中,有【路由模式】的配置,这里必须选history,否则你会在获取code的时候崩溃。
看一下代码:

onLoad(options={}){
	if(!options.code){
		// 没有code传入的时候,直接走静默获取code
		let scope = 'snsapi_userinfo';
		let appid = ''; // 填写公众号的appid,服务号哦,订阅号即时认证了也没用
		let redirect_uri = window.location.href.split('?')[0]; // 这里就是坑1,你如果选hash就废了
		let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`;
		window.location.href = url;
		// 上边这段执行之后,会自动跳转到要求用户登录的页面(获取头像、昵称)
		// 用户同意后,会跳转回你原本的页面,同时在最后带上‘?code=xxxxxxxxxxxxx’
		// 还是要说坑点,如果你是hash路由,你压根获取不到这个code,然后就会无限循环这里
		// 而如果获取到了,则跳过这个if
	}
}

好,获取到code之后,跟着uniCloud文档就很简单可以完成用户登录、数据入库了,这里就不继续说。

主要就是需求2比较麻烦,我们来看一下:
首先,我们要引入jssdk,uni-app官方在社区贴了一个封装好的,我们直接npm拿来用

npm install jweixin-module --save  

然后我们就可以用这个jssdk来配置获取分享接口,本来到这里我觉得我应该分分钟搞定了,结果是噩梦的开始。

先看代码:

var wxjssdk = require('jweixin-module');
var url = encodeURIComponent(window.location.href.split('#')[0]); 
// 获取当前页面的url中#前面那部分
// 这里就是需求2的坑1了,如果你不encodeURI,那就会出现很多奇葩的问题

var config = { url };
// 下边这个请求是vk-router的一个写法,就当我是请求了云函数就行
// 这个云函数是为了获取jssdk的签名
vk.callFunction({
	url: 'client/common/pub/getSignature',
	data: config // 这里需要把当前url传到后端,几乎9成9的bug都是这里出的,务必注意encodeURI
}).then(response => {
	// 如果这里你正常获取到签名了,不要太高兴,往下看
	let wxconfig = {
		debug: false,
		appId: '', // 必填,公众号的唯一标识
		// 首先这个时间戳一定要后端来生成,传回前端来验证
		timestamp: response.data.timestamp, 
		// 其次,历史大坑,一会儿看云函数的代码你就知道了
		// 生成签名和验证签名的两个参数的S,一个大写一个小写你敢信?
		nonceStr: response.data.nonceStr, 
		signature: response.data.signature, // 必填,你后端计算好的签名
		// 必填,需要使用的JS接口列表,这里分别是分享给朋友和分享到朋友圈两个接口
		jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'] 
	};
	
	// 以上全部正确之后,开始配置jssdk
	wxjssdk.config(wxconfig);
	// 完成之后会有一个ready的回调
	wxjssdk.ready(function() {
		wxjssdk.updateAppMessageShareData({
			title: '', // 分享标题
			desc: '',
			link: '', // 分享链接,该链接域名或路径必须与当前页面对应的公众号 JS 安全域名一致
			imgUrl: '', // 分享图标
			success: function() {
				// 设置成功
			}
		});
		wxjssdk.updateTimelineShareData({
			title: '', // 分享标题
			link: '', // 分享链接,该链接域名或路径必须与当前页面对应的公众号 JS 安全域名一致
			imgUrl: '', // 分享图标
			success: function() {
				// 设置成功
			}
		});
	});
});

好,这代码其实很简单,但里边有以下几个重大问题
1.传给后台的url一定要encode
2.时间戳一定要后端来生,11位
3.验证时的nonceStr的S是大写,后端计算的时候是小写
4.url的域名要先在公众号后台添加

继续看云函数部分
这里依旧,vk-router封装了很多东西,所以里边的写法和直接用uniCloud不太一样,理解万岁。

// 先把url从请求中取出来
let {
	url
} = data;

// 然后decode
url = decodeURIComponent(url);

// 初始化我们要返回的数据
res.data = {};

// 生成一个32位的字符串,这个就随机就行,你用啥方法都无所谓
let nonceStr = vk.pubfn.random(32, "abcdefghijklmnopqrstuvwxyz0123456789");
// 11位的时间戳
let timestamp = Date.parse(new Date()) / 1000;

// 赋值
res.data.nonceStr = nonceStr;
res.data.timestamp = timestamp;

// 这里要注意了
// 我们计算签名的逻辑是,先请求一个accessToken,再换一个jsTicket,最后再计算签名
// 而这里accessToken和jsTicket都是2小时有效,且每天有请求上线,所以要自己做缓存
// 但是uniCloud或者说云开发都没有cache这一说
// 所以这里我直接存到数据表里去了
let jsTicket;
// 这个写法也不用管,就是封装了数据库的查询
let oldJSTicket = await vk.baseDao.selects({
	dbName: "bwin-mp",
	getOne: true,
	getMain: true,
	// 主表where条件
	whereJson: {
		name: 'jsTicket'
	},
	sortArr: [{ name: 'expire_time', type: 'desc' }]
});
// 反正最终就是按有效期倒序取了最后一条
if (vk.pubfn.isNotNull(oldJSTicket) && oldJSTicket.expire_time > timestamp) {
	// 如果有效,这里就可以直接计算签名了
	jsTicket = oldJSTicket.ticket;
	// 计算签名我封装了一个方法,写在最后了
	res.data.signature = await pubFun.createWXSignature(nonceStr, jsTicket, timestamp, url);
	res.msg = '已获取数据库中的ticket';
	return res;
	// 完事儿,这是最简单的逻辑,就是jsTicket没有失效的情况
}

// 如果jsTicket失效了,就继续判断accessToken是否有效
let accessToken;
// 请求access_token
let oldAccessToken = await vk.baseDao.selects({
	dbName: "bwin-mp",
	getOne: true,
	getMain: true,
	// 主表where条件
	whereJson: {
		name: 'accessToken'
	},
	sortArr: [{ name: 'expire_time', type: 'desc' }]
});

let jsTicketRes;
// 判断是否过期
if (vk.pubfn.isNotNull(oldAccessToken) && oldAccessToken.expire_time > timestamp) {
	// 有效,就直接使用本token请求新的jsTicket
	accessToken = oldAccessToken.token;
	let apiUrl =
		`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`
	// 这个请求就是原生的云函数访问外部的写法
	jsTicketRes = await uniCloud.httpclient.request(apiUrl, {
		method: 'GET',
		contentType: 'json', // 指定以application/json发送data内的数据
		dataType: 'json' // 指定返回值为json格式,自动进行parse
	})

	if (jsTicketRes.errmsg !== 'ok') {
		return { code: -1, msg: jsTicketRes.errmsg }
	}
	
	// 这里注意,中间有一层data
	jsTicket = jsTicketRes.data.ticket;
	// 获取到之后我存起来到数据库方便下次用
	await vk.baseDao.add({
		dbName: "bwin-mp",
		dataJson: {
			name: 'jsTicket',
			ticket: jsTicket,
			expire_time: (parseInt(timestamp) + 7200) // 2小时有效
		}
	});

	// 计算签名
	res.data.signature = await pubFun.createWXSignature(nonceStr, jsTicket, timestamp, url);
	res.msg = '已获取新的ticket';
	return res;
}

// 如果accessToken也过期了
// 就重新获取access_token
let apiUrl =
	'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=wxa14d3d2611883509&secret=03b6d6aeab6952f02425162a79221013';
let accessTokenRes = await uniCloud.httpclient.request(apiUrl, {
	method: 'GET',
	contentType: 'json', // 指定以application/json发送data内的数据
	dataType: 'json' // 指定返回值为json格式,自动进行parse
})

// 这里也存到数据库去
accessToken = accessTokenRes.data.access_token;
await vk.baseDao.add({
	dbName: "bwin-mp",
	dataJson: {
		name: 'accessToken',
		token: accessToken,
		expire_time: (parseInt(timestamp) + 7200)
	}
});

// 然后用这个新的accessToken去换取jsTicket
apiUrl =
	`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`
jsTicketRes = await uniCloud.httpclient.request(apiUrl, {
	method: 'GET',
	contentType: 'json', // 指定以application/json发送data内的数据
	dataType: 'json' // 指定返回值为json格式,自动进行parse
})

jsTicket = jsTicketRes.data.ticket;
await vk.baseDao.add({
	dbName: "bwin-mp",
	dataJson: {
		name: 'jsTicket',
		ticket: jsTicket,
		expire_time: (parseInt(timestamp) + 7200)
	}
});

// 最后再计算签名
res.data.signature = await pubFun.createWXSignature(nonceStr, jsTicket, timestamp, url);
res.msg = '已获取新的ticket和token';

上边这个云函数实际上并不复杂,核心逻辑就是依次判断jsTicket是否过期、token是否过期,以及对应去不同的接口请求新的过来。
最后再计算出有效的签名:

// 这是上边云函数调用的那个计算方法
// 里边也一样,vk给封装好了md5
createWXSignature = function(noncestr, jsTicket, timestamp, url) {
const crypto = require('crypto');
const md5 = crypto.createHash('sha1');

let res = {
	noncestr: noncestr, // 看到这儿了么,这里你计算的时候键名里的s是小写的
	jsapi_ticket: jsTicket,
	timestamp: timestamp,
	url: url
}

// 排序
var keys = Object.keys(res)
keys = keys.sort()
var newArgs = {}
keys.forEach(function(key) {
	newArgs[key.toLowerCase()] = res[key]
})

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

// md5
signature = md5.update(string).digest('hex');
return signature;
}

这个计算签名的环节有几个坑:
1.一定要先把微信后台域名绑定的地方都绑定上,别弄错;
2.一定要添加IP白名单,并且,重点!!!!
阿里云的空间没有固定IP,所以你只能用腾讯云的收费空间!
我在这里卡了好久,就死活获取不到AccessToken,把res打印出来才发现是白名单的锅,又把云空间折腾到腾讯云去,本来还想省点钱,这可好……
3.最大的坑,那个随机字符串的S的大小写,我真的是服死,这到底是什么脑回路?

这样最后就算完事儿了,你传回来的签名被jssdk.config配置之后,就会进入ready,用户调用分享时,就会使用你设置的那些字段内容(链接上加个参数,就可以做裂变了)。

最后附上 腾讯官方的签名校验工具,你自己计算完不能用,就把参数复制过去看看结果是否一致,
uni-app+uniCloud开发微信公众号H5网页如何使用云函数计算jssdk的签名,以及invalid signature 和 realUrl的问题_第1张图片

如果结果一致,但开发者工具就死活报错invalid signature,那9成9是url没有传对导致的,
其表现为:开发者工具报invalid signature、手机端弹窗realUrl xxx

好了,没想到好久不写分享,一下就干了三篇出来,
今天就到这儿了,继续撸代码去。

你可能感兴趣的:(uniapp,前端,javascript,微信)