标签搜索

workerman+websocket实现公众号扫码关注登录

basil
2024-01-02 / 21 阅读

背景

为了方面用户登录系统以及引导用户关注公众号,需要做一个微信扫码登录的功能

实现逻辑说明

  • 用到的技术或框架
    websocket thinkphp5.1 workerman3.5.0 overtrue/wechat
  • 时序图 微信扫码时序图
  • 说明
    1.用户访问登录页面后,web端请求获取二维码生成接口
    2.登录服务接收到二维码生成请求后,生成loginId,然后携带loginId请求微信二维码生成接口获取二维码URL,最后将二维码URL以及loginId返回给web端
    3.web端获取到二维码URL和loginId后,显示二维码并携带loginId与登录服务建立websocket连接
    4.用户扫描后关注并跳转到微信公众号,同时触发微信的事件消息回调,将用户openid返回给登录服务
    5.登录服务执行登录逻辑后,将登录结果通过websocket返回到web端,web端再跳转到登录成功或账号绑定界面

关键代码

  • 获取微信二维码
public function getLoginQrCode()
    {
        $app = Factory::officialAccount(Config::get('official_account.'));
        $loginId = uniqid();
        $params = [
            'scene' => 'login',
            'login_id' => $loginId,
            'staff_id' => Request::param('staff_id') ?? ''
        ];
        $content = $app->qrcode->temporary(http_build_query($params));
        Cache::store('redis')->set($loginId, 'true', 120);
        $content['login_id'] = $params['login_id'];
        isset($content['ticket']) && $content['picture_url'] = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' . $content['ticket'];

        throw new SuccessException(['content' => $content]);
    }
  • workerman实现websocket协议,并创建一个内部的http协议服务接收消息
<?php

namespace app\service\workerman;
use app\common\MyLog;
use think\facade\Cache;
use think\facade\Config;
use think\facade\Env;
use think\worker\Server;
use Workerman\Connection\TcpConnection;
use Workerman\Lib\Timer;
use Workerman\Worker;
use app\facade\model\InstantMessageLog;

class WebsocketServer extends Server
{

    protected $socket = '';

    protected $context = [
        'ssl' => [
            // 请使用绝对路径
            'local_cert'        => '', // pem文件也可以是crt文件
            'local_pk'          => '', // key文件
            'verify_peer'       => false,
            'allow_self_signed' => true, //如果是自签名证书需要开启此选项
        ]
    ];

    protected $option = [
        'name' => 'official_account_login',
        'count' => 1, // 必须是单进程,因为要通过connection的ID来区分客户端连接
        'transport' => 'ssl'
    ];

    protected $connections = [];

    public function __construct()
    {
        $this->socket = Config::get('server.official_account_scan_login_server');
        $this->context['ssl']['local_cert'] = Env::get('wss.pem');
        $this->context['ssl']['local_pk'] = Env::get('wss.key');
        parent::__construct();
    }

