tp5使用workerman实现微信扫码登录,微信公众号关注登录

引言

本篇介绍的二维码登录不是微信开发平台的二维码登录,而是利用微信公众号临时二维码扫码事件关注公众号进行登录注册,

浏览器判断扫码状态有两种方式,

第一种是ajax每隔一秒进行轮询,如果用户扫码了则后台给个成功状态

第二种是进入页面后链接websocket等待服务器主动通知,

优缺点分析:

轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。 
优点:后端程序编写比较容易。 
缺点:请求中有大半是无用,浪费带宽和服务器资源。 

websocket

优点:在无消息的情况下不会频繁的请求,耗费资源小。 
缺点:服务器报纸连接会消耗资源

简单流程

  1. 后台监听websocket
  2. 浏览器请求登录页--
  3. 后台生成微信零时二维码(带参数uid)--
  4. 浏览器显示二维码并连接websocket---
  5. 用户扫码
  6. 微信服务器通知业务服务器
  7. 服务器获取到用户信息进行注册并生成token通过websocket通知浏览器登陆成功
  8. 浏览器携带token跳转到登陆成功页面

实现

1.后台开启websocket

这里以tp5集成的workman为例进行设置,跟自己下载的基本相同,其他框架或自己安装的请参考workman官方文档

1.安装workerman     

  1. tp5文档:https://www.kancloud.cn/manual/thinkphp5_1/354134
composer require topthink/think-worker=2.0.*

 2.配置 config/worker_server.php

因为tp5配置方式不支持自定义函数(或许是我没找到),这里使用自定义类作为Worker服务入口文件类,在配置文件里填写服务类名即可开启,

// 扩展自身需要的配置
    'protocol'       => 'websocket', // 协议 支持 tcp udp unix http websocket text
    'host'           => '0.0.0.0', // 监听地址
    'port'           => 2345, // 监听端口
    'socket'         => '', // 完整监听地址
    'context'        => [], // socket 上下文选项
    'worker_class'   => 'app\http\Worker', // 自定义Workerman服务类名 支持数组定义多个服务

    // 支持workerman的所有配置参数
    'name'           => 'thinkphp',
    'count'          => 4,
    'daemonize'      => false,
    'pidFile'        => Env::get('runtime_path') . 'worker.pid',

Worker.php

 

 

ps:这里把浏览器第一次发来的信息作为uid保存,为了后面主动给浏览器发送消息,

必须设置心跳,客户端每隔一定时间向服务器发送消息,不然超过超时时间服务器将断开连接,避免客户端异常时浪费资源

$inner_text_worker内部服务是为了后面扫码成功后接收通知,然后向特定客户端发送消息用

详细请参考:https://wenda.workerman.net/question/508

onMessage = function($connection, $buffer)
        {
            // $data数组格式,里面有uid,表示向那个uid的页面推送数据
            $data = json_decode($buffer, true);
            $uid = $data['uid'];
            // 通过workerman,向uid的页面推送数据
            $ret = $this->sendMessageByUid($uid, $buffer);
            if ($ret)
            {
                $msg['error'] = 0;
                $msg['msg'] = 'ok';
            }else{
                $msg['error'] = 1;
                $msg['msg'] = '异常';
            }
            $msg = json_encode($msg);

            // 返回推送结果
            $connection->send($msg);
        };
        $inner_text_worker->listen();

        // 心跳间隔55秒
        define('HEARTBEAT_TIME', 55);
        Timer::add(1, function()use($worker){
            $time_now = time();
            foreach($worker->connections as $connection) {
                // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
                if (empty($connection->lastMessageTime)) {
                    $connection->lastMessageTime = $time_now;
                    continue;
                }
                // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
                if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
                    $connection->close();
                }
            }
        });
    }

    public function onMessage($connection,$data)
    {
        global $worker;
        // 判断当前客户端是否已经验证,即是否设置了uid
        if(!isset($connection->uid))
        {
            // 没验证的话把第一个包当做uid(这里为了方便演示,没做真正的验证)
            $connection->uid = $data;
            /* 保存uid到connection的映射,这样可以方便的通过uid查找connection,
             * 实现针对特定uid推送数据
             */
            $worker->uidConnections[$connection->uid] = $connection;
            //return $connection->send('login success, your uid is ' . $connection->uid);
        }
        // 给connection临时设置一个lastMessageTime属性,用来记录上次收到消息的时间
        $connection->lastMessageTime = time();
        //$connection->send('receive success');
        echo $data;
        echo "\n";
    }

    public function onConnect($connection)
    {

    }

    /**
     * 当连接断开时触发的回调函数
     * @param $connection
     */
    public function onClose($connection)
    {
        global $worker;
        if(isset($connection->uid))
        {
            // 连接断开时删除映射
            unset($worker->uidConnections[$connection->uid]);
        }
    }
    /**
     * 当客户端的连接上发生错误时触发
     * @param $connection
     * @param $code
     * @param $msg
     */
    public function onError($connection, $code, $msg)
    {
        echo "error $code $msg\n";
    }


    // 针对uid推送数据
    public function sendMessageByUid($uid, $message)
    {
        global $worker;
        if(isset($worker->uidConnections[$uid]))
        {
            $connection = $worker->uidConnections[$uid];
            $connection->send($message);
            return true;
        }
        return false;
    }


}

 启动服务器  如下图

