準備工作:
申請伺服器 公眾號 基本配置 這些微信公眾平臺上都有,就不介紹了,接下來進入正題。
➣ 微信網頁授權
node js-sdk 授權
公眾平臺的技術文件目的為了簡明扼要的交代介面的使用,語句難免晦澀,這裡寫了些了我所理解的微信開放平臺中關於利用node.js使用授權和js-sdk的一些方法,詳情請見微信公眾平臺.如果使用者在微信客戶端中訪問第三方網頁,公眾號可以通過微信網頁授權機制,來獲取使用者基本資訊,進而實現業務邏輯。隨著微信管控越發嚴厲,像一些最基本的網頁轉發都需要授權處理才能獲取到圖片和描述,描述審查也是相當嚴格。#
網頁授權回撥域名的說明
在微信公眾號請求使用者網頁授權之前,開發者需要先到公眾平臺官網中的“開發 – 介面許可權 – 網頁服務 – 網頁帳號 – 網頁授權獲取使用者基本資訊”的配置選項中,修改授權回撥域名。請注意,這裡填寫的是域名(是一個字串),而不是URL,因此請勿加 http:// 等協議頭;
授權回撥域名配置規範為全域名,比如需要網頁授權的域名為:www.qq.com,配置以後此域名下面的頁面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以進行OAuth2.0鑑權。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com無法進行OAuth2.0…
網頁授權的兩種scope的區別(snsapi_base snsapi_userinfo)
以snsapi_base為scope發起的網頁授權,是用來獲取進入頁面的使用者的openid的,並且是靜默授權並自動跳轉到回撥頁的。使用者感知的就是直接進入了回撥頁(往往是業務頁面)
以snsapi_userinfo為scope發起的網頁授權,是用來獲取使用者的基本資訊的。但這種授權需要使用者手動同意,並且由於使用者同意過,所以無須關注,就可在授權後獲取該使用者的基本資訊。
網頁授權access_token和普通access_token的區別
微信網頁授權是通過OAuth2.0機制實現的,在使用者授權給公眾號後,公眾號可以獲取到一個網頁授權特有的介面呼叫憑證(網頁授權access_token),通過網頁授權access_token可以進行授權後介面呼叫,如獲取使用者基本資訊;
其他微信介面,需要通過基礎支援中的“獲取access_token”介面來獲取到的普通access_token呼叫。
➣ 具體步驟:
* 程式碼配置:
package.json
{
"name": "js-sdk",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"babel-runtime": "^6.26.0",
"body-parser": "^1.18.2",
"cheerio": "^1.0.0-rc.2",
"connect-mongo": "^2.0.1",
"connect-redis": "^3.3.3",
"cookie-parser": "^1.4.3",
"crypto": "^1.0.1",
"ejs": "^2.5.7",
"express": "^4.16.2",
"express-session": "^1.15.6",
"fs": "^0.0.1-security",
"mongoose": "^5.0.16",
"morgan": "^1.9.0",
"redis": "^2.8.0",
"request": "^2.83.0",
"sha1": "^1.1.1",
"util": "^0.10.3",
"utility": "^1.13.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.1.0",
"gulp-babel": "^7.0.0",
"gulp-concat": "^2.6.1",
"gulp-connect": "^5.2.0",
"gulp-imagemin": "^4.1.0",
"gulp-minify-css": "^1.2.4",
"gulp-minify-html": "^1.0.6",
"gulp-px2rem-plugin": "^0.4.0",
"gulp-uglify": "^3.0.0",
"gulp-util": "^3.0.8"
}
}
app.js
const express = require("express");
const bodyParser = require("body-parser");
const path = require("path");
const logger = require("morgan");
const cookieParser = require("cookie-parser");
const indexRoute = require("./app/routes/index.route");
const app = express();
app.set(`views`, path.join(__dirname, `app/views`));
app.set(`view engine`, `ejs`);
/*配置靜態檔案路徑*/
app.use(express.static(path.join(__dirname, "public")));
/*配置請求日誌*/
app.use(logger("dev"));
/*解析application/json格式資料*/
app.use(bodyParser.json());
/*解析application/www-x-form-urlencoded格式資料*/
app.use(bodyParser.urlencoded({extended: false}));
/*解析cookie*/
app.use(cookieParser());
/*解析session*/
const session = require(`express-session`);
app.use(session({
secret: "123456", //建議使用隨機字串
resave: true,
saveUninitialized: true,
cookie: {maxAge: 24 * 60 * 60 * 1000}
}));
/*配置路由*/
app.use("/", indexRoute);
app.use((req,res,next)=>{
let err = new Error("Error 404, the source is not found!");
err.status = 404;
next(err);
});
app.use((err, req, res, next)=>{
console.log(err);
res.status(err.status || 500).send(err.message);
next();
});
module.exports = app;
config/env.config.js
module.exports = {
port:"80",
"token":"yourtoken",
"appID":"***",
"appsecret":"***",
"userAppID": "***",
"userAppSecret": "***"
}
app/routes/index.routes.js
const express = require(`express`);
const path = require("path");
const authMiddleware = require("../middlewares/auth.middleware");
const router = express.Router();
const querystring = require(`querystring`);
const url = require(`url`);
const cheerio = require(`cheerio`)
router.get("/", authMiddleware.getCode, (req,res,next)=>{
res.sendFile(path.join(__dirname, "../views/index.html"));
})
app/views/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
這裡只是測試getCode成功與否
</body>
</html>
新建 app/config.access_token.json待用
新建 app/config.ticket.json待用
app/middlewares/auth.middlewares.js
exports.getUserInfo = (req,res,next)=>{
console.log("<-----------------獲取getUserInfo--------------------->")
console.log(`----->req.access_token : `+req.access_token);
let access_token = req.access_token;
let openid = req.openid;
let url = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`;
request(url, (err,httpResponse,body)=>{
console.log("---->--通過access_token和openid獲取到的使用者個人資訊 :")
console.log(body);
let result = JSON.parse(body);
res.cookie("openid", result.openid, {maxAge: 24 * 60 * 60 * 1000, httpOnly: false});
res.cookie("nickname", result.nickname, {maxAge: 24 * 60 * 60 * 1000, httpOnly: false});
res.cookie("headimgurl", result.headimgurl, {maxAge: 24 * 60 * 60 * 10000, httpOnly: false});
res.cookie("unionid", result.unionid, {maxAge: 24 * 60 * 60 * 1000, httpOnly: false})
next();
})
}
* 以snsapi_base為scope發起的授權
第一步:使用者同意授權,獲取code
app/middleares/auth.middlewares.js
const config = require("../../config/env.config");
const request = require("request");
const appid = config.appID;
const appsecret = config.appsecret;
/*獲取code*/
exports.getCode = function(req,res,next){
console.log(`--|cookies : `+ JSON.stringify(req.cookies));
if(req.cookies.openid){
next();
}else{
let back_url = escape(req.url);//解碼,解決url?後面引數返回消失問題 2.req.url 獲取URL
console.log(`獲取的url路由引數為 :`+back_url)
let redirect_uri = `{你的域名}/getUserInfo?back_url=${back_url}`; //注意這裡執行了getUserInfo路由
let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect `;
console.log(`重定向的url : `+url);
//next();
res.redirect(url);//res.redirect()重定向跳轉 引數僅為URL時和res.location(url)一樣
};
};
第二步:通過code換取網頁授權access_token
/*獲取access_token*/
exports.getAccess_token = (req,res,next)=>{
console.log("<------------------獲取snsapi_base access_token----------------------->")
console.log(JSON.stringify(req.query))
let code = req.query.code;
let url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${appsecret}&code=${code}&grant_type=authorization_code `;
request(url, (err, httpResponse, body)=>{
console.log(err);
console.log(`--||--code換取的所有資訊 :`+body);
let result = JSON.parse(body);
req.access_token = result.access_token;
req.openid = result.openid;
next();
})
};
第三步:拉取使用者資訊(需scope為 snsapi_userinfo)
/getUserInfo使用了getAccess_token getUserInfo 中介軟體 在code沒過期的情況下可以進一步獲取access_token 和個人資訊
router.get("/getUserInfo", authMiddleware.getAccess_token, authMiddleware.getUserInfo, function (req, res, next) {
console.log("<------------------`/getUserInfo`----------------------->");
console.log(`----->|查詢的url字串引數 :` + JSON.stringify(req.query));
let back_url = req.query.back_url;
for (let item in req.query) {
if (item !== "back_url" && item !== "code" && item !== "state") {
back_url += "&" + item + "=" + req.query[item];
};
};
console.log(`---->|重新篩選路徑back_url : ` + back_url);
res.redirect(back_url);
});
# * 以snsapi_userinfo為scope發起的授權
app/middlewares/accessToken.middlesware.js
let weixinConfig = require("../../config/env.config.js");
let request = require("request");
let fs = require("fs");
//獲取accessToken
exports.accessToken = function (req, res, next) {
console.log("<------------------`獲取snsapi_userinfo accessToken`----------------------->");
let valide = isValide(); //{ code: 0, result: result.access_token } or{code:1001}
if (valide.code === 0) {
//access_token還沒過期,用以前的
req.query.access_token = valide.result;
next();
} else {
//重新獲取access_token && expire_in
let appid = weixinConfig.appID;
let secret = weixinConfig.appsecret;
let url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret;
request(url, function (error, response, body) {
let result = JSON.parse(body);
let now = new Date().getTime(); //new Date().getTime() 獲得的是毫秒
result.expires_in = now + (result.expires_in - 20) * 1000; //expire_in一般是7200s 提前20毫秒
req.query.access_token = result.access_token; //new access_token
req.query.tokenExpired = result.expires_in; // 7200s
next();
});
};
};
//獲取ticket
exports.ticket = function (req, res, next) {
console.log("<------------------`獲取ticket`----------------------->");
let ticketResult = isTicket();
if (ticketResult.code === 0) {
console.log(`已經有了ticket : ` + JSON.stringify(ticketResult));
req.query.ticket = ticketResult.result;
next();
} else {
console.log("開始獲取ticket");
let access_token = req.query.access_token;
let _tokenResult = {
access_token: req.query.access_token,
expires_in: req.query.tokenExpired
};
let url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + access_token + "&type=jsapi";
request(url, function (err, response, body) {
let result = JSON.parse(body);
console.log(result);
if (result.errcode == "0") {
let now = new Date().getTime();
result.expires_in = now + (result.expires_in - 20) * 1000; // 改變時間為當前時間的兩小時後
fs.writeFileSync("./config/access_token.json", JSON.stringify(_tokenResult)); //fs.writeFileSync:以同步的方式將data寫入檔案,檔案已存在的情況下,原內容將被替換。
fs.writeFileSync("./config/ticket.json", JSON.stringify(result));
console.log(`非同步寫入access_token ticket.json`);
req.query.ticket = result.ticket;
next();
};
});
};
};
function isValide() {
//有效
let result = fs.readFileSync("./config/access_token.json").toString(); //同步讀取json檔案 //這裡用toString的原因:讀出來的資料是一堆包含著16進位制數字的物件,必須通過toString轉為字串形式
if (result) {
result = JSON.parse(result);
let now = new Date().getTime();
if (result.access_token && result.expires_in && now < result.expires_in) {
console.log("access_token 還在7200s以內,沒有過期"); //access_token有效 expires_in應該指的是距離生成時間的7200秒後
return { code: 0, result: result.access_token };
} else {
console.log("access_token 失效");
return { code: 1001 };
}
} else {
return { code: 1001 };
};
};
function isTicket() {
let result = fs.readFileSync("./config/ticket.json").toString();
console.log("result:", result);
if (result) {
result = JSON.parse(result);
console.log(result);
let now = new Date().getTime();
if (result.ticket && result.expires_in && now < result.expires_in) {
console.log("ticket有效,沿用當前ticket.json裡的ticket");
return { code: 0, result: result.ticket };
} else {
console.log("ticket無效需要獲取");
return { code: 1001 };
}
} else {
return { code: 1001 };
};
}
accessToken.middlesware.js寫了關於獲取以snsapi_userinfo為scope發起的網頁授權的access_token ticket,並用fs以json字串的形式存到本地,並檢測過期時間,如果沒過期就繼續讀取使用,如果過期就重新獲取並儲存在心的access_token ticket到本地
app/routes/index.routes.js
const crypto = require("crypto");
const sha1 = require("sha1");
const accessTokenMiddle = require("../middlewares/accessToken.middleware.js");
const weixin = require("../../config/env.config");
router.get("/weixin", accessTokenMiddle.accessToken, accessTokenMiddle.ticket, function (req, res, next) {
console.log("<------------------`/weixin`----------------------->");
console.log(`----->| req.query : ` + JSON.stringify(req.query));
crypto.randomBytes(16, function (ex, buf) {
let appId = weixin.appID;
let noncestr = buf.toString("hex");
let jsapi_ticket = req.query.ticket;
let timestamp = new Date().getTime();
timestamp = parseInt(timestamp / 1000);
let url = req.query.url;
console.log("引數 :");
console.log(noncestr);
console.log(jsapi_ticket);
console.log(timestamp);
console.log(url);
let str = ["noncestr=" + noncestr, "jsapi_ticket=" + jsapi_ticket, "timestamp=" + timestamp, "url=" + url].sort().join("&");
console.log("待混淆加密的字串 : ");
console.log(str);
let signature = sha1(str);
console.log("微信sdk簽名signature :");
console.log(signature);
let result = { code: 0, result: { appId: appId, timestamp: timestamp, nonceStr: noncestr, signature: signature } };
res.json(result); //res.json 等同於將一個物件或陣列傳到給res.send()
});
});
在html頁面使用微信公眾平臺提供的API 需要引用 http://res.wx.qq.com/open/js/…
在靜態檔案中呼叫分享功能的api 更多API請開啟 # 微信JS-SDK說明文件
public/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<p>userList....</p>
<button style="color:purple;" onclick="clickMe()">clickMe</button>
</body>
<script src="http://www.jq22.com/jquery/jquery-2.1.1.js"></script>
<script src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
<script src="../js/userList.js"></script>
</html>
public/js/userList.js
let signatureUrl = url.split("#")[0];
let URL = encodeURIComponent(signatureUrl);
let title = "這是分享的表標題";
let desc = "this is description";
let shareUrl = window.location.href;
let logo = "http://yizhenjia.com/dist/newImg/logo.png";
SHARE(title, desc, shareUrl, logo);
$.get("/weixin?url=" + URL, function(result) {
if (result.code == 0) {
wx.config({
debug: false, // 開啟除錯模式,呼叫的所有api的返回值會在客戶端alert出來,若要檢視傳入的引數,可以在pc端開啟,引數資訊會通過log打出,僅在pc端時才會列印。
appId: result.result.appId, // 必填,公眾號的唯一標識
timestamp: result.result.timestamp, // 必填,生成簽名的時間戳
nonceStr: result.result.nonceStr, // 必填,生成簽名的隨機串
signature: result.result.signature, // 必填,簽名,見附錄1
jsApiList: ["onMenuShareAppMessage", "onMenuShareTimeline", "chooseImage", "scanQRCode", "getLocation", "openLocation"] // 必填,需要使用的JS介面列表,所有JS介面列表見附錄2
});
};
});
function SHARE(title, desc, shareUrl, logo) {
wx.ready(function() {
// config資訊驗證後會執行ready方法,所有介面呼叫都必須在config介面獲得結果之後,config是一個客戶端的非同步操作,所以如果需要在頁面載入時就呼叫相關介面,則須把相關介面放在ready函式中呼叫來確保正確執行。對於使用者觸發時才呼叫的介面,則可以直接呼叫,不需要放在ready函式中。
//分享
wx.onMenuShareAppMessage({
title: title, // 分享標題
desc: desc, // 分享描述
link: shareUrl, // 分享連結
imgUrl: logo, // 分享圖示
type: ``, // 分享型別,music、video或link,不填預設為link
dataUrl: ``, // 如果type是music或video,則要提供資料連結,預設為空
success: function() {
使用者確認分享後執行的回撥函式
alert("分享成功!");
},
cancel: function() {
// 使用者取消分享後執行的回撥函式
},
fail: function(err) {
alert("分享失敗");
}
});
});
wx.error(function(res) {
// config資訊驗證失敗會執行error函式,如簽名過期導致驗證失敗,具體錯誤資訊可以開啟config的debug模式檢視,也可以在返回的res引數中檢視,對於SPA可以在這裡更新簽名。
//alert("Error");
});
}
註釋:
微信開發必須在微信開發者工具上開發,且只能是預設80埠,在開發中經常有80埠被佔用的情況,如果有請使用
lsof -i tcp:80
kill -9 程式
如果想在手機上測試 並抓包資料 可以使用charles抓包工具
開啟charles 點選Proxy setting 設定 port
保證手機和電腦處於同一Wi-Fi下,配置手動代理 輸入IP和埠 檢視ip地址 :charles上可檢視 或者終端輸入ifconfig (cmd:ipconfig)
掃碼或使用地址即可訪問