共计 13235 个字符,预计需要花费 34 分钟才能阅读完成。
准备工作:
申请服务器 公众号 基本配置 这些微信公众平台上都有,就不介绍了,接下来进入正题。
➣ 微信网页授权
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:80kill -9 进程
如果想在手机上测试 并抓包数据 可以使用 charles 抓包工具
打开 charles 点击 Proxy setting 设置 port 保证手机和电脑处于同一 Wi-Fi 下,配置手动代理 输入 IP 和端口 查看 ip 地址:charles 上可查看 或者终端输入 ifconfig (cmd:ipconfig) 扫码或使用地址即可访问
在获取以 snsapi_userinfo 为 scope 发起的网页授权的时候使用的方式是 fs 储存到本地的方式,你也可以采用其他方式