目录
需求来源
实现思路
1、进入登录页面,生成微信公众号的临时二维码;
2、用户通过微信扫一扫二维码;
3、登录页面定时查询扫码结果;
代码实现(基于Laravel框架前后端混合)
HTML
PHP-路由文件
PHP-控制器
PHP-模型
数据表(请根据实际业务定义表结构)
感谢阅读,欢迎交流
业务系统的PC端增加微信二维码扫码登录功能
传入二维码的场景值(开发者接收微信服务器推送的数据包用以区分业务场景)、过期时间(用户无操作时多久刷新一次二维码)
用户扫描二维码,微信服务器推送扫描事件的xml数据包给开发者服务器,开发者通过xml数据包处理用户的扫码登录逻辑,更新用户成功登录的标识;
定义轮询方法,通过生成二维码的场景值定时查询用户扫码的结果,成功则处理登录成功的逻辑,反之继续轮询,本文自动更新二维码(可不做自动刷新二维码,二维码到期时停止轮询);
{{$title}}
@include('plugins.izi-modal')
@include('plugins.loading')
@if(!\Session::get('user'))
打开微信扫一扫
@else
用户已登录
@endif
'wechat/'], function (Router $router) {
// 接收微信推送信息,完成验证消息真实性
$router->get('receive_push', "WechatController@receivePush");
// 接收微信推送信息,完成接收普通消息并回复
$router->post('receive_push', "WechatController@receivePush");
// 对话服务-基础支持-获取access_token
$router->get('get_access_token', 'WechatController@getAccessToken');
// 微信公众号集成功能
$router->group(['prefix' => '/func/'], function(Router $router) {
// 微信公众号集成功能--用户扫码登录
$router->group(['prefix' => 'login/'], function(Router $router) {
// 微信公众号集成功能--用户扫码登录--微信登录页面
$router->get('wx_login_view', 'WechatController@wxLoginView');
// 微信公众号集成功能--用户扫码登录--创建登录二维码
$router->get('create_login_qrcode', 'WechatController@createLoginQrcode');
// 微信公众号集成功能--用户扫码登录--查询扫描二维码状态
$router->get('check_login_status', 'WechatController@checkLoginStatus');
// 微信公众号集成功能--用户扫码登录--微信退出登录
$router->get('wx_logout', 'WechatController@wxLogout');
});
});
});
getUri(), "wechat");
try {
switch ($_SERVER["REQUEST_METHOD"]) {
case "GET":
file_log("验证消息真实性" . $request->getUri(), "wechat");
$params = $request->all();
$params['token'] = config("wechat_develop.Token");
// 校验签名是否正确
$isMatch = $this->verifySignature($params);
if (!$isMatch) {
exit(json_encode(["errcode" => -40001, "errmsg" => "signature sha1 error"], JSON_UNESCAPED_UNICODE));
}
echo $params["echostr"];
exit;
break;
case "POST":
file_log("接收公众号发来的信息" . $request->getUri(), "wechat");
// 接收微信公众号传送的xml信息包
$this->postXml = isset($GLOBALS["HTTP_RAW_POST_DATA"]) ? $GLOBALS["HTTP_RAW_POST_DATA"] : file_get_contents("php://input");
file_log("公众号发来的xml数据包" . $this->postXml, "wechat");
// xml转换成array
$postArray = json_decode(json_encode(simplexml_load_string($this->postXml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
// 按照实际业务处理不同类型的信息,并返回xml包
$this->receive = $postArray;
@ob_clean();
echo $this->reply();
exit;
break;
default:
file_log("【非验证微信服务器信息】或【非微信服务器发送的信息】" . $request->getUri(), "wechat");
exit;
}
} catch (\Exception $e) {
exception_file_log($e, 'wechat');
exit($e->getMessage() . $e->getTraceAsString());
}
}
private function verifySignature($getParam)
{
$tmpArr = array($getParam["token"], $getParam["timestamp"], $getParam["nonce"]);
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
return $tmpStr == $getParam['signature'];
}
private function reply()
{
$this->message = [
"MsgType" => $this->receive["MsgType"],
"CreateTime" => time(),
"ToUserName" => $this->receive["FromUserName"],
"FromUserName" => $this->receive["ToUserName"],
];
switch ($this->receive["MsgType"]) {
case "event":
$this->event();
break;
default:
return $this->postXml;
break;
}
$replyXml = self::arr2xml($this->message);
file_log("开发者发送的信息:" . $replyXml, "wechat");
return $replyXml;
}
public static function arr2xml($data)
{
return "" . self::_arr2xml($data) . " ";
}
private static function _arr2xml($data, $xmlContent = '')
{
foreach ($data as $key => $val) {
is_numeric($key) && $key = 'item';
$xmlContent .= "<{$key}>";
if (is_array($val) || is_object($val)) {
$xmlContent .= self::_arr2xml($val);
} elseif (is_string($val)) {
$xmlContent .= '';
} else {
$xmlContent .= $val;
}
$xmlContent .= "{$key}>";
}
return $xmlContent;
}
# event类型
private function event()
{
$this->message["MsgType"] = "text";
$this->message["Content"] = "遇到未知推送事件";
file_log("推送事件名称:" . strtolower($this->receive["Event"]), "wechat");
switch (strtolower($this->receive["Event"])) {
case "subscribe":
$this->message["MsgType"] = "text";
$this->message["Content"] = "关注公众号触发,暂无做其他处理";
$this->subscribe();
break;
case "scan":
$this->message["MsgType"] = "text";
$this->message["Content"] = "扫码二维码触发,二维码附带的场景值:{$this->receive["EventKey"]},Ticket:{$this->receive["Ticket"]}";
$this->scan();
break;
default:
break;
}
WechatPushRecord::create($this->message);
}
# 关注公众号逻辑
private function subscribe()
{
// 判断数据库是否已存在用户--实际业务
if (!$user = WechatUser::get_one(["where" => [["openid", '=', $this->message['ToUserName']]]])) {
// 拉取用户信息并存库
$userInfo = $this->user_info(['openid' => $this->message['ToUserName'], 'lang' => 'zh_CN']);
file_log(json_encode($userInfo,JSON_UNESCAPED_UNICODE),'wechat');
check_result_issuccess($user = WechatUser::saveSubscribeUser($userInfo), "{$this->message['ToUserName']}保存微信用户信息失败", false, WechatEnum::Exception);
} else {
// 更新用户信息--暂时只在保存时提交数据,不做更新用户信息操作
}
// 判断是否存在事件键值,格式:qrscene_login-xxxxxxxx
if (isset($this->receive['EventKey']) && $this->receive['EventKey']) {
$eventKey = explode('_', $this->receive['EventKey']);
$scene = $eventKey[0];
switch ($scene) {
case 'qrscene':
// 扫码登录
if (isset($this->receive['Ticket']) && $this->receive['Ticket']) {
// 更新当前用户的扫码登录事件键值,表示用户已扫码登录成功
WechatUser::updateLoginStatus($this->message['ToUserName'], $eventKey[1]);
$this->message['Content'] = "用户扫码登录成功";
}
break;
}
}
}
# 扫描二维码逻辑
private function scan()
{
// 定制二维码的场景值时,通过符号“-”进行分割,如login-123456,login为二维码的分类场景值
if (isset($this->receive['EventKey']) && $this->receive['EventKey']) {
$scene = explode('-', $this->receive['EventKey'])[0];
switch ($scene) {
case 'login': // 扫码登录
// 更新当前用户的扫码登录事件键值,表示用户已扫码登录成功
WechatUser::updateLoginStatus($this->message['ToUserName'],$this->receive['EventKey']);
$this->message['Content'] = "用户扫码登录成功";
break;
default:
$this->message['Content'] = "当前二维码场景未定义,请尽快接入";
break;
}
}
}
/**
* todo 微信公众号集成功能--用户扫码登录--微信登录页面
*/
public function wxLoginView()
{
$user = \Session::get('user');
$title = "用户扫码登录";
return view("wechat.wx-login.wxLoginView", compact("title", "user"));
}
/**
* todo 微信公众号集成功能--用户扫码登录--创建登录二维码
*/
public function createLoginQrcode(Request $request)
{
try {
$eventKey = "login-". uniqid('');
$expireSeconds = 60 * 20;
$ticketAndUrl = $this->qrcode_create([
"expire_seconds" => $expireSeconds,
"scene" => $eventKey // 可选择:字符串、整型
]);
if (isset($ticketAndUrl['ticket'])) {
$qrcodeUrl = $this->show_qrcode($ticketAndUrl);
}
} catch (\Exception $e) {
exception_file_log($e, "wechat");
exit("【createLoginQrcode】创建登录二维码失败,请到wechat日志文件查看详情");
}
return return_info(200, '获取二维码链接成功', ['url' => $qrcodeUrl, 'scene' => $eventKey, 'expireTime' => time() + $expireSeconds]);
}
/**
* todo 微信公众号集成功能--用户扫码登录--查询扫描二维码状态
*/
public function checkLoginStatus(Request $request)
{
if (\Session::get('user')) return return_info(202, '用户已登录');
try {
if (is_null($eventKey = $request->eventKey)) return return_info(500, '请先传入必要参数-eventKey');
if (is_null($expireTime = $request->expireTime)) return return_info(500, '请先传入必要参数-expireTime');
if ($expireTime < time()) return return_info(201, '二维码已过期');
// 查询扫码登录的事件键值是否已更新到用户的信息中
$checkLoginStatus = WechatUser::checkLoginStatus($eventKey);
// 处理扫码成功的逻辑
if ($checkLoginStatus) {
# 代码...
\Session::put('user', $eventKey);
\Session::save();
}
} catch (\Exception $e) {
exception_file_log($e, 'wechat');
exit("【checkLoginStatus】查询扫描二维码状态失败,请到wechat日志文件查看详情");
}
return return_info(200, '查询扫描状态成功', ['result' => $checkLoginStatus ? 1 : 0]);
}
/**
* todo 微信公众号集成功能--用户扫码登录--微信退出登录
*/
public function wxLogout(Request $request)
{
try {
if (\Session::get('user')) {
\Session::put('user', null);
\Session::save();
}
} catch (\Exception $e) {
exception_file_log($e, 'wechat');
exit("【wxLogout】微信退出登录失败,请到wechat日志文件查看详情");
}
return return_info(200, '微信退出登录成功');
}
/**
* todo 创建二维码ticket
*/
public function qrcode_create($data)
{
$url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={$this->getAccessToken()}";
// 处理action_info
if (is_integer($data["scene"])) {
$data["action_info"]["scene"] = ["scene_id" => $data["scene"]];
} else {
$data["action_info"]["scene"] = ["scene_str" => $data["scene"]];
}
// 处理action_name
if (isset($data["expire_seconds"]) && $data["expire_seconds"] > 0) {
$data["action_name"] = is_integer($data["scene"]) ? "QR_SCENE" : "QR_STR_SCENE";
} else {
$data["action_name"] = is_integer($data["scene"]) ? "QR_LIMIT_SCENE" : "QR_LIMIT_STR_SCENE";
}
return $this->post_http_request($url, $data);
}
/**
* todo 通过ticket换取二维码
*/
public function show_qrcode($data)
{
$ticket = urlencode($data["ticket"]);
return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={$ticket}";
}
public function post_http_request($url, $postData)
{
$resultArr = json_decode($this->http_url($url, json_encode($postData, JSON_UNESCAPED_UNICODE)), true);
if (isset($resultArr["errcode"]) && $resultArr["errcode"] != 0) {
file_log($resultArr, $this->logFile ?: "HttpRequest");
throw new \Exception($resultArr['errmsg']);
}
return $resultArr;
}
public function http_url($url, $data = null)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
if (!empty($data)) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
$res = curl_exec($ch);
if (curl_errno($ch)) {
throw new \Exception("error:" . curl_error($ch));
}
curl_close($ch);
return $res;
}
}
toArray() : false;
}
/**
* todo 保存关注的用户数据
*/
public static function saveSubscribeUser($userInfo)
{
$insertInfo = [];
$allowFiled = ["subscribe", "openid", "nickname", "sex", "language", "city", "province", "country", "headimgurl", "subscribe_time", "remark", "groupid", "tagid_list", "subscribe_scene", "qr_scene", "qr_scene_str"];
foreach ($allowFiled as $field) {
if (isset($userInfo[$field])) {
if ($field == "tagid_list") {
$userInfo[$field] = json_encode($userInfo[$field],JSON_UNESCAPED_UNICODE);
}
$insertInfo[$field] = $userInfo[$field];
}
} unset($field);
$user = self::insert(new self(), $insertInfo);
return $user ? $user->toArray() : false;
}
/**
* todo 查询用户的扫码登录状态信息
*/
public static function checkLoginStatus($eventKey,$field = null)
{
$param = [];
$param['where'] = [
['wechat_scan_event_key', '=', $eventKey],
['wechat_scan_event_key_expire_time', '>', time()]
];
if ($field) $param['field'] = $field;
return self::get_one($param);
}
/**
* todo 更新用户的扫码登录事件键值
*/
public static function updateLoginStatus($openId, $eventKey, $expireTime = 3600)
{
$where = [['openid', '=', $openId]];
$data = ['wechat_scan_event_key_expire_time' => time() + $expireTime, 'wechat_scan_event_key' => $eventKey];
return self::update_by_where(['where' => $where, 'data' => $data]);
}
}
CREATE TABLE `wechat_users` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
`openid` varchar(32) DEFAULT '' COMMENT '公众号唯一标识',
`subscribe` varchar(1) DEFAULT '' COMMENT '是否关注 1-是',
`nickname` varchar(50) DEFAULT '' COMMENT '关注用户昵称',
`sex` tinyint(1) unsigned DEFAULT '0' COMMENT '关注用户性别 0:未知 1:男 2:女',
`province` varchar(50) DEFAULT '' COMMENT '关注用户省份',
`city` varchar(50) DEFAULT '' COMMENT '关注用户城市',
`country` varchar(50) DEFAULT '' COMMENT '关注用户国家',
`headimgurl` varchar(255) DEFAULT '' COMMENT '关注用户头像',
`unionid` varchar(32) DEFAULT '' COMMENT '关注用户开放平台唯一标识',
`language` varchar(20) DEFAULT '' COMMENT '关注用户使用语言',
`subscribe_time` int(10) unsigned DEFAULT '0' COMMENT '关注时间',
`remark` varchar(100) DEFAULT '' COMMENT '公众号运营者对粉丝的备注',
`groupid` varchar(10) DEFAULT '' COMMENT '用户所在的分组ID',
`tagid_list` varchar(255) DEFAULT '' COMMENT '用户被打上的标签ID列表',
`subscribe_scene` varchar(20) DEFAULT '' COMMENT '用户关注的渠道来源',
`qr_scene` varchar(20) DEFAULT '' COMMENT '二维码扫码场景(开发者自定义)',
`qr_scene_str` varchar(50) DEFAULT '' COMMENT '二维码扫码场景描述(开发者自定义)',
`wechat_scan_event_key` varchar(50) DEFAULT '' COMMENT '用户扫码登录的事件键值',
`wechat_scan_event_key_expire_time` int(10) unsigned DEFAULT '0' COMMENT '用户扫码登录的事件键值过期时间',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='微信公众号用户表';