├── LICENSE ├── server └── wechat.js └── public └── wechat.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 zincing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/wechat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 微信 3 | * 服务端授权接口 4 | */ 5 | var Promise = require('promise'); 6 | var https = require('https'); 7 | var querystring = require('querystring'); 8 | var crypto = require('crypto'); 9 | // 在单台服务器的情况下,在内存中缓存 access_token 和 jsapi_ticket 10 | // 如果是多台服务器,建议使用 redis 缓存 11 | var cache_access_token = ''; 12 | var cache_jsapi_ticket = ''; 13 | // 微信公众平台测试账号 14 | // 申请地址 http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login 15 | var appid = ''; // 微信平台 app id 16 | var secret = ''; // 微信平台 app 加密串 17 | // 生成随机字符串 18 | var nonstr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 19 | 20 | /*@Controller*/ 21 | module.exports = { 22 | 23 | /*@Qmonitor("MP_verify_1Dzk5461NaRaimIi")*/ 24 | /*@RequestMapping({url: "/activity/MP_verify_ofjOR1yIIOXOXoTp.txt"})*/ 25 | serverConfigFile: function(req, res) { 26 | res.setHeader('Content-Type', 'text/plain'); 27 | res.end('ofjOR1yIIOXOXoTp'); 28 | }, 29 | 30 | /*@RequestMapping({url: "/activity/accesstoken"})*/ 31 | getWechatjsConfig: function(req, res) { 32 | var allUrl = req.protocol + '://' + req.get('host') + req.originalUrl; 33 | var url = allUrl.split('#')[0]; 34 | getAccessToken() 35 | .then(function(token) { 36 | // 使用 access_token 去获取 jsapi_ticket 37 | return getJsapiTicket(token); 38 | }) 39 | .then(function(jsapiTicket) { 40 | // 生成随机串 41 | var noncestr = genNoncestr(); 42 | // 生成时间戳 43 | var timestamp = new Date().getTime(); 44 | // 计算签名 45 | var signature = compSignature(jsapiTicket, noncestr, timestamp, url); 46 | // 返回结果 47 | res.setHeader('Content-Type', 'application/json;charset=utf-8'); 48 | res.end(JSON.stringify({ 49 | appId: appid, 50 | timestamp: timestamp, 51 | nonceStr: noncestr, 52 | signature: signature 53 | })); 54 | }) 55 | .catch(function(e) { 56 | res.setHeader('Content-Type', 'application/json;charset=utf-8'); 57 | res.end(JSON.stringify({ 58 | status: 1, 59 | message: e 60 | })); 61 | }); 62 | } 63 | 64 | } 65 | 66 | /** 67 | * 缓存,这里模拟 redis 68 | * @param {[type]} key [description] 69 | * @return {[type]} [description] 70 | */ 71 | function getAccessToken () { 72 | if (cache_access_token) { 73 | return Promise.resolve(cache_access_token); 74 | } 75 | return new Promise(function(resolve, reject) { 76 | var params = querystring.stringify({ 77 | grant_type: 'client_credential', 78 | appid: appid, 79 | secret: secret 80 | }); 81 | sendHttpsRequest({ 82 | host: 'api.weixin.qq.com', 83 | port: 443, 84 | method: 'GET', 85 | path: '/cgi-bin/token?' + params 86 | }) 87 | .then(function(resp) { 88 | // 设置缓存在指定时间后过期 89 | // ****** 如果是多台服务器,这里应该替换为 redis ****** 90 | cache_access_token = resp.access_token; 91 | var expires = isNaN(resp.expires_in) ? 0 : resp.expires_in * 1000; 92 | if (expires === 0) { 93 | var errStr = '访问微信接口错误,get jsapi tikect error, expires is not a number!'; 94 | console.error(errStr); 95 | reject(errStr); 96 | } 97 | setTimeout(function() { 98 | cache_access_token = ''; 99 | }, expires); 100 | // 返回结果 101 | resolve(cache_access_token); 102 | }) 103 | .catch(function(e) { 104 | reject(e); 105 | }); 106 | }); 107 | } 108 | 109 | /** 110 | * 获取 jsapi ticket 111 | * @return {[type]} [description] 112 | */ 113 | function getJsapiTicket (accessToken) { 114 | if (cache_jsapi_ticket) { 115 | return Promise.resolve(cache_jsapi_ticket); 116 | } 117 | return new Promise(function(resolve, reject) { 118 | var params = 'appid=' + appid + '&secret=' + secret; 119 | var params = querystring.stringify({ 120 | type: 'jsapi', 121 | access_token: accessToken 122 | }); 123 | sendHttpsRequest({ 124 | host: 'api.weixin.qq.com', 125 | port: 443, 126 | method: 'GET', 127 | path: '/cgi-bin/ticket/getticket?' + params 128 | }) 129 | .then(function(resp) { 130 | // 设置缓存在指定时间后过期 131 | // ****** 如果是多台服务器,这里应该替换为 redis ****** 132 | cache_jsapi_ticket = resp.ticket; 133 | var expires = isNaN(resp.expires_in) ? 0 : resp.expires_in * 1000; 134 | if (expires === 0) { 135 | var errStr = '访问微信接口错误,get jsapi tikect error, expires is not a number!'; 136 | console.error(errStr); 137 | reject(errStr); 138 | } 139 | setTimeout(function() { 140 | cache_jsapi_ticket = ''; 141 | }, expires); 142 | // 返回结果 143 | resolve(cache_jsapi_ticket); 144 | }) 145 | .catch(function(e) { 146 | reject(e); 147 | }); 148 | }); 149 | } 150 | 151 | /** 152 | * 发送请求 153 | * @param {[type]} options [description] 154 | * @return {[type]} [description] 155 | */ 156 | function sendHttpsRequest (options) { 157 | return new Promise(function(resolve, reject) { 158 | var data = ''; 159 | var reqHttps = https.request(options, function(response) { 160 | response.on('data', function(chunk) { 161 | data += chunk; 162 | }).on('end', function() { 163 | resolve(JSON.parse(data)); 164 | }); 165 | }); 166 | 167 | reqHttps.on('error', function(e) { 168 | console.log(e); 169 | reject(e); 170 | }); 171 | 172 | reqHttps.end(); 173 | }); 174 | } 175 | 176 | /** 177 | * 获取随机字符串 178 | * @return {[type]} [description] 179 | */ 180 | function genNoncestr () { 181 | var length = 16; 182 | var arr = []; 183 | var randomNumber = 0; 184 | while(length > arr.length) { 185 | randomNumber = parseInt(Math.random() * nonstr.length, 10) + 1; 186 | arr.push(nonstr[randomNumber]); 187 | } 188 | return arr.join(''); 189 | } 190 | 191 | /** 192 | * 计算签名 193 | * @param {[type]} jsapiTicket [description] 194 | * @param {[type]} noncestr [description] 195 | * @param {[type]} timestamp [description] 196 | * @param {[type]} url [description] 197 | * @return {[type]} [description] 198 | */ 199 | function compSignature(jsapiTicket, noncestr, timestamp, url) { 200 | var str = [ 201 | 'jsapi_ticket=', jsapiTicket, 202 | '&noncestr=', noncestr, 203 | '×tamp=', timestamp, 204 | '&url=', url 205 | ].join(''); 206 | return sha1(str); 207 | } 208 | 209 | /** 210 | * sha1 加密 211 | * @param {[type]} data [description] 212 | * @return {[type]} [description] 213 | */ 214 | function sha1(data) { 215 | var generator = crypto.createHash('sha1'); 216 | generator.update(data); 217 | return generator.digest('hex') 218 | } -------------------------------------------------------------------------------- /public/wechat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 动微信分享 3 | */ 4 | (function(global, factory) { 5 | if ( typeof define === "function") { 6 | if(define.amd) { 7 | define(function() { 8 | return factory(); 9 | }); 10 | } else if(define.cmd) { 11 | define(function(require, exports, module) { 12 | module.exports = factory(); 13 | }); 14 | } 15 | } else if( typeof module === "object" && typeof module.exports === "object" ) { 16 | module.exports = factory(); 17 | } else { 18 | global.Qwechat = factory(); 19 | } 20 | }(typeof window !== "undefined" ? window : this, function() { 21 | 22 | var URL_WX_API_JS = 'http://res.wx.qq.com/open/js/jweixin-1.0.0.js', 23 | _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 24 | G_WX_JSAPI = [ 25 | 'checkJsApi', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 26 | 'onMenuShareWeibo', 'hideMenuItems', 'showMenuItems' 27 | ], 28 | // 配置签名相关的参数 29 | URL_Q_SIGNATURE = '', // 获取签名的服务端地址 30 | // api 的加载状态 31 | API_STATE_LOADING = 1, 32 | API_STATE_LOADED = 2, 33 | apiLoadState = 0, 34 | // 缓存回调函数 35 | callbackList = []; 36 | 37 | // 初始化 38 | function initWechat (callback) { 39 | // 加载脚本 40 | loadScript(URL_WX_API_JS, function() { 41 | // 获取微信签名 42 | getSignature(function(sign) { 43 | // 验证微信授权 44 | wx.config({ 45 | debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 46 | appId: sign.appId, // 必填,公众号的唯一标识 47 | timestamp: sign.timestamp, // 必填,生成签名的时间戳 48 | nonceStr: sign.nonceStr, // 必填,生成签名的随机串 49 | signature: sign.signature, // 必填,签名,见附录1 50 | jsApiList: G_WX_JSAPI // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 51 | }); 52 | // 授权验证通过 53 | wx.ready(function() { 54 | callback(); 55 | }); 56 | // 授权错误 57 | wx.error(function(res) { 58 | console.error('授权错误'); 59 | console.error(res); 60 | }); 61 | }); 62 | }); 63 | } 64 | 65 | /** 66 | * 动态加载 js 脚本 67 | * @param {[type]} url [description] 68 | * @param {Function} callback [description] 69 | * @return {[type]} [description] 70 | */ 71 | function loadScript(url, callback) { 72 | var head = document.getElementsByTagName('head')[0], 73 | js = document.createElement('script'); 74 | js.setAttribute('type', 'text/javascript'); 75 | js.setAttribute('src', url); 76 | head.appendChild(js); 77 | //执行回调 78 | js.onload = function() { 79 | if (typeof callback === 'function') { 80 | callback(); 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * 获取签名 87 | * @param {[type]} arguments [description] 88 | * @return {[type]} [description] 89 | */ 90 | function getSignature (callback) { 91 | var currentPageUrl = window.location.href.split('#')[0], 92 | params = { 93 | activityUrl: base64_encode(currentPageUrl) 94 | }; 95 | $.post(URL_Q_SIGNATURE, params, function(resp) { 96 | if (typeof resp === 'object' && resp.status === 0 && 97 | typeof resp.data === 'object' 98 | ) { 99 | if(typeof callback === 'function') { 100 | callback(resp.data); 101 | } 102 | } else { 103 | console.error("微信签名请求出错!"); 104 | } 105 | }); 106 | } 107 | 108 | function base64_encode(input) { 109 | var output = ""; 110 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 111 | var i = 0; 112 | input = _utf8_encode(input); 113 | while (i < input.length) { 114 | chr1 = input.charCodeAt(i++); 115 | chr2 = input.charCodeAt(i++); 116 | chr3 = input.charCodeAt(i++); 117 | enc1 = chr1 >> 2; 118 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 119 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 120 | enc4 = chr3 & 63; 121 | if (isNaN(chr2)) { 122 | enc3 = enc4 = 64; 123 | } else if (isNaN(chr3)) { 124 | enc4 = 64; 125 | } 126 | output = output + 127 | _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + 128 | _keyStr.charAt(enc3) + _keyStr.charAt(enc4); 129 | } 130 | return output; 131 | } 132 | 133 | function _utf8_encode(string) { 134 | string = string.replace(/\r\n/g, "\n"); 135 | var utftext = ""; 136 | for (var n = 0; n < string.length; n++) { 137 | var c = string.charCodeAt(n); 138 | if (c < 128) { 139 | utftext += String.fromCharCode(c); 140 | } else if ((c > 127) && (c < 2048)) { 141 | utftext += String.fromCharCode((c >> 6) | 192); 142 | utftext += String.fromCharCode((c & 63) | 128); 143 | } else { 144 | utftext += String.fromCharCode((c >> 12) | 224); 145 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 146 | utftext += String.fromCharCode((c & 63) | 128); 147 | } 148 | } 149 | return utftext; 150 | } 151 | 152 | return { 153 | ready: function(fn) { 154 | if (typeof fn !== 'function') { 155 | console.error('Qwechat.ready(fn) error, fn param is invalid!'); 156 | return; 157 | } 158 | if (apiLoadState === API_STATE_LOADED) { 159 | // api 已经加载完了, 直接回调即可 160 | fn(wx); 161 | } else if (apiLoadState === API_STATE_LOADING) { 162 | // api 还在准备中, 先缓存回调函数 163 | callbackList.push(fn); 164 | } else { 165 | // api 还没有加载,先缓存回调函数 166 | callbackList.push(fn); 167 | // 设置当前状态为加载中 168 | apiLoadState = API_STATE_LOADING; 169 | // 执行初始化函数 170 | initWechat(function() { 171 | // 设置当前状态为已加载 172 | apiLoadState = API_STATE_LOADED; 173 | // 初始化完成, 执行回调队列中缓存的所有函数 174 | var callback = null; 175 | while((callback = callbackList.shift())) { 176 | callback(wx); 177 | } 178 | }); 179 | 180 | } 181 | } 182 | }; 183 | })); --------------------------------------------------------------------------------