微信受權和sdk加密算法

準備工做:css

申請服務器 公衆號 基本配置 這些微信公衆平臺上都有,就不介紹了,接下來進入正題。html

➣ 微信網頁受權

node js-sdk 受權
公衆平臺的技術文檔目的爲了簡明扼要的交代接口的使用,語句不免晦澀,這裏寫了些了我所理解的微信開放平臺中關於利用node.js使用受權和js-sdk的一些方法,詳情請見微信公衆平臺.若是用戶在微信客戶端中訪問第三方網頁,公衆號能夠經過微信網頁受權機制,來獲取用戶基本信息,進而實現業務邏輯。隨着微信管控愈加嚴厲,像一些最基本的網頁轉發都須要受權處理才能獲取到圖片和描述,描述審查也是至關嚴格。#node

網頁受權回調域名的說明

在微信公衆號請求用戶網頁受權以前,開發者須要先到公衆平臺官網中的「開發 - 接口權限 - 網頁服務 - 網頁賬號 - 網頁受權獲取用戶基本信息」的配置選項中,修改受權回調域名。請注意,這裏填寫的是域名(是一個字符串),而不是URL,所以請勿加 http:// 等協議頭;jquery

受權回調域名配置規範爲全域名,好比須要網頁受權的域名爲:www.qq.com,配置之後此域名下面的頁面http://www.qq.com/music.htmlhttp://www.qq.com/login.html 均可以進行OAuth2.0鑑權。但http://pay.qq.comhttp://music.qq.comhttp://qq.com沒法進行OAuth2.0...redis

網頁受權的兩種scope的區別(snsapi_base snsapi_userinfo)

以snsapi_base爲scope發起的網頁受權,是用來獲取進入頁面的用戶的openid的,而且是靜默受權並自動跳轉到回調頁的。用戶感知的就是直接進入了回調頁(每每是業務頁面)express

以snsapi_userinfo爲scope發起的網頁受權,是用來獲取用戶的基本信息的。但這種受權須要用戶手動贊成,而且因爲用戶贊成過,因此無須關注,就可在受權後獲取該用戶的基本信息。json

網頁受權access_token和普通access_token的區別

微信網頁受權是經過OAuth2.0機制實現的,在用戶受權給公衆號後,公衆號能夠獲取到一個網頁受權特有的接口調用憑證(網頁受權access_token),經過網頁受權access_token能夠進行受權後接口調用,如獲取用戶基本信息;gulp

其餘微信接口,須要經過基礎支持中的「獲取access_token」接口來獲取到的普通access_token調用。api

➣ 具體步驟:

* 代碼配置:

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)
掃碼或使用地址便可訪問

在獲取以snsapi_userinfo爲scope發起的網頁受權的時候使用的方式是fs儲存到本地的方式,你也能夠採用其餘方式

相關文章
相關標籤/搜索