{"id":16347041,"url":"https://github.com/ospoon/wechat4node","last_synced_at":"2025-07-21T07:32:07.559Z","repository":{"id":105557582,"uuid":"517743750","full_name":"OSpoon/wechat4node","owner":"OSpoon","description":null,"archived":false,"fork":false,"pushed_at":"2022-09-01T06:41:35.000Z","size":3583,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-19T05:05:45.016Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OSpoon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-25T16:38:37.000Z","updated_at":"2022-07-25T16:39:39.000Z","dependencies_parsed_at":null,"dependency_job_id":"02458853-5984-4746-abfa-a216182a00cc","html_url":"https://github.com/OSpoon/wechat4node","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/OSpoon/wechat4node","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fwechat4node","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fwechat4node/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fwechat4node/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fwechat4node/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OSpoon","download_url":"https://codeload.github.com/OSpoon/wechat4node/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fwechat4node/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266261123,"owners_count":23901284,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T00:39:19.313Z","updated_at":"2025-07-21T07:32:07.529Z","avatar_url":"https://github.com/OSpoon.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# wechat4node\n\n## 写作背景：\n**💢第一坑来自ali云，ali云的函数计算FC产品在使用模板创建项目的过程中，如下图中的服务名和函数名每次创建都是一样的，我们很容易就按默认的创建了，但是多次创建会覆盖前一次的内容，我写好的授权接口就被覆盖了~**\n![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1658762396888-189f0787-021f-44e5-aea0-d5d688e96829.png#clientId=ub46562fc-96b2-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026height=448\u0026id=uaf18ad52\u0026margin=%5Bobject%20Object%5D\u0026name=image.png\u0026originHeight=448\u0026originWidth=1983\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026size=30288\u0026status=done\u0026style=shadow\u0026taskId=udf804778-9607-4747-9b97-247362e2c31\u0026title=\u0026width=1983)\n![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1658762622452-539ab3a3-8563-4307-b79c-261d0250ef03.png#clientId=ub46562fc-96b2-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026id=u005c734e\u0026margin=%5Bobject%20Object%5D\u0026name=image.png\u0026originHeight=218\u0026originWidth=650\u0026originalType=url\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026size=34863\u0026status=done\u0026style=shadow\u0026taskId=u01b632b4-61c6-4acb-a190-b1c76a10461\u0026title=)\n**💘ali的产品原来也有这种低级的错误，不管你是产品设计的缺陷还是系统开发的缺陷，这都是致命的，因为正常运行的服务丢了~**\n![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1658762603386-3c18ec45-0bf3-46f4-9757-3442bf8d3c10.png#clientId=ub46562fc-96b2-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026height=836\u0026id=u1deadc8a\u0026margin=%5Bobject%20Object%5D\u0026name=image.png\u0026originHeight=836\u0026originWidth=2293\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026size=85055\u0026status=done\u0026style=shadow\u0026taskId=u30c2f83b-be21-43ea-b289-1ccf01363da\u0026title=\u0026width=2293)**💥第二坑来自WeChat，在ali云调试好的代码丢就丢了吧，我就打算还在ali云上开始重新来一遍，靠着搜索到的网站还没关掉，就写完了第二遍云上的代码，接口测试可以返回信息，但是【折】在了前端wx.config函数的校验问题上，写过的都知道文档提供了校验失败的排查过程，你要是能按这个解决了，微信开发社区就不会提5k+近似重复的问题而没有结果了~**\n![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1658762850917-5b843dc3-cd9e-4f44-9d43-9d8a8f3742b1.png#clientId=ub46562fc-96b2-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026height=292\u0026id=udeafccb7\u0026margin=%5Bobject%20Object%5D\u0026name=image.png\u0026originHeight=292\u0026originWidth=1059\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026size=55472\u0026status=done\u0026style=shadow\u0026taskId=ua44a8fb1-b0d3-43a3-988b-e42a428f7e2\u0026title=\u0026width=1059)![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1658762983648-25ba01f0-db2b-4ea9-bab5-b3fc337dfb9c.png#clientId=ub46562fc-96b2-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026height=415\u0026id=uda6aed7c\u0026margin=%5Bobject%20Object%5D\u0026name=image.png\u0026originHeight=415\u0026originWidth=1239\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026size=41868\u0026status=done\u0026style=shadow\u0026taskId=u0a8da0d7-208f-497a-a960-07eb859f10b\u0026title=\u0026width=1239)\n💤**写代码就应该【有始有终】我还是在本地搭建全套环境来再搞一遍试试看，请熟悉下面各个地址的快速上手文档来搭建环境~**\n\n1. **服务端：**[**Egg.js**](https://www.eggjs.org/zh-CN/intro/quickstart)\n1. **前端：**[**Vue3**](https://staging-cn.vuejs.org/)\n1. **微信：**[**公众号测试平台**](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo\u0026t=sandbox/index)\n1. **内网穿透：**[Natapp](https://natapp.cn/)\n## 代码实操：\n### 第一步，启动Natapp：\n\n1. 在natapp平台找到免费提供的隧道进行配置更改：将`7001`端口进行映射，`7001`是eggjs项目默认端口；\n1. 在下载好natapp客户端（window）后在客户端同级目录编写`config.ini`配置文件并将平台提供的免费隧道键入到配置文件中，格式同：`authtoken=\u003cyour authtoken\u003e`；\n1. 配置后双击客户端如下图就启动正常了，免费隧道的IP随时可能会变，开发中需要注意~![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1658763589867-b9f2cb73-da38-414d-b758-ffe6d7e1223d.png#clientId=ub46562fc-96b2-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026height=251\u0026id=u921d5805\u0026margin=%5Bobject%20Object%5D\u0026name=image.png\u0026originHeight=251\u0026originWidth=828\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026size=14420\u0026status=done\u0026style=shadow\u0026taskId=u93c3d1ba-62db-4a65-96ec-12c6bfd27cd\u0026title=\u0026width=828)\n### 第二步，先搞前端部分：\n\n1. 使用`yarn create vite`创建一个前端项目，用着顺手就行，为的就是**快~**\n1. 安装`axios`来发送授权信息获取请求；\n1. 微信的JS-SDK既然没看到官方提供依赖包那就用配置到index.html，`\u003cscript src=\"[http://res2.wx.qq.com/open/js/jweixin-1.6.0.js\"\u003e\u003c/script\u003e](http://res2.wx.qq.com/open/js/jweixin-1.6.0.js\"\u003e\u003c/script\u003e)`\n1. 在模板中我们编写少量的代码来显示授权的状态：\n```html\n\u003ctemplate\u003e\n  \u003ch3 v-if=\"message\"\u003e{{ message }}\u003c/h3\u003e\n  \u003ch3 v-else\u003e\n    点击右上角=\u003e分享给朋友\n    \u003ch5\u003e{{ status }}\u003c/h5\u003e\n  \u003c/h3\u003e\n\u003c/template\u003e\n```\n\n5. 我们通过`axios`获取`/signature`接口的数据，为了避免**url**参数的传递可能存在编码的问题，那我们稍后在设计`/signature`接口的时候**url**参数通过**body**传递来避免，授权数据拿到后就是`wx.config`执行验证了~\n```typescript\n\u003cscript setup\u003e\n  import { ref } from \"vue\";\nimport axios from \"axios\";\nconst message = ref(\"\");\nconst status = ref(\"\");\naxios\n  .post(`http://t2fzbb.natappfree.cc/signature`, {\n  url: location.href.split(\"#\")[0],\n})\n  .then((res) =\u003e {\n  const { appId, nonceStr, signature, timestamp } = res.data;\n  console.log(\"[ res ] \u003e\", appId, nonceStr, signature, timestamp);\n  wx.config({\n    debug: true, // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来，若要查看传入的参数，可以在 pc 端打开，参数信息会通过 log 打出，仅在 pc 端时才会打印。\n    appId, // 必填，公众号的唯一标识\n    timestamp, // 必填，生成签名的时间戳\n    nonceStr, // 必填，生成签名的随机串\n    signature, // 必填，签名\n    jsApiList: [\"updateAppMessageShareData\"], // 必填，需要使用的 JS 接口列表\n  });\n  wx.error((res) =\u003e {\n    message.value = res.errMsg;\n  });\n  wx.ready(() =\u003e {\n    wx.checkJsApi({\n      jsApiList: [\"updateAppMessageShareData\"], // 需要检测的 JS 接口列表，所有 JS 接口列表见附录2,\n      success: (res) =\u003e {\n        wx.updateAppMessageShareData({\n          title: \"我的掘金\", // 分享标题\n          desc: \"我在掘金输出前端知识~\", // 分享描述\n          link: \"https://juejin.cn/user/3966693685871694\", // 分享链接，该链接域名或路径必须与当前页面对应的公众号 JS 安全域名一致\n          imgUrl:\n          \"https://p9-passport.byteacctimg.com/img/user-avatar/71ca4682501063257d8413ff726105a0~300x300.images\", // 分享图标\n          success: function () {\n            status.value = \"设置成功\";\n          },\n        });\n      },\n    });\n  });\n});\n\u003c/script\u003e\n```\n最终我们要在微信开发者工具的公众台看到一堆**ok**就说明可以了~\n![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1658764406742-3e698acc-a4c0-45c5-a82f-d66ceeaf39db.png#clientId=ub46562fc-96b2-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026height=968\u0026id=u3f6ed55e\u0026margin=%5Bobject%20Object%5D\u0026name=image.png\u0026originHeight=968\u0026originWidth=1240\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026size=142095\u0026status=done\u0026style=shadow\u0026taskId=u94209905-7996-4dd9-a735-b69a420aec8\u0026title=\u0026width=1240)\n### 第三步，编写服务端接口：\n🤣在开始前有一个问题，微信的各种**票据**都需要我们自己做缓存处理，仅在超时后再刷新，要不会遇到调用次数超限制的麻烦，服务端缓存**票据**我最先想到的是**Redis**，但是多一份配置还是挺麻烦的，后来想到的是**读写文件**，我在【ali云】的两次就是通过读写文件来实现的，毕竟是Demo，效率靠后呗~\n😋最后我想到的是服务运行后我可以使用全局对象来缓存数据呀，并不需要借助额外的渠道来实现，我们以前的**单利模式**就是在需要的时候再实例化对象的呀💗\n#### 编写个接口的Controller层：\n\n1. checkOrigin：用来在公众号测试平台发起验证服务的请求来验证数据来源；\n1. token：用来获取`access_token`，我会在server层做`access_token`的**对象缓存**；\n1. ticket：用来获取`ticket`，这个也会在server层做**对象缓存**；\n1. signature：用来生成验签，并返回**wx.config**需要配置数据，期间需要一次获取`access_token`和`ticket`，可以看到`url`我们是通过`body`获取的。\n```typescript\nimport { Controller } from 'egg';\n\nexport default class HomeController extends Controller {\n  \n  public async checkOrigin() {\n    const { ctx } = this;\n    const { signature, timestamp, nonce, echostr } = ctx.query;\n    const result = await ctx.service.weChat.checkSignature(\n      signature,\n      timestamp,\n      nonce,\n    );\n    ctx.body = result ? echostr : '';\n  }\n\n  public async token() {\n    const { ctx } = this;\n    ctx.body = await ctx.service.weChat.getToken();\n  }\n\n  public async ticket() {\n    const { ctx } = this;\n    ctx.body = await ctx.service.weChat.getTicket();\n  }\n\n  public async signature() {\n    const { ctx } = this;\n    const { url } = ctx.request.body;\n    console.log('[ url ] \u003e', url);\n    ctx.body = await ctx.service.weChat.genSignature(url);\n  }\n}\n\n```\n#### 编写Service的第一个函数checkSignature：\n💕需要接口微信平台发送来的**signature**，**timestamp**，**nonce**，在测试平台我们配置了个自定义的TOKEN，我们需要将【TOKEN, timestamp, nonce】进行**sort**排序后拼接成一个字符串，并使用**SHA1**加密算法（这里使用的crypto-js依赖）来生成我们自己的**验签**，将我们的生成的**验签**和平台发送过来的**signature**对比并将结果返回到Controller，并响应微信测试平台~\n```typescript\n  public async checkSignature(\n    signature: string,\n    timestamp: string,\n    nonce: string,\n  ) {\n    const own = SHA1([ TOKEN, timestamp, nonce ].sort().join('')).toString();\n    return own === signature;\n  }\n```\n#### 编写Service的第二个函数getToken：\n\n1. 我在服务的最开始定义了一个**cache**对象来缓存**token**；\n1. 当**token**中的`access_token`字段不存在时就需要使用上下文对象中的`curl`来发起请求获取一份新的`access_token`数据；\n1. 在拿到新数据后通过`Date.now() + expires_in * 1000`得到一个失效的时间并赋值给`last_time`；\n1. 当前的`Date.now()`大于存储的`last_time`时需要再次刷新`access_token`。\n```typescript\nconst cache: {\n  token: {\n    access_token: string | undefined;\n    expires_in: number;\n    last_time: number;\n  };\n} = {\n  token: {\n    access_token: undefined,\n    expires_in: 0,\n    last_time: 0,\n  }\n};\n\npublic async getToken() {\n  const { ctx } = this;\n  const request = async () =\u003e {\n    const { data } = await ctx.curl(URL_TOKEN, { dataType: 'json' });\n    const { access_token, expires_in } = data;\n    console.log('[ token data ] \u003e', data);\n    cache.token = {\n      access_token,\n      expires_in,\n      last_time: Date.now() + expires_in * 1000,\n    };\n  };\n  if (!cache.token.access_token) {\n    console.log('[ token 还没有~ ] ');\n    await request();\n  } else {\n    if (Date.now() \u003e cache.token.last_time) {\n      console.log('[ token 超时了 ] ');\n      await request();\n    }\n  }\n  return cache.token;\n}\n```\n#### 编写Service的第三个函数getTicket：\n**🤖getTicket**和**getToken**的缓存逻辑一致，区别在于getTicket发起请求时需要携带`access_token`，其它直接看代码~\n```typescript\nconst cache: {\n  token: {\n    access_token: string | undefined;\n    expires_in: number;\n    last_time: number;\n  };\n  ticket: {\n    ticket: string | undefined;\n    expires_in: number;\n    last_time: number;\n  };\n} = {\n  token: {\n    access_token: undefined,\n    expires_in: 0,\n    last_time: 0,\n  },\n  ticket: {\n    ticket: undefined,\n    expires_in: 0,\n    last_time: 0,\n  },\n};\n\npublic async getTicket() {\n  const { ctx } = this;\n  const request = async () =\u003e {\n    const { access_token } = await this.getToken();\n    const { data } = await ctx.curl(URL_TICKET, {\n      dataType: 'json',\n      method: 'GET',\n      data: {\n        access_token,\n      },\n    });\n    const { ticket, expires_in } = data;\n    console.log('[ ticket data ] \u003e', data);\n    cache.ticket = {\n      ticket,\n      expires_in,\n      last_time: Date.now() + expires_in * 1000,\n    };\n  };\n  if (!cache.ticket.ticket) {\n    console.log('[ ticket 还没有~ ] ');\n    await request();\n  } else {\n    if (Date.now() \u003e cache.ticket.last_time) {\n      console.log('[ ticket 超时了 ] ');\n      await request();\n    }\n  }\n  return cache.ticket;\n}\n```\n#### 编写Service的最后一个函数genSignature：\n\n1. 调用上一次函数得到`jsapi_ticket`；\n1. 生成随机字符串noncestr：`Math.random().toString(36).substr(2, 15)`；\n1. 生成时间戳timestamp：`Date.now()`；\n1. ~~验签生成在官网描述中需要：~~\n   1. ~~对所有待签名参数按照字段名的ASCII 码从小到大排序（字典序）；~~\n   1. ~~使用 URL 键值对的格式（即key1=value1\u0026key2=value2…）拼接成字符串string1；~~\n   1. ~~需要注意的是所有参数名均为小写字符；~~\n   1. ~~对string1作sha1加密，字段名和字段值都采用原始值，不进行URL 转义；~~\n5. 官网的描述太麻烦了，我们直接按ASCII 码从小到大凭借模板字符串，排序、键值对格式、大小写这些注意事项就都省了，我们只需要按第一个函数中做一次`SHA1`加密并`toString()`得到验签字符串就可以le~\n5. 组装前端`wx.config`中需要的四个参数并返回~\n```typescript\npublic async genSignature(url: string) {\n  const { ticket: jsapi_ticket } = await this.getTicket();\n  const noncestr = Math.random().toString(36).substr(2, 15);\n  const timestamp = Date.now();\n  const signature = SHA1(\n    `jsapi_ticket=${jsapi_ticket}\u0026noncestr=${noncestr}\u0026timestamp=${timestamp}\u0026url=${url}`,\n  ).toString();\n  return {\n    appId: APPID,\n    nonceStr: noncestr,\n    timestamp,\n    signature,\n  };\n}\n```\n## 微信测试号平台：\n![uTools_1658766397916.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1658766495601-d6ae9d80-fe20-49fd-bf07-70aabc26ec9a.png#clientId=u5b222d9c-5eae-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026height=695\u0026id=dab96\u0026margin=%5Bobject%20Object%5D\u0026name=uTools_1658766397916.png\u0026originHeight=695\u0026originWidth=1193\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026size=45177\u0026status=done\u0026style=shadow\u0026taskId=ud843cc43-3f68-471a-8873-31e9aced1ad\u0026title=\u0026width=1193)\n### 接口配置信息：\n💥需要调用我们的`/checkOrigin`，Token字段为我们自定义的内容，和服务中验证签名时使用的一致就可以了，配置提交后会向我们的服务发起**GET**请求，并通过query携带参数给我们，当我们验证通过后需要将平台传递过来的随机字符返回回去代表我们验证通过~\n### JS接口安全域名：\n这个因为我们在微信开发者工具中调试前端页面用的`localhost`，所以需要将`localhost`配置在这个地方~\n## 总结：\n💯代码的编写还是很容易的，就是和这种大平台沟通成本太高，每次编写都有新坑要踩，最后我将代码的配置信息剔除后上传到[GitHub](https://github.com/OSpoon/)，需要的jym可以下载跑跑看~\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fospoon%2Fwechat4node","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fospoon%2Fwechat4node","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fospoon%2Fwechat4node/lists"}