workerman实现微信公众号带参数二维码扫码识别用户

完整功能体验

查看源代码,有完整的通讯流程实现。

https://iyuu.cn/

什么是Workerman

workerman是一个高性能的PHP socket 服务器框架,workerman基于PHP多进程以及libevent事件轮询库,PHP开发者只要实现一两个接口,便可以开发出自己的网络应用,例如Rpc服务、聊天室服务器、手机游戏服务器等。
workerman的目标是让PHP开发者更容易的开发出基于socket的高性能的应用服务,而不用去了解PHP socket以及PHP多进程细节。 workerman本身是一个PHP多进程服务器框架,具有PHP进程管理以及socket通信的模块,所以不依赖php-fpm、nginx或者apache等这些容器便可以独立运行。

说明

此功能是利用微信公众号带参数二维码,实现扫码识别用户,并且实时通知前端扫码状态,并非ajax轮询!从而进行后续的其他业务逻辑。

工作流程详解

  1. 用workerman框架,编写websocket服务后端监听2129端口等待前端页面https://iyuu.cn/发起连接;进程启动同时再监听一个内部通讯5678端口
  2. 用户进入前端页面,自动连接wss://iyuu.cn:2129;
  3. 用户点击获取二维码,请求二维码生成接口:https://iyuu.cn/qrcode,返回二维码参数:

    {"ticket":"gQH47zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAycTMtdzlMVEhlYzIxcF9jQU50MWsAAgQHjGRdAwR4AAAA","expire_seconds":120,"uid":1735536450}

    注:uid通过函数rand(1,4294967200)生成并查询缓存,确保唯一后放入Redis缓存。

  4. 二维码参数,转发到websocket服务wss://iyuu.cn:2129,websocket服务保存转发来的信息建立映射关系;
  5. 显示二维码:https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={data.ticket},用户扫码;
  6. 微信开发者接口会收到扫码结果,获取到场景值ID;
  7. 根据场景值ID从Redis缓存取出ticket校验通过,执行业务逻辑(登录、绑定、解绑、积分等等),并通过5678端口实时通知用户扫码后的处理结果,服务器返回消息(多了个token字段)如下:
{"ticket":"gQE28DwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyTkVRcEEzazI5NlQxYWhJYXh2MWIAAgQZrApfAwR4AAAA","expire_seconds":120,"uid":2875681051,"token":"IYUU1T01000000a90000b6000002100000b0a8a0000089"}

完整Websocket服务代码

<?php
use Workerman\Worker;
use Workerman\Lib\Timer;
define("APP_PATH",  dirname(__FILE__));
// 心跳间隔40秒
define('HEARTBEAT_TIME', 40);
require_once __DIR__ . '/../../vendor/autoload.php';
require_once APP_PATH . '/Library/Function.php';
$context = array(
    'ssl' => array(
        // 请使用绝对路径
        'local_cert'    => __DIR__ . '/../../Cert/www.iyuu.cn.crt',
        'local_pk'        => __DIR__ . '/../../Cert/www.iyuu.cn.key',
        'verify_peer'                => false,
        //'allow_self_signed' => true, //如果是自签名证书需要开启此选项
    )
);
$worker = new Worker('websocket://0.0.0.0:2129', $context);
$worker->transport = 'ssl';
$worker->name = 'WebSocket';
/*
 * 注意这里进程数必须设置为1,否则会报端口占用错误
 * (php 7可以设置进程数大于1,前提是$inner_text_worker->reusePort=true)
 */
$worker->count = 1;
// 新增加一个属性,用来保存uid到connection的映射(uid是用户id或者客户端唯一标识)
$worker->uidConnections = array();
// 当有客户端连接时
$worker->onConnect = function($connection)
{
    /*
    //定时10秒关闭这个链接,需要10秒内发认证并删除定时器阻止关闭连接的执行
    $connection->auth_timer_id = Timer::add(10, function(){
        $connection->close();
    });

    Timer::del($connection->auth_timer_id);
    */
};

