lua resty wechat
1.0.0
使用Lua編寫的nginx伺服器微信公眾平台代理.
目標:
在前置的nginx內做微信代理, 降低內部應用層和微信服務的耦合.
設定微信公眾號的自動回覆, 在nginx內處理部分用戶訊息, 減少應用層壓力.
統一管理微信公眾號API中使用的access_token
, 作為中控伺服器隔離業務層和API實現, 降低access_token
衝突率, 增加服務穩定性.
部署微信JS-SDK授權回呼頁, 減小應用層壓力.
config
公有號全域設定資料, 包含介面Token, 自動回覆設定.
server
接收微信發出的普通訊息和事件推送等請求, 並按配置做出回應, 未做對應配置則按微信要求返回success
.
此部分核心程式碼由aCayF/lua-resty-wechat做重構修改而來.
使用config.autoreplyurl
, 設定後台處理服務位址, 轉送處理並回應複雜的微信訊息. (依賴pintsized/lua-resty-http)
proxy_access_token
使用Redis快取access_token
和jsapi_ticket
, 定時自動呼叫微信服務更新, 支援分散式更新.
proxy
代理呼叫微信公眾平台API介面, 自動加入access_token
參數.
proxy_access_filter
過濾客戶端IP, 限制請求來源.
oauth
jssdk_config
nginx設定:
http { lua_package_path 'path to lua files'; resolver 114.114.114.114; lua_shared_dict wechat 1M; # 利用共享記憶體保持單例定時器 init_by_lua ' ngx.shared.wechat:deleteete(" require("resty.wechat.config") '; init_worker_by_lua ' local ok, err = ngx.shared.wechat:add("updater", "1") -- 單一進程啟動定時器if not okor err then return end require("resty.wechat.proxy_access_token")() '; server {location /wechat-server { content_by_lua ' require("resty.wechat.server")() '; }location /wechat-proxy/ { rewrite_by_lua ' require("resty.wechat.proxy")("wechat-proxy") -- 參數為location路徑'; access_by_lua ' require("resty.wechat.proxy_access_filter")() ' ; proxy_pass https://api.weixin.qq.com/; }location /wechat-baseoauth { # param: goto rewrite_by_lua ' require("resty.wechat.oauth").base_oauth("path to /wechat-redirect") '; }location /wechat-useroauth { # param: goto rewrite_by_lua ' require("resty.wechat.oauth").userinfo_oauth("path to /wechat-redirect") '; }location /wechat-redirect { rewrite_by_lua ' require("resty.wechat.oauth").redirect() '; }location /wechat-jssdk-config { # GET/POST, param: url, [api] add_header Access-Control-Allow-Origin "if need cross-domain call"; content_by_lua ' require("resty.wechat.jssdk_config") () '; } } }
網頁注入JS-SDK權限:
$.ajax({ url: "url path to /wechat-jssdk-config", data: {url: window.location.href,api: "onMenuShareTimeline|onMenuShareAppMessage|onMenuShareQQ|onMenuShareWeibo|onMenuShareQZone" }, success: function(response) {wx.config(response); }});$.ajax({ url: "url path to /wechat-jssdk-config", data: {url: window.location.href }, success: function(response) {wx.config({ appId: response.appId, timestamp: response.timestamp, nonceStr: response.nonceStr, signature: response.signature, jsApiList: [ 'onMeTimeShareline', onShareShareline', 'MeponseShareAppline'Qnuon’ , 'onMenuShareWeibo', 'onMenuShareQZone' ]}); }});
使用Java解析代理網頁授權所取得的cookie
Map authInfo = JSON.parseObject(decryptAES(unBase64("cookie value"), getKey("AES key")));// 預設AES key: "vFrItmxI9ct8JbAg"// 配置於config.lua -> cookie_aes_key/ 9ct8JbAg"// 配置在config.lua -> cookie_aes_key/ 方法依賴方法import com.alibaba.fastjson.JSON;import com.google.common.base.Charsets;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import java.security.Key;public StringBuilder padding(String s, char l, int repeatsetter) {StringBuilder sb = new StringBuilder(s);while (repeats-- > 0) {sb.append(letter); }return sb; }public String padding(String s) {return padding(s, '=', s.length() % 4).toString(); }public byte[] unBase64(String value) {return org.apache.commons.codec.binary.Base64.decodeBase64(padding(value)); }public String string(byte[] bytes) {return new String(bytes, Charsets.UTF_8); }public String decryptAES(byte[] value, Key key) {try {Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");cipher.init(Cipher.DECRYPT_MODE, key);byte[] decrypted = cipher. doFinal(value);return string(decrypted); } catch (Exception e) {throw new RuntimeException(e); } }public byte[] bytes(String str) {return str == null ? null : str.getBytes(Charsets.UTF_8); }public Key keyFromString(String keyString) {return new SecretKeySpec(bytes(keyString), "AES"); }public Key getKey(String key) {if (key.length() >= 16) {return keyFromString(key.substring(0, 16)); }StringBuilder sb = new StringBuilder(key);while (sb.length() < 16) {sb.append(key); }return keyFromString(sb.toString().substring(0, 16)); }