背景
为了方面用户登录系统以及引导用户关注公众号,需要做一个微信扫码登录的功能
实现逻辑说明
- 用到的技术或框架
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,你来啦~ 已登录成功。';
}