// worker进程启动后创建一个text Worker以便打开一个内部通讯端口
$worker->onWorkerStart = function($worker)
{
    sc('WebSocket服务进程启动成功!');
    // 开启一个内部端口,方便内部系统推送数据,Text协议格式 文本+换行符
    $inner_text_worker = new Worker('text://0.0.0.0:5678');
    $inner_text_worker->onMessage = function($connection, $buffer)
    {
        global $worker;
        if (empty($buffer)) return;
        // $data数组格式,里面有uid,表示向那个uid的页面推送数据
        $data = json_decode($buffer, true);
        if (isset($data['uid'])) {
            $uid = $data['uid'];
            //uid + ticket双重安全验证(防止前端冒用随机uid)
            $data['ticket'] = isset($data['ticket'])&&$data['ticket'] ? $data['ticket'] : '';
            $conn = $worker->uidConnections[$uid];
            $ticket = isset($conn->ticket)&&$conn->ticket ? $conn->ticket : '';
            if($data['ticket'] != $ticket){
                return;
            }
            // 通过workerman,向uid的页面推送数据
            $ret = sendMessageByUid($uid, $buffer);
            // 返回推送结果
            $connection->send($ret ? 'ok' : 'fail');
        }
        return;
    };
    // ## 执行监听 ##
    $inner_text_worker->listen();
    // 进程启动后设置一个每秒运行一次的定时器
    Timer::add(1, function()use($worker){
        $time_now = time();
        foreach($worker->uidConnections as $connection) {
            // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
            if (empty($connection->lastMessageTime)) {
                $connection->lastMessageTime = $time_now;
                continue;
            }
            // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
            if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
                if(isset($connection->uid))
                {
                    // 连接断开时删除映射
                    unset($worker->uidConnections[$connection->uid]);
                }
                $connection->close();
            }
        }
    });
    //每天重启进程
    Timer::add(86400, function()use($worker)
    {
        sc('WebSocket服务进程定时重启任务,执行成功!');
        Worker::stopAll();
    });
};

// 当有客户端发来消息时执行的回调函数
$worker->onMessage = function($connection, $data)
{
    global $worker;
    // 给connection临时设置一个lastMessageTime属性,用来记录上次收到消息的时间
    $connection->lastMessageTime = time();
    // 客户端传递的是json数据
    if (empty($data)) return;
    $message = json_decode($data, true);
    if(empty($message)) return;
    if(isset($message['cmd'])) {
        // 根据类型执行不同的业务
        switch($message['cmd'])
        {
            case 'ping':
                return;
            case 'login':
                return;
            case 'sms':
                return;
            case 'mail':
                return;
            default:
                return;
        }
    }else{
        // 判断当前客户端是否已经验证,即是否设置了uid
        if(isset($connection->uid))
        {
            //上次uid和ticket过期
            if (isset($message['uid']) && ($message['uid']!=$connection->uid)) {
                unset($worker->uidConnections[$connection->uid]);
            }
        }
        if (isset($message['uid']) && $message['uid']) {
            // 没验证的话把第一个包当做uid
            $connection->uid = $message['uid'];
            if (isset($message['ticket'])) {
                //带参数二维码的ticket
                $connection->ticket = $message['ticket'];
            }
            /* 保存uid到connection的映射,这样可以方便的通过uid查找connection,
            * 实现针对特定uid推送数据
            */
            $worker->uidConnections[$connection->uid] = $connection;
            $connection->send($data);
            return;
        } else {
            //不带uid的消息
            # code...
        }
    }
};

// 当有客户端连接断开时
$worker->onClose = function($connection)
{
    global $worker;
    if(isset($connection->uid))
    {
        // 连接断开时删除映射
        unset($worker->uidConnections[$connection->uid]);
    }
};
// 进程关闭时
$worker->onWorkerStop = function($worker)
{
    //通知运维人员
    //sc('WebSocket服务进程退出,如非定时重启,请检查!');
};
// 针对uid推送数据
function sendMessageByUid($uid, $message)
{
    global $worker;
    if(isset($worker->uidConnections[$uid]))
    {
        $connection = $worker->uidConnections[$uid];
        $connection->send($message);
        return true;
    }
    return false;
}

// 如果不是在根目录启动,则运行runAll方法
if(!defined('GLOBAL_START'))
{
    Worker::runAll();
}

前端页面关键代码

