近期把自用的微信公衆號微信分享模塊從 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
複製代碼
至此,整篇文章所有結束,這裏的代碼部分也有參照一些別的教程,因此整體來講,並非難度多大的東西,你們互相學習互相進步吧。