tp5使用workerman实现微信扫码登录,微信公众号关注登录
基于公众号实现微信扫码登录,扫码登录同时关注公众号
引言
本篇介绍的二维码登录不是微信开发平台的二维码登录,而是利用微信公众号临时二维码扫码事件关注公众号进行登录注册,登录的同时关注了公众号,一举两得
浏览器判断扫码状态有两种方式,
第一种是ajax每隔一秒进行轮询,如果用户扫码了则后台给个成功状态
第二种是进入页面后链接websocket等待服务器主动通知,
优缺点分析:
轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
优点:后端程序编写比较容易。
缺点:请求中有大半是无用,浪费带宽和服务器资源。
websocket
优点:在无消息的情况下不会频繁的请求,耗费资源小。
缺点:服务器报纸连接会消耗资源
视频教程
https://study.163.com/provider/480000002210444/index.htm?share=2&shareId=480000002210444
简单流程
- 后台监听websocket
- 浏览器请求登录页--
- 后台生成微信零时二维码(带参数uid)--
- 浏览器显示二维码并连接websocket---
- 用户扫码
- 微信服务器通知业务服务器
- 服务器获取到用户信息进行注册并生成token通过websocket通知浏览器登陆成功
- 浏览器携带token跳转到登陆成功页面
实现
1.后台开启websocket
这里以tp5集成的workman为例进行设置,跟自己下载的基本相同,其他框架或自己安装的请参考workman官方文档
1.安装workerman
- tp5文档:https://www.kancloud.cn/manual/thinkphp5_1/354134
// 扩展自身需要的配置'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;// 判断当前客户端是否已经验证,即是否设置了uidif(!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;}}
启动服务器 如下图
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 "登录成功";elsereturn '登录异常';}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');}}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
