近期把自用的微信公众号微信分享模块从 php 修改成 nodejs 的版本,虽然这是一个很小的功能,但仍然选择了 egg 框架,也算是为将来继续开发公众号,作点扩展的准备。javascript
本文章仅为项目介绍,不涉及 egg 的原理,请不要问我为啥不直接用koa。php
koa框架:基于 Node.js 平台的新的 web 框架,由 Express 幕后的原班人马打造,它与Express使用同一套http基础库。最新的koa2,是基于ES7开发的,完美支持了promise及async。html
egg框架:egg2.x 以Koa2.x 做为其基础框架,兼容Koa 2.x 的中间件,最低支持 Node.js 8java
经过一张图来描述 egg2.x :node
更多的这里就不说了,有兴趣的童鞋请看 eggjs.org/zh-cn/intro…jquery
npm i egg --save
npm i egg-bin --save-dev
复制代码
{
"name": "egg-example",
"scripts": {
"dev": "egg-bin dev"
}
}
复制代码
建立本地目录以下:nginx
node
├── package.json
├── app
│ ├── extend // 扩展
│ | ├── helper.js
│ ├── service // 服务
| ├── controller // 控制器
| ├── public // 静态资源路径
│ ├── middleware // 中间件
│ └── router.js // 路由
└── config
├── config.default.js // 配置
└── plugin.js // 插件
复制代码
以上仅是本案例中的结构,完整结构,参考官方:eggjs.org/zh-cn/basic…git
egg 服务端默认采用的是 7001 端口,由于咱们将二级域名: share.xxx.com 解析到 7001 上,实现域名直接访问 egg 接口。github
解析二级域名web
server {
listen 80;
server_name share.xxx.com;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:7001;
}
}
复制代码
nginx 反向代理
服务器从新启动 nginx 后,便可经过 share.xxx.com 访问到咱们的 7001 端口了!
微信分享实际上是一个比较简单的功能,难点也就是了解微信公众号的token如何转换成签名,这里简单画了一个图:
权限,指的是安全域名,须要到公众号后台进行设置(以下图),并填写你的分享连接域名。
填写域名时,须要将公众号给的验证文件放到根目录,在你点击保存时,公众号服务器将会去请求本文件,验证该域名是否有效。
具体的获取 token 及 ticket 接口及代码,会在下面详细讲解。
安全域名,须要在根目录放一个文本文件,公众号会尝试打开该文件,并验证其中的key。因为 egg 没法直接访问根目录文件,所以使用路由来实现验证接口
// 路由
router.get('/MP_verify_ysZJMVdQxMoU8v35.txt', controller.check.index);
// 验证
class CheckController extends Controller {
async index() {
let cache = await this.ctx.helper.readFile(path.join(this.config.baseDir, 'app/MP_verify_ysZJMVdQxMoU8v35.txt'));
this.ctx.body = cache;
}
}
复制代码
egg 针对 csrf 安全作了如下几种处理:
在 CSRF 默认配置下,token 会被设置在 Cookie 中,在 AJAX 请求的时候,能够从 Cookie 中取到 token,放置到 query、body 或者 header 中发送给服务端。
以 jquery 为例, 在 beforeSend 中,增长 header 项 x-csrf-token:
// 请求签名
var token = getCookie('csrfToken');
if(token){
var url = location.href.split('#')[0];
var host = location.origin;
$.ajax({
url: host + "/getTicket",
type: 'post',
data: {
url: encodeURIComponent(url)
},
beforeSend: function (request) {
request.setRequestHeader("x-csrf-token", token);
},
success: function (res) {
if(res.code === 0){
wx.config({
debug: true,
appId: res.data.appId,
timestamp: res.data.timestamp,
nonceStr: res.data.nonceStr,
signature: res.data.signature,
jsApiList: [
'updateTimelineShareData',
'updateAppMessageShareData'
]
});
wx.ready(function () {
var shareData = {
title: '个人分享',
desc: '个人文字介绍,详细的',
link: host,
imgUrl: host + "/public/images/icon.jpg"
};
wx.updateTimelineShareData(shareData);
wx.updateAppMessageShareData(shareData);
});
wx.error(function (res) {
console.log(res.errMsg);
});
}else{
console.log(res);
}
}
});
}else{
alert('invalid csrf token');
}
复制代码
async getToken(ctx, config){
let timestamp = new Date().valueOf();
let cache = await this.ctx.service.fileService.read('token');
let result = cache;
// 缓存失效
if (!cache || cache.expires_in < timestamp || cache.app_id !== config.wx.appId) {
result = await ctx.curl(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.wx.appId}&secret=${config.wx.secret}`, {
dataType: 'json'
});
if (this.ctx.helper.checkResponse(result)) {
result = {
access_token: result.data.access_token,
expires_in: timestamp + result.data.expires_in * 1000,
app_id: config.wx.appId
};
this.ctx.service.fileService.write('token', result);
} else {
this.ctx.service.fileService.write('token', '');
this.ctx.logger.error(new Error(`${timestamp}--wxconfig: ${JSON.stringify(config.wx)}--tokenResult: ${JSON.stringify(result)}`));
result = null;
}
}
return result;
}
复制代码
async getTicket(ctx, config, res){
let timestamp = new Date().valueOf();
let cache = await this.ctx.service.fileService.read('ticket');
let result = cache;
// 缓存失效
if (!cache || cache.expires_in < timestamp || cache.app_id !== config.wx.appId) {
result = await ctx.curl(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${res.access_token}&type=jsapi`, {
dataType: 'json'
});
if (this.ctx.helper.checkResponse(result)) {
result = {
ticket: result.data.ticket,
expires_in: timestamp + result.data.expires_in * 1000,
app_id: config.wx.appId
};
this.ctx.service.fileService.write('ticket', result);
} else {
this.ctx.service.fileService.write('ticket', '');
this.ctx.logger.error(new Error(`${timestamp}--wxconfig: ${JSON.stringify(config.wx)}--jsapiResult: ${JSON.stringify(jsapiResult)}`));
result = null;
}
}
return result;
}
复制代码
oken 及 ticket 的有效期均为 7200 秒,且有必定的请求频率限制,所以推荐在服务器本地缓存这两个串,开发者能够自行选择存储在本地、数据库、全局中。
以本项目为例,这里偷懒了下,直接存在本地txt中,别问我为啥用文件存储 ^_^,我不会告诉你是懒的装数据库。
async read(type) {
let src = type === 'token' ? this.tokenFile : this.ticketFile;
let data = await this.ctx.helper.readFile(src);
data = JSON.parse(data);
return data;
}
async write(type, data) {
let src = type === 'token' ? this.tokenFile : this.ticketFile;
await this.ctx.helper.writeFile(src, JSON.stringify(data));
}
复制代码
签名生成规则以下:
参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket,timestamp(时间戳),url(当前网页的URL,不包含#及其后面部分) 。对全部待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL拼接成字符串string1,这里须要注意的是全部参数名均为小写字符。对string1做sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
const uuidv1 = require('uuid/v1');
const noncestr = uuidv1();
const timestamp = Math.round(new Date().valueOf() / 1000);
const string1 = `jsapi_ticket=${jsapi_ticket}&noncestr=${noncestr}×tamp=${timestamp}&url=${url}`;
const crypto = require('crypto');
const hash = crypto.createHash('sha1');
hash.update(string1);
const signature = hash.digest('hex');
return {
nonceStr: noncestr,
timestamp,
signature,
appId: appId,
jsapi_ticket,
url,
string1
};
复制代码
这里推荐使用 vs code 来调试服务端代码,须要简单进行配置,参考 egg 官方文档: eggjs.org/zh-cn/core/…
在正式部署前,开发者根据须要自行配置中间件、模板、插件、启动配置项等。
egg 提供了 egg-scripts 来支持线上环境的启停。
npm i egg-scripts --save
复制代码
添加 npm scripts
{
"scripts": {
"start": "egg-scripts start --daemon --title=egg-server-showcase",
"stop": "egg-scripts stop"
}
}
复制代码
egg 默认会开启 cpu 数量的进程,性能方面仍是不错的。
egg-scripts stop [--title=egg-server]
复制代码
框架内置了 egg-cluster 来启动 Master 进程,所以不须要作额外配置便可。若有特殊需求,框架也支持使用 pm2 来作管理:
咱们使用 egg 官方推荐的 Node.js 性能平台(alinode)
AliNode Runtime 能够直接替换掉 Node.js Runtime
// 安装版本管理工具 tnvm,安装过程出错参考:https://github.com/aliyun-node/tnvm
wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash
source ~/.bashrc
// https://help.aliyun.com/knowledge_detail/60811.html,这里有node版本对应的alinode版本
tnvm install alinode-v4.2.2 # 安装须要的版本
tnvm use alinode-v4.2.2 # 使用须要的版本
// 因为egg官方封装了egg-alinode 来快速接入,无需安装 agenthub
复制代码
npm i egg-alinode --save
复制代码
开启插件:
// config/plugin.js
exports.alinode = {
enable: true,
package: 'egg-alinode',
};
复制代码
配置:
// config/config.default.js
exports.alinode = {
// 从 `Node.js 性能平台` 获取对应的接入参数
appid: '<YOUR_APPID>',
secret: '<YOUR_SECRET>',
};
复制代码
阿里官方使用的开启方式,是在命令行加入如下代码:
ENABLE_NODE_LOG=YES node demo.js
复制代码
但在egg中,有本身的启动方式,仍然是:
npm start
复制代码
官方解释是:
成功启动后,访问几回你的接口,稍等一会,便可在控制台看到数据了。
控制台地址: node.console.aliyun.com
怀疑是 1.7.1 git 版本太老形成,升级 git...
// 下载 git 2.21.1 版本
wget https://github.com/git/git/archive/v2.21.1.tar.gz
tar -zxvf v2.21.1.tar.gz
// 安装依赖
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker
// 删除老版本git
yum remove git
// 进入解压后的文件夹
cd git-2.21.1
// 编译
make prefix=/usr/local/git all
make prefix=/usr/local/git install
// 环境变量
echo "export PATH=$PATH:/usr/local/git/bin" >> /etc/profile
// 让环境变量生效
source /etc/profile
复制代码
cd /usr/local/src
// 下载新版的libiconv
wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.15.tar.gz
tar -zxvf libiconv-1.15.tar.gz
// 安装
cd libiconv-1.15
./configure --prefix=/usr/local/libiconv && make && make install
// 建立连接
ln -s /usr/local/lib/libiconv.so /usr/lib
ln -s /usr/local/lib/libiconv.so.2 /usr/lib
复制代码
而后重复上面安装git过程的12行以后便可
GitHub 前几天开始不支持老的加密方式,升级到 CentOS 6.8 或者单独升级SSH,如下命令2选1
yum update -y
yum update openssh
复制代码
至此,整篇文章所有结束,这里的代码部分也有参照一些别的教程,因此整体来讲,并非难度多大的东西,你们互相学习互相进步吧。