php think worker:server

 tp5使用workerman实现微信扫码登录,微信公众号关注登录_第1张图片

2.生成微信临时二维码  (这里使用easywechat,详细请看我其他相关博文)

生成唯一uid,并把uid当做二维码的场景值生成二维码

public function index()
    {
        $uid = make_uid();
        //$ticket = Cache::store('redis')->get('login_ticket');
        $ticket = $this->loginCode($uid);
        Cache::store('redis')->set('login'.$uid,1,600);
        $data = array(
            'uid' => $uid,
            'ticket' => $ticket
        );
        return view('index',$data);
    }



    public function loginCode($uid)
    {
        $config = [
            'app_id' => Option::get('app_id')->option_value,
            'secret' => Option::get('app_secret')->option_value,
            'token' => Option::get('app_token')->option_value,
            'response_type' => 'array',
            'log' => [
                'level' => 'debug',
                'file' => Env::get('runtime_path').'/wechat.log',
            ],
            'oauth' => [
                'scopes'   => ['snsapi_userinfo'],
                'callback' => '/index/login/oauth_callback',
            ],
        ];
        $app = Factory::officialAccount($config);
        $result = $app->qrcode->temporary($uid, 600);

        $url = $app->qrcode->url($result['ticket']);
        return $url;

    }

/**
 * Notes:生成UID
 * @auther: xxf
 * Date: 2019/7/17
 * Time: 17:28
 * @return string
 */
function make_uid()
{
    @date_default_timezone_set("PRC");
    //号码主体(YYYYMMDDHHIISSNNNNNNNN)
    $order_id_main = date('YmdHis') . rand(10000000,99999999);
    $order_id_len = strlen($order_id_main);
    $order_id_sum = 0;
    for($i=0; $i<$order_id_len; $i++){
        $order_id_sum += (int)(substr($order_id_main,$i,1));
    }
    //唯一号码(YYYYMMDDHHIISSNNNNNNNNCC)
    $uid = $order_id_main . str_pad((100 - $order_id_sum % 100) % 100,2,'0',STR_PAD_LEFT);
    return $uid;
}

 

3.浏览器显示二维码并向后台发送uid

前端页面index.html

 

ps:每隔30秒向服务器发送一条数据,避免超时,

onmessage里,后台返回token说明登录成功,进行跳转




    
    
    
    
    
    微信登录








4.用户扫码,后台进行注册与通知

微信消息处理请看:https://blog.csdn.net/flysnownet/article/details/90239582

 

 Option::get('app_id')->option_value,
            'secret' => Option::get('app_secret')->option_value,
            'token' => Option::get('app_token')->option_value,
            'response_type' => 'array',
            'log' => [
                'level' => 'debug',
                'file' => Env::get('runtime_path').'/wechat.log',
            ],
            'oauth' => [
                'scopes'   => ['snsapi_userinfo'],
                'callback' => '/index/login/oauth_callback',
            ],
        ];

        //微信首次接入验证
        if (!empty($_GET['echostr']) && $this->checkSignature($_W['config']['token'])) {
            header('content-type:text');
            echo $_GET['echostr'];
            exit;
        }
    }


    public function index(Request $request)
    {
        global $_W;
        $app = Factory::officialAccount($_W['config']);

        $app->server->push(function ($message) {
            // $message['FromUserName'] // 用户的 openid
            // $message['MsgType'] // 消息类型:event, text....

            $handler = new MessageHandler($message);
            switch ($message['MsgType']) {
                case 'event':
                    //return '收到事件消息';
                    return $handler->eventHandler($message['FromUserName']);
                    break;
                case 'text':
                    //return '收到文字消息';
                    return $handler->textHandler($message['FromUserName']);
                    break;
                case 'image':
                    return '收到图片消息';
                    break;
                case 'voice':
                    return '收到语音消息';
                    break;
                case 'video':
                    return '收到视频消息';
                    break;
                case 'location':
                    //return '收到坐标消息';
                    return $handler->test();
                    break;
                case 'link':
                    return '收到链接消息';
                    break;
                case 'file':
                    return '收到文件消息';
                // ... 其它消息
                default:
                    return '收到其它消息';
                    break;
            }
        });

        $response = $app->server->serve();
        $response->send();
        //return $response;

    }

    /*
     * 接入验签
     */
    private function checkSignature($token)
    {
        $signature = $_GET["signature"];
        $timestamp = $_GET["timestamp"];
        $nonce = $_GET["nonce"];
        $tmpArr = array($token, $timestamp, $nonce);
        sort($tmpArr);
        $tmpStr = implode($tmpArr);
        $tmpStr = sha1($tmpStr);
        if ($tmpStr == $signature) {
            return true;
        } else {
            return false;
        }
    }



}


 

