大家好,我是小鑫同学。一位从事过 Android 开发、混合开发,现在长期从事前端开发的编程爱好者,我觉得在编程之路上最重要的是知识的分享,所谓三人行必有我师。所以我开始在社区持续输出我所了解到、学习到、工作中遇到的各种编程知识,欢迎有想法、有同感的伙伴加我 fe-xiaoxin 微信交流~
写作背景:
💢第一坑来自 ali 云,ali 云的函数计算 FC 产品在使用模板创建项目的过程中,如下图中的服务名和函数名每次创建都是一样的,我们很容易就按默认的创建了,但是多次创建会覆盖前一次的内容,我写好的授权接口就被覆盖了~
💘ali 的产品原来也有这种低级的错误,不管你是产品设计的缺陷还是系统开发的缺陷,这都是致命的,因为正常运行的服务丢了~
💥第二坑来自 WeChat,在 ali 云调试好的代码丢就丢了吧,我就打算还在 ali 云上开始重新来一遍,靠着搜索到的网站还没关掉,就写完了第二遍云上的代码,接口测试可以返回信息,但是【折】在了前端 wx.config 函数的校验问题上,写过的都知道文档提供了校验失败的排查过程,你要是能按这个解决了,微信开发社区就不会提 5k+近似重复的问题而没有结果了~
💤写代码就应该【有始有终】我还是在本地搭建全套环境来再搞一遍试试看,请熟悉下面各个地址的快速上手文档来搭建环境~
服务端:Egg.js
前端:Vue3
微信:公众号测试平台
内网穿透:Natapp
代码实操:
第一步,启动 Natapp:
在 natapp 平台找到免费提供的隧道进行配置更改:将7001
端口进行映射,7001
是 eggjs 项目默认端口;
在下载好 natapp 客户端(window)后在客户端同级目录编写config.ini
配置文件并将平台提供的免费隧道键入到配置文件中,格式同:authtoken=<your authtoken>
;
配置后双击客户端如下图就启动正常了,免费隧道的 IP 随时可能会变,开发中需要注意~
第二步,先搞前端部分:
使用yarn create vite
创建一个前端项目,用着顺手就行,为的就是快~
安装axios
来发送授权信息获取请求;
微信的 JS-SDK 既然没看到官方提供依赖包那就用配置到 index.html,<script src="[http://res2.wx.qq.com/open/js/jweixin-1.6.0.js"></script>](http://res2.wx.qq.com/open/js/jweixin-1.6.0.js"></script>)
在模板中我们编写少量的代码来显示授权的状态:
<template>
<h3 v-if="message">{{ message }}</h3>
<h3 v-else>
点击右上角=>分享给朋友
<h5>{{ status }}</h5>
</h3>
</template>
复制代码
我们通过axios
获取/signature
接口的数据,为了避免 url 参数的传递可能存在编码的问题,那我们稍后在设计/signature
接口的时候 url 参数通过 body 传递来避免,授权数据拿到后就是wx.config
执行验证了~
<script setup>
import { ref } from "vue";
import axios from "axios";
const message = ref("");
const status = ref("");
axios
.post(`http://t2fzbb.natappfree.cc/signature`, {
url: location.href.split("#")[0],
})
.then((res) => {
const { appId, nonceStr, signature, timestamp } = res.data;
console.log("[ res ] >", appId, nonceStr, signature, timestamp);
wx.config({
debug: true, // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
appId, // 必填,公众号的唯一标识
timestamp, // 必填,生成签名的时间戳
nonceStr, // 必填,生成签名的随机串
signature, // 必填,签名
jsApiList: ["updateAppMessageShareData"], // 必填,需要使用的 JS 接口列表
});
wx.error((res) => {
message.value = res.errMsg;
});
wx.ready(() => {
wx.checkJsApi({
jsApiList: ["updateAppMessageShareData"], // 需要检测的 JS 接口列表,所有 JS 接口列表见附录2,
success: (res) => {
wx.updateAppMessageShareData({
title: "我的掘金", // 分享标题
desc: "我在掘金输出前端知识~", // 分享描述
link: "https://juejin.cn/user/3966693685871694", // 分享链接,该链接域名或路径必须与当前页面对应的公众号 JS 安全域名一致
imgUrl:
"https://p9-passport.byteacctimg.com/img/user-avatar/71ca4682501063257d8413ff726105a0~300x300.images", // 分享图标
success: function () {
status.value = "设置成功";
},
});
},
});
});
});
</script>
复制代码
最终我们要在微信开发者工具的公众台看到一堆 ok 就说明可以了~
第三步,编写服务端接口:
🤣在开始前有一个问题,微信的各种票据都需要我们自己做缓存处理,仅在超时后再刷新,要不会遇到调用次数超限制的麻烦,服务端缓存票据我最先想到的是 Redis,但是多一份配置还是挺麻烦的,后来想到的是读写文件,我在【ali 云】的两次就是通过读写文件来实现的,毕竟是 Demo,效率靠后呗~😋最后我想到的是服务运行后我可以使用全局对象来缓存数据呀,并不需要借助额外的渠道来实现,我们以前的单利模式就是在需要的时候再实例化对象的呀💗
编写个接口的 Controller 层:
checkOrigin:用来在公众号测试平台发起验证服务的请求来验证数据来源;
token:用来获取access_token
,我会在 server 层做access_token
的对象缓存;
ticket:用来获取ticket
,这个也会在 server 层做对象缓存;
signature:用来生成验签,并返回 wx.config 需要配置数据,期间需要一次获取access_token
和ticket
,可以看到url
我们是通过body
获取的。
import { Controller } from 'egg';
export default class HomeController extends Controller {
public async checkOrigin() {
const { ctx } = this;
const { signature, timestamp, nonce, echostr } = ctx.query;
const result = await ctx.service.weChat.checkSignature(
signature,
timestamp,
nonce,
);
ctx.body = result ? echostr : '';
}
public async token() {
const { ctx } = this;
ctx.body = await ctx.service.weChat.getToken();
}
public async ticket() {
const { ctx } = this;
ctx.body = await ctx.service.weChat.getTicket();
}
public async signature() {
const { ctx } = this;
const { url } = ctx.request.body;
console.log('[ url ] >', url);
ctx.body = await ctx.service.weChat.genSignature(url);
}
}
复制代码
编写 Service 的第一个函数 checkSignature:
💕需要接口微信平台发送来的 signature,timestamp,nonce,在测试平台我们配置了个自定义的 TOKEN,我们需要将【TOKEN, timestamp, nonce】进行 sort 排序后拼接成一个字符串,并使用 SHA1 加密算法(这里使用的 crypto-js 依赖)来生成我们自己的验签,将我们的生成的验签和平台发送过来的 signature 对比并将结果返回到 Controller,并响应微信测试平台~
public async checkSignature(
signature: string,
timestamp: string,
nonce: string,
) {
const own = SHA1([ TOKEN, timestamp, nonce ].sort().join('')).toString();
return own === signature;
}
复制代码
编写 Service 的第二个函数 getToken:
我在服务的最开始定义了一个 cache 对象来缓存 token;
当 token 中的access_token
字段不存在时就需要使用上下文对象中的curl
来发起请求获取一份新的access_token
数据;
在拿到新数据后通过Date.now() + expires_in * 1000
得到一个失效的时间并赋值给last_time
;
当前的Date.now()
大于存储的last_time
时需要再次刷新access_token
。
const cache: {
token: {
access_token: string | undefined;
expires_in: number;
last_time: number;
};
} = {
token: {
access_token: undefined,
expires_in: 0,
last_time: 0,
}
};
public async getToken() {
const { ctx } = this;
const request = async () => {
const { data } = await ctx.curl(URL_TOKEN, { dataType: 'json' });
const { access_token, expires_in } = data;
console.log('[ token data ] >', data);
cache.token = {
access_token,
expires_in,
last_time: Date.now() + expires_in * 1000,
};
};
if (!cache.token.access_token) {
console.log('[ token 还没有~ ] ');
await request();
} else {
if (Date.now() > cache.token.last_time) {
console.log('[ token 超时了 ] ');
await request();
}
}
return cache.token;
}
复制代码
编写 Service 的第三个函数 getTicket:
🤖getTicket 和 getToken 的缓存逻辑一致,区别在于 getTicket 发起请求时需要携带access_token
,其它直接看代码~
const cache: {
token: {
access_token: string | undefined;
expires_in: number;
last_time: number;
};
ticket: {
ticket: string | undefined;
expires_in: number;
last_time: number;
};
} = {
token: {
access_token: undefined,
expires_in: 0,
last_time: 0,
},
ticket: {
ticket: undefined,
expires_in: 0,
last_time: 0,
},
};
public async getTicket() {
const { ctx } = this;
const request = async () => {
const { access_token } = await this.getToken();
const { data } = await ctx.curl(URL_TICKET, {
dataType: 'json',
method: 'GET',
data: {
access_token,
},
});
const { ticket, expires_in } = data;
console.log('[ ticket data ] >', data);
cache.ticket = {
ticket,
expires_in,
last_time: Date.now() + expires_in * 1000,
};
};
if (!cache.ticket.ticket) {
console.log('[ ticket 还没有~ ] ');
await request();
} else {
if (Date.now() > cache.ticket.last_time) {
console.log('[ ticket 超时了 ] ');
await request();
}
}
return cache.ticket;
}
复制代码
编写 Service 的最后一个函数 genSignature:
调用上一次函数得到jsapi_ticket
;
生成随机字符串 noncestr:Math.random().toString(36).substr(2, 15)
;
生成时间戳 timestamp:Date.now()
;
验签生成在官网描述中需要:
对所有待签名参数按照字段名的 ASCII 码从小到大排序(字典序);
使用 URL 键值对的格式(即 key1=value1&key2=value2…)拼接成字符串 string1;
需要注意的是所有参数名均为小写字符;
对 string1 作 sha1 加密,字段名和字段值都采用原始值,不进行 URL 转义;
官网的描述太麻烦了,我们直接按 ASCII 码从小到大凭借模板字符串,排序、键值对格式、大小写这些注意事项就都省了,我们只需要按第一个函数中做一次SHA1
加密并toString()
得到验签字符串就可以 le~
组装前端wx.config
中需要的四个参数并返回~
public async genSignature(url: string) {
const { ticket: jsapi_ticket } = await this.getTicket();
const noncestr = Math.random().toString(36).substr(2, 15);
const timestamp = Date.now();
const signature = SHA1(
`jsapi_ticket=${jsapi_ticket}&noncestr=${noncestr}×tamp=${timestamp}&url=${url}`,
).toString();
return {
appId: APPID,
nonceStr: noncestr,
timestamp,
signature,
};
}
复制代码
微信测试号平台:
接口配置信息:
💥需要调用我们的/checkOrigin
,Token 字段为我们自定义的内容,和服务中验证签名时使用的一致就可以了,配置提交后会向我们的服务发起 GET 请求,并通过 query 携带参数给我们,当我们验证通过后需要将平台传递过来的随机字符返回回去代表我们验证通过~
JS 接口安全域名:
💥这个因为我们在微信开发者工具中调试前端页面用的localhost
,所以需要将localhost
配置在这个地方~
未解决 微信开放社区 wx.config 提示 invalid signature ?
总结:
💯代码的编写还是很容易的,就是和这种大平台沟通成本太高,每次编写都有新坑要踩,最后我将代码的配置信息剔除后上传到GitHub,需要的 jym 可以下载跑跑看~
如果看完觉得有收获,欢迎点赞、评论、分享支持一下。你的支持和肯定,是我坚持写作的动力~
最后可以关注我 @小鑫同学,共同进步(还可以帮你 fix🐛)~
评论