<!DOCTYPE html> 
<html> 
<head> 
<meta charset="UTF-8" />
<title>微信公众号模板消息通知Token申请页 - 大卫科技blog www.iyuu.cn</title>
<meta name="keywords" content="大卫科技blog,www.iyuu.cn" />
<meta name="description" content="微信公众号模板消息通知Token申请页" />
<meta name="copyright" content="海南大卫电子科技有限公司" />
<meta name="author" content="大卫" />
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
</head>
<body>
<div id="panel">
    <div id="header">
        <h1>微信公众号模板消息通知<span>Token申请页</span></h1>
        <noscript><h1>你的浏览器不支持 JavaScript,请启用 JavaScript 后访问。</h1></noscript>
        <address>制作 by <a href="http://www.iyuu.cn/">大卫科技blog</a></address>
    </div>
    <div id="token" style="display: none;"></div>
    <div id="qrcode">点击下面的按钮,获取微信二维码!</div>
    <div id="expire" style="display: none;">请尽快使用手机微信扫码,二维码<span id="dd">120</span>秒后过期。</div>    
    <a class="J_scanWeixin">获取微信二维码</a>
</div>
<script type="text/javascript">
var ws,ping_t,qrcode_t,expire_t;
var WEB_URL  = {
    QRCODE_IMG_URL : 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=',
    wshost : 'wss://www.iyuu.cn:2129',        //websocket服务器地址
};
// 连接服务器
function connect() {
    ws = new WebSocket(WEB_URL.wshost);
    ws.onopen = function(e)    {
        console.log(ws);
        console.log("Server onOpen",e);
        ping_t = setInterval(function(){
            ws.send('{"cmd":"ping"}');
            console.log("ping Server");
        }, 30000);
    };
    ws.onmessage = onmessage;
    ws.onclose = function(e) {
        console.log("Server onClose",e.data);
        //关闭定时器
        if (typeof ping_t !="undefined")
        {
            clearInterval(ping_t);
            console.log("clearInterval ping_t");
        }
        if (typeof qrcode_t !="undefined")
        {
            clearInterval(qrcode_t);
            console.log("clearInterval qrcode_t");
        }
    };
    ws.onerror = function(e) {
        connect();
        console.log("Server onError",e.data);
    };
}

function onmessage(e)
{
    var timestamp = new Date().getTime();
    var data = JSON.parse(e.data);    //JSON.parse() 将 JSON字符串转换为对象。
    if (typeof data.cmd != 'undefined')
    {
        switch(data.cmd){
            case 'login':
                window.location='/admin/login.php?token='+data.token;
            break;
            case 'scan':
                console.log('Server Cmd scan',e.data);
            break;
            case 'bind':
                console.log('Server Cmd bind',e.data);
            break;
            default:   //服务器下发其他指令
                console.log('Server Cmd?',e.data);
            break;
        }
    }else{
        if (typeof data.token != 'undefined')
        {
            clearInterval(expire_t);
            clearInterval(qrcode_t);
            $("#token").html("<h3>您的Token是:"+ data.token +"</h3><br /><h3>请求URL是:https://iyuu.cn/"+ data.token +".send</h3>");
            $("#token").show();
            $("#qrcode").hide();
            $("#expire").hide();
            $(".J_scanWeixin").hide();
        }        
    }    
    console.log('收到Server消息',e.data);
}
connect();
//dom载入完毕执行
$(function(){
    //点击按钮,显示二维码
    $('.J_scanWeixin').click(function(){
        if (ws.readyState == 1)
        {
            $.get("/qrcode",function(ret){
                ws.send(ret);    //发送uid
                var data = JSON.parse(ret);
                $("#qrcode").html("<img class='' src='"+ WEB_URL.QRCODE_IMG_URL + escape(data.ticket) +"' width='375' height='375' />");
                $(".J_scanWeixin").hide();    //隐藏获取二维码按钮
                $("#qrcode").show();
                $("#expire").show();    //显示倒计时
                //扫码提示
                qrcode_t = setTimeout(function(){
                    $("#qrcode").hide();                    
                    $("#expire").hide();
                    $(".J_scanWeixin").show();
                }, data.expire_seconds*1000);

                var dd = data.expire_seconds;                
                expire_t = setInterval(function(){
                    if(dd <=1){
                        clearInterval(expire_t);
                    }
                    dd--;                    
                    $("#dd").html("<b>"+ dd +"</b>");
                }, 1000);
            });
        }else{
            $("#qrcode").html("<b>Websocker链接失败,请刷新页面重试!</b>");
            $(".J_scanWeixin").hide();    //隐藏获取二维码按钮
        }        
    });
});
</script>
</body>
</html>

执行方法:

/磁盘/路径/php /路径/start_wss.php start -d
最后修改:2020 年 07 月 12 日 02 : 25 PM
如果觉得我的文章对你有用,请随意赞赏

发表评论