本文主要是介绍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
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
<?php
namespace app\http;use think\worker\Server;
use Workerman\Lib\Timer;
use Workerman\Worker as W;class Worker extends Server
{protected $socket = 'websocket://0.0.0.0:2345';/*** 每个进程启动* @param $worker*/public function onWorkerStart($worker){// 开启一个内部端口,方便内部系统推送数据,Text协议格式 文本+换行符$inner_text_worker = new W('Text://0.0.0.0:5678');$inner_text_worker->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;}}
启动服务器 如下图
php think worker:server
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说明登录成功,进行跳转
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><link rel="stylesheet" type="text/css" href="__CSS__/common.css"><link rel="stylesheet" type="text/css" href="__CSS__/weLogin.css" /><title>微信登录</title>
</head><body>
<div class="login block"><div class="we-login"><p class="login-title">微信登录</p><div class="login-img"><img src="{$ticket}" /></div><div class="login-desc"><p><span>扫码</span><span>></span><span>关注</span><span>></span><span>登录</span></p></div></div>
</div><script>var wsServer = 'ws://127.0.0.1:2345';var websocket = new WebSocket(wsServer);websocket.onopen = function (evt) {console.log("Connected to WebSocket server.");//ws.send("Hello WebSockets!");websocket.send("{$uid}");//设置心跳,避免服务器断开setInterval(function () {websocket.send("{$uid}");}, 30000)};websocket.onclose = function (evt) {console.log("Disconnected");};websocket.onmessage = function (evt) {console.log('Retrieved data from server: ' + evt.data);var res = JSON.parse(evt.data);if(res.token !== '' && res.token !== undefined){window.location='/index/login/login?token='+res.token;}};websocket.onerror = function (evt, e) {console.log('Error occured: ' + evt.data);};
</script>
</body></html>
4.用户扫码,后台进行注册与通知
微信消息处理请看:https://blog.csdn.net/flysnownet/article/details/90239582
<?phpnamespace app\api\controller;use app\common\model\Option;
use EasyWeChat\Kernel\Messages\News;
use EasyWeChat\Kernel\Messages\NewsItem;
use think\Controller;
use think\facade\Env;
use think\Request;
use EasyWeChat\Factory;
use think\facade\Session;
use message\MessageHandler;/**微信消息处理* Class Message* @package app\api\controller*/
class Message extends Controller
{public function __construct(Request $request){global $_W;$_W['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',],];//微信首次接入验证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;}}}
<?phpnamespace message;use app\common\model\Users;
use EasyWeChat\Kernel\Messages\Link;
use EasyWeChat\Kernel\Messages\Message;
use EasyWeChat\Kernel\Messages\Text;
use EasyWeChat\Kernel\Messages\News;
use EasyWeChat\Kernel\Messages\NewsItem;
use think\Exception;
use think\facade\Cache;
use think\facade\Log;
use EasyWeChat\Factory;
use Token\Token;
/**微信消息* Class MessageHandler* @package message*/
class MessageHandler
{/** 消息对象*/private $message;private $user_model;public function __construct($message){$this->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');}}
这篇关于tp5使用workerman实现微信扫码登录,微信公众号关注登录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!