[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录

文档标识:[C-180826-M-180908]
Github来源:ryan4g

原创内容,转载请先告知

项目背景

看到登录时可以选择微信扫描登录,想了想其中的原理好像并不复杂。正好一直在学习Node.js,想着靠自己现有的水平,实现这个PC端生成二维码,通过手机扫描二维码,实现PC端触发登录的例子。

技能准备

本人的Node.js水平刚入门,所用到的技能并不深入,能够实现基本的路由和二维码生成就刚好能满足这个项目要求。项目用到了以下npm库:

  1. express (用来实现路由处理)
  2. qr-image(用来生成带跳转网址的二维码)
  3. uuid-js (用来生成唯一UID,以区分响应不同窗口的二维码)
  4. Node自带的一些库,不需要再npm 安装:fs (用于读取本地文件)、http (网络库)、url (网址解析)、path(本地路径解析)

项目架构

在开始项目编程之前,先把想要实现的东西过一遍,形成了以下脑图:


[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第1张图片
项目结构脑图.png

文件结构和使用Express 应用生成器生成的差不多,但对于本项目来说,还是有些多余了,对文件结构进行了一些调整:

.
├── index.js
├── bin
│   └── data.json
├── package.json
├── public
│   ├── css
│       └── style.css
│   ├── images
│   └── js
│       ├── index.js
│       └── jquery-3.3.1.min.js
└── views
    ├── confirm.html
    └── main.html

从文件结构图上可以看到,用到了jquery,这里主要是有一个AJAX操作,用到了jquery.ajax比较方便实现。然后,views里面的是页面模板,express支持的是ejs和jade,由于目前还没实际使用过,所以用了比较笨的方法。(后记:实际上可以用ejs模板,然后可以使用express.render,这是比较正常的做法。)

项目编程

开始之前

在开始编程之前,由于这篇文章可能会帮助到一部分比我都新的入门者,这里还是要提及一些工具的安装:

  1. 首先,需要确认自己的node.js环境搭建完毕,具体安装教程请参考这里——Node.js安装
  2. 然后,在国内的话,由于连接npm源的速度较慢,最好把npm源换成国内的cnpm,更换方法请参考这里——淘宝NPM镜像
  3. 最后,项目中用到了jquery,可在jquery官网下载

npm模块安装

在了解以上情况后,现在把所需要用到的模块安装进来,这里假设之前没有全局安装过,仅对本项目安装。

  1. 新建一个文件夹用于存放项目,这里命名为qrcodelogindemo,使用以下命令初始化项目:
npm init

跳出的提示按回车默认即可,或者根据自己想法写点什么。主要是为了生成package.json文件。

  1. 按照前面技能准备里提及的模块,现在可以一个个安装,使用以下命令进行安装:
cnpm install express qr-image uuid-js --save
  1. 文档结构按照项目架构里手动新建文件夹(文件可以暂时不用管),或者使用express 应用生成器自动生成,然后删除一些不需要的文件夹,自动生成需要使用以下命令:
cnpm install express-generator -g
# 使用自动生成的话,需要在项目外部执行,或者手动移出内容
express qrcodelogindemo

PC端页面构成

页面构思

在安装完毕所有模块后,对PC端需要的展现的页面进行构思,要实现的效果,就和微信授权登录页面一样:

  1. 显示一个二维码图片


    [Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第2张图片
    微信登录授权.PNG
  2. 扫描后,页面会显示“成功扫描,请确认登录”


    [Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第3张图片
    微信成功扫描.PNG
  3. 在手机端如果点击取消,页面会显示“已取消登录,可再次扫描”


    [Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第4张图片
    微信取消登录.PNG
  4. 在手机端如果点击确认,页面跳转。(这里是跳回二维码页面重新生成,按的操作就是跳回主页,原理一样)
实现难点

在这个页面中,难点是:手机端扫描后,如何通知PC端页面进行状态处理? 一般来说,在前后端的交互过程,是由前端主动向后端进行数据请求,这里可以使用的技术有:

  1. AJAX
  2. Websocket

然而在本项目里,要在手机端确认登录后,后端通知PC端网页进行状态处理,明显和一般情况不一样,这里更像是后端主动向前端发送数据。Websocket不太适合用在这个项目,更适合AJAX请求,那么这里,就涉及到一个概念:反向AJAX。实现这个技术可以有以下几种:

  1. 间隔轮询(隔一段时间发送一个AJAX请求)
  2. 长轮询(一个AJAX请求不间断,即成功返回又立即请求)
  3. 长连接(一个AJAX请求后不结束,后端不断返回数据)

受限于本人的水平,反向AJAX的概念并没有找到更多的资料,其中第1、2种都比较容易理解,第3种如何保持一个AJAX请求不结束的方法暂时还没有思路。

那么来看看微信授权的二维码页面是采用哪种方式,打开google开发者工具,切换到Network页面,可以看到请求队列:

[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第5张图片
微信页面-长轮询.PNG

可以发现,它使用的是长轮询,维持了一个27s左右的AJAX请求。查看网页的javascript脚本,可以发现这么一段代码:

[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第6张图片
微信页面-javascript.PNG

复制下来并格式化后:

[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第7张图片
微信页面-javascript格式化.PNG
[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第8张图片
微信页面-javascript格式化2.PNG

这里可以发现,在页面的javascript脚本中,使用了AJAX请求并根据服务端返回不同的state代码,对该请求进行了重新延时发送请求。那么基于这点,可以在服务端也提供一个返回状态的接口用于响应AJAX请求。
解决了这个问题,就可以根据不同的状态进行页面更改,例如:已扫描、取消扫描、确认登录的状态。

页面编码

现在主要是页面的AJAX请求写法,这里使用了Jquery的Ajax方法,直接向后端发送请求,并提供了状态响应。
以下是main.html页面中的javascript代码:

let scanned = function(){
            jQuery('.qrcode-bottom p').text("已成功扫描,等待手机确认登录");
        };

        let verified = function(username) {  
            jQuery('.qrcode-bottom p').text('欢迎您:' + username + ',您已成功登录');
            
            // 成功登录后,2秒后跳转,此处重新加载
            setTimeout(()=>{
                window.location.reload();
            }, 2000);
        };

        let canceled = function(){
            jQuery('.qrcode-bottom p').text("已取消登录,请重新扫描确认");
        };

        let longpolling = function (){
            jQuery.ajax({
                url: "/verified?uid=SESSION_UID",
                dataType: "json",
                cache: false,
                timeout: 30000,
                success: function(respond){
                    let cmdCode = respond.cmd;
                    let delayTime = 3000;

                    if(cmdCode === "scanned"){
                        scanned();

                        delayTime = 1000;
                        setTimeout(longpolling, delayTime);
                    }
                    else if (cmdCode === "verified"){
                        verified(respond.user);
                    }
                    else if (cmdCode === "canceled"){
                        canceled();
                        setTimeout(longpolling, delayTime);
                    }
                    else{
                        setTimeout(longpolling, delayTime);
                    }
                },
                error: function(){
                    setTimeout(longpolling, 5000);
                }
            });
        };

        setTimeout(longpolling, 100);

注意:这里有个SESSION_ID的预置字符串,这里后端需要进行处理。当然,如果使用模板的话,可以使用ejs模板的变量赋值。

手机端页面构成

页面构思

在解决PC端的网页后,现在进行手机端的页面构思,需要使用的功能:

  1. 显示确认登录、取消登录的界面
  2. 取消登录、确认登录的状态能够发送到服务端
  3. 确认登录后进行成功跳转
  4. 取消登录后退出微信内置浏览器
实现难点

没有难点,只要按下按钮时能够直接使用href调用服务端的接口,或者通过DOM的click事件,使用AJAX与服务端交互,把UID和状态传入即可。

页面编码

这里确认按钮可以直接进行href跳转,也可使用click事件处理AJAX请求,这里使用了href跳转:

确认登录
取消登录

然后,由于取消登录按钮需要响应微信内置浏览器的退出功能,则只能使用AJAX的方式,调用成功后直接调用浏览器的退出功能,以下是取消登录按钮的javascript代码:

jQuery('#cancelBtn').click(()=>{

      jQuery.ajax({
        url: "/confirmed?uid=SESSION_UID&operate=cancel",
        dataType: "html",
        success: function (){

          // 微信内置浏览器退出
          WeixinJSBridge.call('closeWindow');
        }
      });
      
});

数据存储方式

JSON文件读写函数

由于这个项目需要的数据存储功能非常简单,在此使用一个JSON文件进行存储即可,而且使用fs类库进行读写也非常方便。
在node服务端定义了两个函数,在进行状态更改时,可直接调用进行JSON文件的读写。以下是node端的代码:

/*
 * Description: 写入JSON文件
 * Params: 
 * fileName - JSON文件所在路径
 * uid - 生成的uid
 * writeData - 需要写入的JSON格式数据
 * 
 */
setJSONValue = function(fileName, uid, writeData){

  let data = fs.readFileSync(fileName);

  let users = JSON.parse(data.toString());
  let addFlag = true;
  let delFlag = (writeData === null);

  for (let i = 0; i < users.data.length; i++){
    if (users.data[i].uid === uid){
      addFlag = false;

      if (delFlag){
        users.data.splice(i,1);
      }
      else{
        users.data[i].status = writeData.status;

        console.log("writeJSON: " + JSON.stringify(users.data[i]) + " modified.");
      }
    }
  }

  if(addFlag){
    users.data.push(writeData);
    console.log("writeJSON: " + JSON.stringify(writeData) + " inserted.");
  }

  // 同步写入文件
  let writeJSON = JSON.stringify(users);
  fs.writeFileSync(fileName, writeJSON);
}

/*
 * Description: 读取JSON文件(要返回数据,选择同步读取)
 * Params: 
 * fileName - JSON文件所在路径
 * uid - 生成的uid
 * 
 */
getJSONValue = function(fileName, uid){

  let readData = null;

  // 同步读取文件
  let data = fs.readFileSync(fileName);
        
  let users = JSON.parse(data.toString());

  for (let i = 0; i < users.data.length; i++){
    if (users.data[i].uid === uid){
      readData = JSON.stringify(users.data[i]);
      break;
    }
  }

  return readData;
}
JSON文件结构

JSON文件主要写入以下信息,用于解析和数据交互,以下为JSON格式的文件内容(有数据的情况下):

{"data":[
  {"uid":"95c0e9ba-ae99-43c7-ab6a-1e357cc24b5f","status":"scanned","name":"USER"}
]}

如果是还没有的情况下,需要预先创建一个JSON文件,内容包含一个data键的空数组即可:

{"data":[]}

服务端构思

在经过以上PC页面和手机页面的分析,在服务端需要提供相应的接口,这里一共需要提供的功能接口如下:

  1. 返回QRCode 图片的接口
  2. 响应扫描动作的接口
  3. 响应查询状态的接口
  4. 响应确认登录的接口
QRCode图片接口编码

在生成二维码时,考虑到微信扫描二维码后,会根据内容进行处理,如果是一个网址则会直接使用内置浏览器进行跳转。那么生成二维码的时候,确认了需要包含的信息是:一个包含UID的网址

// 生成二维码图片并显示
app.get('/qrcode', function (req, res, next) {
  
  let uid = url.parse(req.url, true).query.uid;

  try {
    if (typeof(uid) !== "undefined"){
      
      // 写入二维码内的网址,微信扫描后自动跳转
      let jumpURL = "http://" + localNetAddress + "/scanned?uid=" + uid;
      // 生成二维码(size:图片大小, margin: 边框留白)
      var img = qrcode.image(jumpURL, {size :6, margin: 2});
      res.writeHead(200, {'Content-Type': 'image/png'});
      img.pipe(res);
    }
    else{
      res.writeHead(414, {'Content-Type': 'text/html'});
      res.end('

414 Request-URI Too Large

'); } } catch (e) { res.writeHead(414, {'Content-Type': 'text/html'}); res.end('

414 Request-URI Too Large

'); } });
响应扫描动作接口

在手机进行扫描二维码后,会直接跳转二维码内包含的网址,这时候调用该接口。需要实现手机确认界面的显示以及将该UID的状态记录到JSON文件中,以便PC端网页做出相应改变。

// 显示手机扫描后的确认界面
app.get('/scanned', function(req, res){

  let uid = url.parse(req.url, true).query.uid;

  if (typeof(uid) !== "undefined"){

    generateHTML(uid, req, res, path.join(__dirname, '/views/confirm.html'));

    console.log("uid: '" + uid + "' scanned.");

    // 获取JSON文件内对应uid的数据,更改其数据状态
    let jsonData = getJSONValue(path.join(__dirname, '/bin/data.json'), uid);
    
    if(jsonData === null){
      jsonData = {
        uid: uid,
        status: "scanned",
        name: "USER"
      }
    }
    else{
      jsonData = JSON.parse(jsonData);
      jsonData.status = "scanned";
    }

    // 写入JSON文件
    setJSONValue(path.join(__dirname, '/bin/data.json'), uid, jsonData);
  }
  else{
    res.writeHead(414, {'Content-Type': 'text/html'});
    res.end('

414 Request-URI Too Large

'); } });
响应查询状态接口

为了响应PC端网页不断的AJAX请求,在该接口需实现从JSON文件中获取对应UID的实际状态,用于PC端网页响应操作:

// 响应主页不断的AJAX请求
app.get('/verified', function(req, res){
  
  let uid = url.parse(req.url, true).query.uid;

  // normal   - 没有任何触发
  // scanned  - 已扫描
  // canceled - 已取消
  // verified - 已验证
  let dataStatus = {
    cmd: "normal",
    user: ""
  }

  console.log("uid: '" + uid + "' query ...");

  if (typeof(uid) !== "undefined"){
    
    let userData = getJSONValue(
      path.join(__dirname, '/bin/data.json'), 
      uid
    );

    // 返回JSON数据用于首页AJAX操作
    if(userData !== null){
      userData = JSON.parse(userData);
      dataStatus.cmd = userData.status;
      dataStatus.user = userData.name;
    }
  }
  
  // 这里如果设置延时返回,那所有AJAX响应都会等待,微信延时27秒左右的时间内,仍能够做出及时response
  // 目前是无等待响应,这里有待改进
  //setTimeout(function(){
    res.end(JSON.stringify(dataStatus));
  //}, 10000);

});
响应确认登录接口

手机端页面显示两个操作按钮,一个确认登录,一个取消登录。点击取消登录按钮关闭微信内置浏览器,确认登录则需通知服务端,进行页面跳转等操作:

// 在确认界面操作的响应
app.get('/confirmed', function(req, res){
  let uid = url.parse(req.url, true).query.uid;
  let operate = url.parse(req.url, true).query.operate;

  if (typeof(uid) !== "undefined"){

    console.log("uid: '" + uid + "' " + operate);

    let jsonData = getJSONValue(path.join(__dirname, '/bin/data.json'), uid);
    let status = (operate === "confirm") ? "verified" : "canceled";
    
    if(jsonData === null){
      jsonData = {
        uid: uid,
        status: status,
        name: "USER"
      }
    }
    else{
      jsonData = JSON.parse(jsonData);
      jsonData.status = status;
    }

    setJSONValue(path.join(__dirname, '/bin/data.json'), uid, jsonData);

    if (status === "verified"){
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end('

Verified!

'); } else{ res.writeHead(200, {'Content-Type': 'text/html'}); res.end('

Canceled!

'); } } else{ res.writeHead(414, {'Content-Type': 'text/html'}); res.end('

414 Request-URI Too Large

'); } });
文件替换网页SESSION_ID

在项目前面阶段,提到了有一种原始的方法向网页模板传输变量,那就是用文件读写的方式,进行对应字符串替换。这里使用了SESSION_ID作为占位符,在服务端返回模板渲染时,替换为对应的UID,实现数据传递。(当然,会使用jade或ejs模板也可以直接使用模板数据传递)为了方便,这里将模板读写统一放在一个函数里:

/*
 * Description: 读取网页文件,用于替换关键字,相当于简易模板
 * Params: 
 * sessionID - 生成的uid
 * req - 网页请求
 * res - 网页应答
 * fileName - 网页文件所在路径
 */
generateHTML = function(sessionID, req, res, fileName){

  fs.readFile(fileName, 'UTF-8', function(err, data) { 
      if(!err){

        data = data.replace(/SESSION_UID/g, sessionID);  
        res.writeHead(200, {  
            'Content-Type' : 'text/html; charset=UTF-8'  
        });  
        res.end(data);  
      }
      else{
        console.log(err);

        res.writeHead(404, {  
          'Content-Type' : 'text/html; charset=UTF-8'  
      });  
        res.end();  
      }
  });  
};

项目结果

运行方式

在所有工作准备完毕后,现在来验证一下,注意,手机和服务端需要在一个局域网内,否则受扫描后无法跳转,同时根据实际情况,修改一下localNetAddress变量,以便IP正确访问。

如果使用VS Code的话,直接Debug运行;或者在命令行中,运行cnpm startnode index.js 。在浏览器中输入: http://localhost:3000

运行截图

PC端主页
[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第9张图片
扫描二维码.jpg
PC端被扫描后
[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第10张图片
扫描成功.jpg
手机端确认页
[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第11张图片
手机确认页.png
手机端取消后,PC端状态
[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第12张图片
扫描取消.jpg
手机端确认后,PC端生成新的二维码
[Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录_第13张图片
扫描确认成功.jpg

后记

项目从构思到实现总共花了3天时间,这个对于刚入门node的菜鸟水平中规中矩了,在实现的过程中加深了自身对node的使用及理解。虽然项目中存在不少相当拙劣的实现技术实现方式,但这也是在不了解更高级技术的前提下的“曲线救国”,在提升技术能力后可以进行完善。

项目源码发布在:github.com/ryan4g ,由于本人水平有限,若文中存在明显技术错误,欢迎指正,避免误导他人。

如认为本篇文章,对你初学node起到了一定的启发作用,欢迎前往本人github主页对该项目进行star,谢谢。

你可能感兴趣的:([Node.js 实例] 模仿微信授权二维码生成通过微信扫描登录)