    public function onWorkerStart(Worker $worker)
    {
        $innerTextWorker = new Worker(Config::get('server.inner_http_server'));
        $innerTextWorker->onMessage = function(TcpConnection $connection, $request)
        {
            MyLog::infoLog('http服务接收到的数据', $request, 'workerman');
            $loginId = $request['request']['login_id'] ?? '';
            if (empty($loginId))
            {
                $connection->send(json_encode(['code' => 1, 'msg' => '缺少login_id'], JSON_UNESCAPED_UNICODE));
                return false;
            }
            if (isset($this->connections[$loginId])) 
            {
                $result = json_encode(['code' => 0, 'msg' => '消息推送成功', 'data' => $request['request']], JSON_UNESCAPED_UNICODE);
                $this->connections[$loginId]->send($result);
                $connection->send(json_encode(['code' => 0, 'msg' => '接收成功'], JSON_UNESCAPED_UNICODE));
                return true;
            }
            $connection->send(json_encode(['code' => 0, 'msg' => '发送成功'], JSON_UNESCAPED_UNICODE));
            return true;
        };
        $innerTextWorker->listen();

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

    public function onMessage(TcpConnection $connection, $data)
    {
        $data = json_decode($data, true);
        if (isset($data['message']) && $data['message'] == 'heart_check') 
        {
            $connection->send(json_encode(['code' => 0, 'msg' => '正常连接中', 'data' => ['login_id' => $data['login_id'], 'link_status' => 'online']], JSON_UNESCAPED_UNICODE));
            return true;
        }
        if (isset($data['response']) && $data['response'] == 'ok' && isset($data['staff_id']) && $data['staff_id'] != '') 
        {
            InstantMessageLog::updateByWhere(['send_status'=>2,'update_time'=>time()], ['user_staff_id' => $data['staff_id'], 'login_id' => $data['login_id']]);
            $connection->send(json_encode(['code' => 0, 'msg' => '完成应答', 'data' => ['login_id' => $data['login_id'], 'link_status' => 'online']], JSON_UNESCAPED_UNICODE));
            Cache::store('redis')->rm('user_staff_'.$data['staff_id']);
            return true;
        }
        if (!isset($data['login_id'])||empty($data['login_id'])) 
        {
            $connection->send(json_encode(['code' => 1, 'msg' => 'missing login_id']));
            return false;
        }
        if (!Cache::store('redis')->get($data['login_id']) && !isset($data['staff_id'])) 
        {
            $connection->send(json_encode(['code' => 1, 'msg' => 'login_id expired'], JSON_UNESCAPED_UNICODE));
            return false;
        }
        if (isset($data['staff_id']) && $data['staff_id'] != '') 
        {
            Cache::store('redis')->set('user_staff_'.$data['staff_id'], $data['login_id']);
        }
        $this->connections[$data['login_id']] = $connection;
        $connection->send(json_encode(['code' => 0, 'msg' => '连接成功', 'data' => ['login_id' => $data['login_id']]], JSON_UNESCAPED_UNICODE));
        return true;
    }

    public function onConnect($connection)
    {

    }
    
}
  • 微信扫描/关注事件回调处理
 /**
     * 扫码登录操作
     * @param  Application $app     [description]
     * @param  [type]      $message [description]
     * @return [type]               [description]
     */
    public static function handleLogin(Application $app, $message)
    {
        $user = $app->user->get($message['FromUserName']);
        MyLog::infoLog('user', $user, 'wechat');
        $oauthUsers = Oauth::where('apiname', 'wechat_official_account')
            ->where('openid', $user['openid'])
            ->findOrEmpty()
            ->toArray();
        $content = [
            'error_code' => 0,
            'msg'        => '未绑定账号',
            'type'       => 'no_binding',
            'oauth_type' => 'wechat_official_account',
            'openid'     => $user['openid'],
            'token'      => '',
            'login_id'   => $message['login_id']
        ];
        if (!empty($oauthUsers) && $message['staff_id'] != '') 
        {
            $content['error_code'] = 1;
            $content['msg'] = '该微信号已绑定其它账号,请更换微信号扫码';
            $content['type'] = 'bound_other';
            Cache::store('redis')->set($message['login_id'], $content, 120);
            $result = (new Client())->post(Config::get('server.inner_http_server_url'), [
                'form_params' => $content]);
            MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat');
            return '账号绑定失败';
        }
        if (empty($oauthUsers) || empty($oauthUsers['staff_id']) || empty($oauthUsers['user_id']))
        {
            if ($message['staff_id'] != '') 
            {
                $userStaff = UserStaff::where(['staff_id'=>$message['staff_id'],'is_delete'=>0])->find();
                $returnStr = '';
                if (!$userStaff) {
                    $content['msg'] = '账号不存在,绑定失败';
                    $content['type'] = 'binding_fail';
                    $returnStr = '账号绑定失败';
                } else {
                    (new UserService())->bindWechatOfficialAccount(['user_id'=>$userStaff->user_id,'staff_id'=>$userStaff->staff_id,'openid'=>$user['openid']]);
                    $content['msg'] = '账号绑定成功';
                    $content['type'] = 'bound';
                    $returnStr = '账号已完成绑定';
                }
                Cache::store('redis')->set($message['login_id'], $content, 120);
                $result = (new Client())->post(Config::get('server.inner_http_server_url'), [
                    'form_params' => $content]);
                MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat');
                return $returnStr;
            } else {
                Cache::store('redis')->set($message['login_id'], $content, 120);
                $result = (new Client())->post(Config::get('server.inner_http_server_url'), [
                    'form_params' => $content]);
                MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat');
                return '感谢您的关注,为了更好的为您提供服务,请绑定已有账号或注册平台账号,只需绑定或注册一次,以后扫码直接登录,且跟您相关的订单进度也会通过公众号推送给您,方便您及时查看。';
            }
        }
        $field = 'staff_id,user_id,password,salt,staff_name,name,staff_face,department_id,role_id,state,is_company_staff';
        $res = UserSv::doUserLogin([], ['staff_id' => $oauthUsers['staff_id']], $field, 'wechat_official_account');
        $content['type'] = 'bound';
        if(!$res['status'])
        {
            $content['error_code'] = 1;
            $content['msg'] = '登录失败,请联系客服';
            Cache::store('redis')->set($message['login_id'], $content, 120);
            $result = (new Client())->post(Config::get('server.inner_http_server_url'), [
                'form_params' => $content]);
            MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat');
            return 'Hi,你来啦~ 已登录成功。';
        }
        $content['msg'] = '登录成功';
        $content['token'] = $res['data']['token'] ?? '';
        $content['user_info'] = $res['data'];
        Cache::store('redis')->set($message['login_id'], $content, 120);
        $result = (new Client())->post(Config::get('server.inner_http_server_url'), [
            'form_params' => $content]);
        MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat');
        return 'Hi,你来啦~ 已登录成功。';
    }
1