微信扫码 - 关注公众号后网站自动注册并登录的实现
需求描述
在自己网站上点击微信登录,网站自己弹出一个二维码、扫描二维码后弹出公众号的关注界面、只要一关注公众号网站自动登录、第二次扫描登录的时候网站直接登录。
大家可以体验一下 「随便找的一个网站」
前期准备
一个公众号(必须认证,配置服务器)
实现原理
公众平台提供了生成带参二维码的接口。使用该接口可以生成带不同场景值的二维码,用户扫描后,公众号可以接收到扫码/关注事件推送,在细分如下:
- 扫描二维码,如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值(自定义值)关注事件推送给开发者。
- 扫描二维码,如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值(自定义值)扫码事件推送给开发者。
设计如下流程:
- 生成二维码的时候你自定义一个参数到二维码中,顺便把这个参数传到前端页面中。
- 用户扫码关注后微信服务器发送一个关注事件或扫码事件消息到自己服务端,消息参数中包括了自定义参数和扫码用户openid等参数。
- 根据openid用微信公众号接口去获取用户信息,拿到用户信息之后就是实现注册逻辑,用自定义参数标记作为缓存key标记可以登录。
- 前端轮询查询定义参数为key的缓存是否标记可登录时,就开始实现登录逻辑,重载页面,流程完毕。
实战
1.微信公众号部署服务器和Token认证
由于我们自己服务,需要接管微信服务器的推送,所以需要在微信公众号后台配置服务器通知地址,公众号测试号的配置大同小异这里不做累述,如图所示:
注:这个配置启用后,微信服务器会把相关的事件推送都转发到用户服务器上,同时微信后台的自定义菜单和回复也失效,需要用户在自己服务器上通过微信接口来接管
Token验证示例代码:
<?php
// 微信token认证
$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"];
$echostr = $_GET["echostr"];
// 你在微信公众号后台的设置的Token
$token = "";
// 1)将token、timestamp、nonce三个参数进行字典序排序
$tmpArr = array($nonce,$token,$timestamp);
sort($tmpArr,SORT_STRING);
// 2)将三个参数字符串拼接成一个字符串进行sha1加密
$str = implode($tmpArr);
$sign = sha1($str);
// 3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
if ($sign == $signature) {
echo $echostr;
}
2.服务端生成带参二维码
先了解下,公众号的带参二维码生成 接口文档 ,我们选择临时二维码,步骤如下:
第一步:通过AppID和AppSecret获取access_token
这里的access_token和上边的token不是一回事,不要混淆。获取access_token接口文档 ,这一步建议抽离封装,以便其他接口复用。
第二步:通过access_token创建二维码ticket
每次创建二维码ticket需要提供一个开发者自行设定的参数(scene_id),分别介绍临时二维码和永久二维码的创建二维码ticket过程。
第三步:通过ticket换取二维码
这一步其实就是拼接成一个二维码图片地址:https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET 提醒:TICKET记得进行UrlEncode,然后把这个二维码图片地址,返回给前端直接显示
示例代码:
/**
* 获取微信场景二维码
* @param $sceneValue 场景值
* @param int $type 1:临时二维码 2:永久二维码
* @return string
*/
public static function getWeChatUrl($sceneValue,$type = 1){
$accessToken = WxToken::getToken();
$url = 'https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token='.$accessToken;
$actionName = 'QR_STR_SCENE';
if($type==2){
$actionName = 'QR_LIMIT_STR_SCENE';
}
$res = self::post($url,json_encode([
'expire_seconds'=>604800,
'action_name'=>$actionName,
'action_info'=>[
'scene'=>[
'scene_id'=>$sceneValue,
'scene_str'=>$sceneValue
]
]
]));
if(isset($res['errcode']) && $res['errcode']){
echo $res['errmsg'];die();
}
return 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket='.$res['ticket'];
}
3.前端请求二维码,轮询状态(也可使用websocket)
// 方便清除轮询
var timer = null;
var flag = '';
$(document).on('click', '.wechat-login', function () {
// 请求登录二维码
$.get('/admin/index/weChatQrCode', function (data, status) {
$('.wechat-url').attr('src', data.data.qr_code_url);
// 显示二维码
flag = data.data.scene_value;
// 轮询登录状态
timer = setInterval(function () {
// 对话框关闭时,清除轮询
if ($('#wx-login-pan').attr('class').indexOf('open') == -1) {
clearInterval(timer);
return;
}
// 请求参数是二维码中的场景值
$.get('/admin/index/loginCheck?wechat_flag='+flag, function (data, status) {
if(data.code==0){
window.location.href = '/home/serve/apply';
}
});
}, 2000);
});
});
// 返回时清除轮询
$('.wechat-back').click(function () {
clearInterval(timer)
});
function closeLoginPan() {
clearInterval(timer);
}
4.后端扫码事件接受处理
// 微信事件接收类
class WxPush
{
/**
* 获得请求时POST:XML字符串
* 不能用$_POST获取,因为没有key
*/
public static function getResponse()
{
$xmlStr = $GLOBALS['HTTP_RAW_POST_DATA'];
Log::record($xmlStr, 'wx_push');
if (empty($xmlStr)) {
return false;
}
// 解析该xml字符串,利用simpleXML
libxml_disable_entity_loader(true);
//禁止xml实体解析,防止xml注入
$xml = simplexml_load_string($xmlStr, 'SimpleXMLElement', LIBXML_NOCDATA);
$data = json_decode(json_encode($xml),true);
//判断该消息的类型,通过元素MsgType
Log::write($data, 'wx_push');
switch ($data['MsgType']) {
case 'event': // 事件处理
self::handleEvent($data);
break;
case 'text'://文本消息
break;
case 'image'://图片消息
break;
case 'voice'://语音消息
break;
case 'video'://视频消息
break;
case 'shortvideo'://短视频消息
break;
case 'location'://位置消息
break;
case 'link'://链接消息
break;
}
}
/**
* 事件引导处理方法(事件有许多,拆分处理)
*
* @param $data
* @return mixed
* @internal param $request
* @internal param $event
*/
static function handleEvent($data)
{
$method = strtolower($data['Event']);
$event = new Event();
if (method_exists($event, $method)) {
return call_user_func_array([$event, $method], [$data]);
}
}
}
// 事件类
class Event
{
/**
* 扫描带参二维码事件
*/
public function scan($data)
{
// 标记前端可登陆
Admin::markLogin($data['EventKey'],$data['FromUserName']);
}
/**
* 关注订阅
*/
public function subscribe($data)
{
// 关注事件的场景值会带一个前缀需要去掉
$data['EventKey'] = str_replace('qrscene_','',$data['EventKey']);
// 标记前端可登陆
Admin::markLogin($data['EventKey'],$data['FromUserName']);
}
/**
* 取消订阅
*/
public function unsubscribe($data)
{
$adminName = $data['FromUserName'];
$admin = Admin::get(['wx_openid'=>$data['FromUserName']]);
if($admin && $admin['name']){
$adminName = $admin['name'];
}
}
/**
* 菜单点击事件
*/
public function click(){
}
/**
* 连接跳转事件
*/
public function view(){
}
}
/**
* 标记可登录
*/
public static function markLogin($key,$val)
{
$admin = self::get(['wx_openid'=>$val]);
$time = time();
if(empty($admin)){
$wxUser = WxUser::getCgiUserInfo($val);
$admin = self::create([
'wx_openid'=>$val,
'name'=>Emoji::encode($wxUser['nickname']),
'head_img_url'=>$wxUser['headimgurl'],
'sex'=>$wxUser['sex'],
'province'=>$wxUser['province'],
'city'=>$wxUser['city'],
'type'=>3,// 客户
'create_time'=>$time,
'update_time'=>$time
]);
}
// 写入缓存
Cache::set($key, $val,60*60);
}
5.登录检查
在前端轮询请求检查登录状态接口时,把自定义参数传给后端,后端以该自定义参数为key去缓存查询可登录状态,查到可以登录了则去授权登录,返回登录成功。
/**
* 微信用户登录检查
*
*/
public function loginCheck()
{
// 判断请求是否有微信登录标识
if (!$flag = $this->request->get('wechat_flag')) {
$this->apiError(-1,'标识不能为空');
}
// 根据微信标识在缓存中获取需要登录用户的 UID
$uid = Cache::get($flag);
if(empty($uid)){
$this->apiError(-1,'void');
}
$admin = Admin::get(['wx_openid'=>$uid]);
if(empty($admin)){
$this->apiError(-1,'用户未注册');
}
Admin::doLogin($admin);
$this->apiSuccess($flag);
}