message = $message;
        $this->user_model = new Users();
    }

    /*
     * 事件响应函数
     */
    public function eventHandler()
    {
        // $message['FromUserName'] // 用户的 openid
        // $message['MsgType'] // 消息类型:event, text....
        global $_W;
        switch ($this->message['Event']) {
            //关注事件
            case 'subscribe':
                if (!empty($this->message['EventKey'])) {
                    $uid = substr($this->message['EventKey'],8);
                    $res = $this->loginEvent($uid);
                    return $res;
                }
                return '欢迎关注';
                break;
            //取消关注事件
            case 'unsubscribe':
                return $this->unSubscribe();
                break;
            //点击事件
            case 'CLICK':
                return '点击';
                break;
            //扫描事件
            case 'SCAN':
                $res = $this->loginEvent($this->message['EventKey']);
                return $res;
                //return '取关';
                break;
            default:
                return '收到其它消息';
                break;
        }
    }



    public function LoginEvent($uid)
    {
        global $_W;
        //注册
        $openid = $this->message['FromUserName'];
        $user_id = $this->addMember($openid);

        $jwtToken = new Token();
        $tokenData = array(
            'user_id' => $user_id,
        );
        $token = $jwtToken->createToken($tokenData, 86400)['token'];
        $time_out = Cache::store('redis')->get('login'.$uid);
        if (empty($time_out))
            return "二维码过期,请重新登陆";
        $data = array(
            'uid' => $uid,
            'token' => $token
        );
        $res = $this->sendSocket($data);
        if($res)
            return "登录成功";
        else
            return '登录异常';



    }



    private function addMember($openid)
    {
        global $_W;
        $user_info = $this->user_model->getInfoByOpenId($openid);
        if (empty($user_info))
        {
            $app = Factory::officialAccount($_W['config']);
            $user_detail = $app->user->get($openid);
            $data = array(
                'nickname' => $user_detail['nickname'] ?? '',
                'openid' => $openid,
                'gender' => $user_detail['sex'] ?? 0,
                'avatar' => $user_detail['headimgurl'] ?? '',
            );
            $user_id = $this->user_model->addUser($data);
        }else{
            $user_id = $user_info->id;
        }
        return $user_id;

    }



    /**
     * Notes:取消关注事件
     * Date: 2019/6/19
     * Time: 14:11
     * @return bool
     */
    private function unSubscribe()
    {
        global $_W;
        $member_model = new Members();
        if ($_W['user']['id'] > 0)
        $member_model->unSubscribe($_W['user']['id']);
        return true;

    }

    public function sendSocket($data)
    {
        try{
            // 建立socket连接到内部推送端口
            $client = stream_socket_client('tcp://127.0.0.1:5678', $errno, $errmsg, 1);
            // 推送的数据,包含uid字段,表示是给这个uid推送
            //$data = array('uid'=>'201907181703404867264713', 'percent'=>'88%');
            // 发送数据,注意5678端口是Text协议的端口,Text协议需要在数据末尾加上换行符
            fwrite($client, json_encode($data)."\n");

            // 读取推送结果
            $res = fread($client, 8192);
            fclose($client);
            $res = json_decode($res,true);
            if($res['error'] == 0){
                return true;
            }else{
                return false;
            }

        }catch (\Exception $e)
        {
            return $e->getMessage();
        }

    }





}

ps:

sendSocket() :注册成功后给内部服务推送消息,告知给$uid的客户端发送token,workerman收到推送后发送消息给浏览器

5.浏览器收到token,进行跳转

6.登陆成功

public function login(Request $request)
    {

        $token = $request->param('token', 0);
        if (!empty($token)) {
            $jwtToken = new Token();
            $checkToken = $jwtToken->checkToken($token);
            $data = (array)$checkToken['data']['data'];
            $uid = $data['uid'] ?? 0;
            $user_id = $data['user_id'] ?? 0;
            Session::set('user_id', $user_id);
            $this->error('登陆成功', 'index/index/index');
        }
    }

 

你可能感兴趣的:(微信公众号,workerman,php,thinkPHP)