本篇介绍的二维码登录不是微信开发平台的二维码登录,而是利用微信公众号临时二维码扫码事件关注公众号进行登录注册,
浏览器判断扫码状态有两种方式,
第一种是ajax每隔一秒进行轮询,如果用户扫码了则后台给个成功状态
第二种是进入页面后链接websocket等待服务器主动通知,
优缺点分析:
轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
优点:后端程序编写比较容易。
缺点:请求中有大半是无用,浪费带宽和服务器资源。
websocket
优点:在无消息的情况下不会频繁的请求,耗费资源小。
缺点:服务器报纸连接会消耗资源
1.后台开启websocket
这里以tp5集成的workman为例进行设置,跟自己下载的基本相同,其他框架或自己安装的请参考workman官方文档
composer require topthink/think-worker=2.0.*
因为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
生成唯一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;
}
前端页面index.html
ps:每隔30秒向服务器发送一条数据,避免超时,
onmessage里,后台返回token说明登录成功,进行跳转
微信登录
微信登录
扫码
>
关注
>
登录
微信消息处理请看: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收到推送后发送消息给浏览器
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');
}
}