准备阶段
注册微信测试号 mp.weixin.qq.com/debug/cgi-b…
内网穿透 选择一个可用的
创建koa项目
项目结构
.
├── doc 文档
├── logs 日志
└── src
├── config 配置文件
├── controllers 控制器
│ └── service
├── lib 部分依赖
├── models 模型
├── public 静态文件
│ └── uploads
├── routes 路由
└── utils 工具
server配置
修改接口配置信息需要后台返回echostr参数 需开发下面的WXauth接口接口
修改接口配置信息 填写本地服务器URL和token
微信服务器会发送一个GET请求
根据请求参数 字典序后 sha1加密 进行比对 如果一致则返回 echostr
WXauth接口
WXauth(ctx) {
console.log("--WXauth--");
const { signature, echostr, timestamp, nonce } = ctx.query;
const sha1Str = sha1([timestamp, nonce, token].sort().join(""));
if (sha1Str === signature) {
ctx.body = echostr;
} else {
ctx.status = 401;
ctx.body = "error";
}
}
处理公众号消息
微信服务器会发送一个POST请求
1.根据请求参数 字典序后 sha1加密 进行比对 如果一致则 证明是来自微信的请求
2.将请求体中的数据 由xml转为json
3.调用 处理用户发送的消息类型和内容组件 根据 配置规则 return 返回内容
4.调用 消息模板组件 将返回内容组合为xml后 return 返回内容
5.将内容响应
// 处理微信消息
async WXMsg(ctx) {
const { signature, timestamp, nonce } = ctx.query;
const sha1Str = sha1([timestamp, nonce, token].sort().join(""));
if (sha1Str !== signature) {
ctx.status = 401;
ctx.body = "error";
} else {
let xmlData = "";
xmlData += ctx.request.body;
// 将xml数据解析为js对象
let Msg = await parseXMLAsync(xmlData);
Msg = formatMessage(Msg);
const options = reply(Msg);
// 最终回复用户的信息
let replyMessage = template(options);
ctx.body = replyMessage;
}
}
此次校验环节与上次相同 应封装为auth方法
获取AccessToken
凭借AccessToken可以调用微信公众号后台功能 如设置公众号菜单等
后续生成JsSdkSignature也需要这个参数
1.通过 appid appsecret 组和url 请求后获得 access_token 和 expires_in(有效时间)
2.access_token 有效期为2小时,一天只能请求2000次,需要做缓存
async getAccessToken() {
// 定义请求地址
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`;
const { access_token, expires_in } = await rp({
method: "GET",
url,
json: true,
});
const time = Date.now() + (expires_in - 300) * 1000;
const wxAToken = { key: "accessToken", value: access_token, time };
return wxAToken;
}
调用微信后台功能接口
1.此处 ctx.state.wxToken 为 上一步获取的access_token
调用创建菜单
前端需要发送 公众号菜单的JSON格式结构
// 创建菜单
async creatMenu(ctx) {
console.log("--创建菜单--");
// 定义请求的地址
const url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${ctx.state.wxToken}`;
// 发送请求
const res = await rp({
method: "POST",
url,
json: true,
body: ctx.request.body,
});
if (res.errcode === 0) {
ctx.body = {
status: 200,
msg: "创建成功",
data: res,
};
} else {
ctx.body = {
status: 400,
msg: "创建失败",
data: res,
};
}
console.log(res);
}
调用删除菜单
前端需要发送 公众号菜单的JSON格式结构
// 删除菜单
async deleteMenu(ctx) {
console.log("--删除菜单--");
// 定义请求地址
const url = `https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=${ctx.state.wxToken}`;
// 发送请求
const res = await rp({
method: "GET",
url,
json: true,
});
if (res.errcode === 0) {
ctx.body = {
status: 200,
msg: "删除成功",
data: res,
};
} else {
ctx.body = {
status: 400,
msg: "删除失败",
data: res,
};
}
console.log(res);
}
获取jsApiSignatur
如果要在微信公众号网页内调用js-sdk需要获取jsApiSignatur
1.设置JS接口安全域名 不能加协议名 如 http://
2.根据access_token组合url请求后获得ticket
同样只有2小时有效期 需要做缓存
async getJsApiTicket(access_token) {
// 定义请求地址
const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${access_token}&type=jsapi`;
let { ticket, expires_in } = await rp({ method: "GET", url, json: true });
const time = Date.now() + (expires_in - 300) * 1000;
const JSTicket = { key: "jsTicket", value: ticket, time };
return JSTicket;
}
3.开发getJsSdkSignature接口 前端需要使用返回的参数 通过config接口注入权限验证配置
参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。
// 获取jsApiSignatur
getJsSdkSignature(ctx) {
console.log("--获取jsSdkSignatur--");
const noncestr = Math.random().toString().split(".")[1];
const timestamp = Date.now();
const signatureArr = [
`noncestr=${noncestr}`,
`jsapi_ticket=${ctx.state.jsToken}`,
`timestamp=${timestamp}`,
`url=${ctx.query.url}`,
];
// 字典序 以&拼接
const signatureStr = signatureArr.sort().join("&");
const signatur = sha1(signatureStr);
ctx.body = {
status: 200,
msg: "获取成功",
data: {
noncestr,
timestamp,
signatur,
appid,
},
};
console.log(signatur);
}
4.前端拿到签名及其他参数后申请权限
jsSdkGet() {
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: this.WXdata.appid, // 必填,公众号的唯一标识
timestamp: this.WXdata.timestamp, // 必填,生成签名的时间戳
nonceStr: this.WXdata.noncestr, // 必填,生成签名的随机串
signature: this.WXdata.signatur, // 必填,签名
jsApiList: [
"updateAppMessageShareData",
"updateTimelineShareData",
"onMenuShareAppMessage",
"onMenuShareQQ",
"chooseImage",
"getNetworkType",
"getLocation",
"scanQRCode",
"openLocation",
"openAddress",
], // 必填,需要使用的JS接口列表
});
wx.ready((res) => {
console.log("--wx--ready--");
console.log(res);
});
},
微信登陆
1.前端向后端微信登陆接口发起请求
auth() {
window.location.href = "/wx/WXAuthorize";
},
2.后端收到请求后组合url并重定向到该url
跳转后需要用户手动授权
授权后重定向至 设置的回调地址
WXAuthorize(ctx) {
console.log("--WXAuthorize-- ");
// state 可以携带参数 微信回调时仍会携带
const state = ctx.query.id;
console.log("ctx..." + ctx.href);
// 回调地址
let redirectUrl = "http://2dad98c2.cpolar.io/wx/WXCallback";
// 授权方式 静默授权 or 提示授权
const scope = "snsapi_userinfo";
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
ctx.redirect(url);
}
3.编写微信回调接口 此时微信已经携带了code
本地服务器拿到Code 组合 appid appsecret Code
获得 access_token, refresh_token, openid, scope
携带OpenID 返回前端
async WXCallback(ctx) {
console.log("--WXCallback--");
const code = ctx.query.code; // 授权码
// 通过code换取网页授权 access_token
const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${appsecret}&code=${code}&grant_type=authorization_code`;
// 获得 user_access_token, refresh_token, openid, scope
let res = await rp({
method: "GET",
url,
json: true,
});
console.log(res);
// 返回前端并携带opendi
// Openid 用户唯一标识
// 每个appid 对应有自己的access_token(user_access_token)
ctx.redirect("/?openid=" + res.openid);
}
此时获取的信息
{
access_token: '38_aweFz-5puKajzU1ragwaufxfPCHzJQu6vmGSOsQLEF1nlkvGwCW_BJybmNB_wVnLWsQiHuJY7UyBY93A0nK7pA',
expires_in: 7200,
refresh_token: '38_CMWsQi0GbFmLm3Nq9F3PjtpxfUdW3tDmuvXJGgIAUFqItEY4lSgiXkl-G3opXyMXvrLeBEBuMEzB5Lkk8wDjJQ',
openid: 'oLGmwxImbJsLroS---------------',
scope: 'snsapi_userinfo'
}
4.前端根据openid请求用户信息
此时前端URL 2dad98c2.cpolar.io/?openid=oLG… web端请求方法
getUser() {
// 获取url
let arr = location.search.slice(1).split("&");
let qs = {};
arr.forEach((item) => {
let itemArr = item.split("=");
qs[itemArr[0]] = itemArr[1];
});
const openid = qs.openid;
console.log(qs.openid);
axios
.get(`${this.baseUrl}/wx/getUserInfo`, {
params: {
openid,
},
})
.then((res) => {
console.log(res.data);
})
.catch((e) => {
console.log("获取数据失败");
});
},
5.后端根据openID查询对应的user_access_token 拼接URL请求微信服务器获得用户信息
async getUserInfo(ctx) {
console.log("--getUserInfo--");
// 接收openID
const openId = ctx.query.openid;
// 回调时通过code获取的access_token
// TODO 此处应改为从数据库中查询 这里直接复制了上个方法的返回结果了😄
const ASToken = '38_m3i_N1WejF3gm6a1b_L3W_MATTqrXt9hHUw3wyPb6-UgMCMWoeGzu3ffbGxkc_0oGFakQRp7OQ0JUWzjZAL56Q'
const url = `https://api.weixin.qq.com/sns/userinfo?access_token=${ASToken}&openid=${openId}&lang=zh_CN`;
let res = await rp({
method: "GET",
url,
json: true,
});
ctx.body = res
}
6.前端接收返回参数 并渲染页面 前端收到
{
openid: "oLGmwxImbJsLroSufafI_8m0zr4k"
nickname: "阿鹏"
sex: 1
language: "zh_CN"
city: "杭州"
province: "浙江"
country: "中国"
headimgurl: "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eqWLdsBA4vNNcVg5ELMaDz5h0JmSI8q3PWFWDR5icV3fW6330Ff1WoibpheqZxP7b1z8Ehk6RKvmjxw/132"
privilege: []
}