背景:在网站上点击微信登录,页面弹出微信二维码、首次扫描二维码跳转至公众号的主页关注界面、关注公众号后网站自动登录、第二次扫描登录的时候网站直接登录。
可直接提高公众号粉丝量,现在好多工具类网站由有此操作,例如:5118,蓝湖等…
必要条件
1、认证过的微信公众号(必须是服务号,要不然没有此权限) 如果没有微信服务号可以申请微信公众平台测试号入口:进入微信公众账号测试号申请系统
2、能访问到的在线服务器
实现流程及原理
原理
使用微信公众平台提供的生成带参二维码的接口可以生成带不同场景值的二维码,用户扫描后,公众号可以接收到扫码/关注事件推送,具体流程如下如下:
1、用户扫描二维码,如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值(自定义值)关注事件推送给网站开发者。
2、扫描二维码,如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值(自定义值)扫码事件推送给开发者。
流程
第一步:用户选择微信登录,自动生成二维码的时候你自定义一个唯一标识到二维码中,顺便把这个唯一标识传到前端页面中。
第二步:用户扫码关注后微信服务器发送一个关注事件或扫码事件消息到网站服务端接口,消息参数中包括了第一步的唯一标识和扫码用户openid等参数。
第三步:根据openid用微信公众号接口去获取用户信息,拿到用户信息之后就是实现注册功能逻辑,用唯一标识标记作为缓存key标记可以登录。
第四步:前端轮询查询定义参数为key的缓存是否标记可登录时,就开始实现登录逻辑,跳转页面,流程完毕。
详细代码实现流程
1.1.微信公众号与服务器和Token认证
由于我们自己服务,需要接管微信服务器的推送,所以需要在微信公众号后台配置服务器通知地址
PS:这个配置启用后,微信服务器会把相关的事件推送都转发到用户服务器当前配置的服务器地址上。
Token认证代码
php"> public function callback()
{
$data = $this->request->param();
if (empty($data['signature']) || empty($data['timestamp']) || empty($data['nonce']) || empty($data['echostr'])) {
return -1;
}
$signature = $data['signature'];
$timestamp = $data['timestamp'];
$nonce = $data['nonce'];
$echostr = $data['echostr'];
$token = 'khePcWQZudbbvnKBoJbZfWrHjne1';
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
if ($tmpStr == $signature) {
return $echostr;
} else {
return -1;
}
}
2.创建WeChat和WeChatEvent两个类
WeChat类主要用于获取a***ess_token,通过openId获取用户信息等与微信之间的请求
<?php
namespace app\api\controller;
class WeChat
{
protected $appid;
protected $secret;
protected $a***essToken;
function __construct()
{
$this->appid = "";
$this->secret = "";
$this->a***essToken = $this->getA***essToken();
}
/***
* 获取a***ess_token
* token的有效时间为2小时,这里可以做下处理,提高效率不用每次都去获取,
* 将token存储到缓存中,每2小时更新一下,然后从缓存取即可
* @return
**/
private function getA***essToken()
{
$url = "https://api.weixin.qq.***/cgi-bin/token?grant_type=client_credential&appid=" . $this->appid . "&secret=" . $this->secret;
$res = json_decode($this->httpRequest($url), true);
return $res['a***ess_token'];
}
/***
* POST或GET请求
* @url 请求url
* @data POST数据
* @return
**/
private function httpRequest($url, $data = "")
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
if (!empty($data)) { //判断是否为POST请求
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($curl);
curl_close($curl);
return $output;
}
/***
* 获取openID和unionId
* @code 微信授权登录返回的code
* @return
**/
public function getOpenIdOrUnionId($code)
{
$url = "https://api.weixin.qq.***/sns/oauth2/a***ess_token?appid=" . $this->appid . "&secret=" . $this->secret . "&code=" . $code . "&grant_type=authorization_code";
$data = $this->httpRequest($url);
return $data;
}
/***
* 通过openId获取用户信息
* @openId
* @return
**/
public function getUserInfo($openId)
{
$url = "https://api.weixin.qq.***/cgi-bin/user/info?a***ess_token=" . $this->a***essToken . "&openid=" . $openId . "&lang=zh_***";
$data = $this->httpRequest($url);
return $data;
}
/***
* 发送模板短信
* @data 请求数据
* @return
**/
public function sendTemplateMessage($data = "")
{
$url = "https://api.weixin.qq.***/cgi-bin/message/template/send?a***ess_token=" . $this->a***essToken;
$result = $this->httpRequest($url, $data);
return $result;
}
/***
* 生成带参数的二维码
* @scene_id 自定义参数(整型)
* @return
**/
public function getQrcode($scene_id)
{
$url = "https://api.weixin.qq.***/cgi-bin/qrcode/create?a***ess_token=" . $this->a***essToken;
$data = array(
"expire_seconds" => 3600, //二维码的有效时间(1小时)
"action_name" => "QR_SCENE",
"action_info" => array("scene" => array("scene_id" => $scene_id))
);
$result = $this->httpRequest($url, json_encode($data));
return $result;
}
/***
* 生成带参数的二维码
* @scene_str 自定义参数(字符串)
* @return
**/
public function getQrcodeByStr($scene_str)
{
$url = "https://api.weixin.qq.***/cgi-bin/qrcode/create?a***ess_token=" . $this->a***essToken;
$data = array(
"expire_seconds" => 3600, //二维码的有效时间(1小时)
"action_name" => "QR_STR_SCENE",
"action_info" => array("scene" => array("scene_str" => $scene_str))
);
$result = $this->httpRequest($url, json_encode($data));
return $result;
}
/**
* 换取二维码
* @ticket
* @return
*/
public function generateQrcode($ticket)
{
return "https://mp.weixin.qq.***/cgi-bin/showqrcode?ticket=" . $ticket;
}
/**
* 表情转换(进行编码)
* @param $nickname
* @return string
*/
public function emoji_encode($nickname){
$strEncode = '';
$length = mb_strlen($nickname,'utf-8');
for ($i=0; $i < $length; $i++) {
$_tmpStr = mb_substr($nickname,$i,1,'utf-8');
if(strlen($_tmpStr) >= 4){
$strEncode .= rawurlencode($_tmpStr);
}else{
$strEncode .= $_tmpStr;
}
}
return $strEncode;
}
}
WeChatEvent类主要用于处理微信返回事件
<?php
namespace app\api\controller;
// 事件类
class WeChatEvent
{
/**
* 扫描带参二维码事件
*/
public function scan($data)
{
// 标记前端可登陆
Login::markLogin($data['EventKey'],$data['FromUserName']);
}
/**
* 关注订阅
*/
public function subscribe($data)
{
// 关注事件的场景值会带一个前缀需要去掉
$data['EventKey'] = str_replace('qrscene_','',$data['EventKey']);
// 标记前端可登陆
Login::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(){
}
}
3.服务端生成带唯一标识二维码并将唯一标识返回给前端
public function wx_code()
{
$aes = new WeChat();
$scene_str = "gjw" . time(); //"lrfun" . time(); //这里建议设唯一值(如:随机字符串+时间戳)
$result = json_decode($aes->getQrcodeByStr($scene_str), true);
$qrcode = $aes->generateQrcode($result['ticket']);
$data['qr_code_url'] = $qrcode;
$data['scene_value'] = $scene_str;
return $data;
}
4.前端请求二维码,定时任务获取扫码状态状态
<style>
.wechat-url{
width: 100px;
height: 100px;
}
</style>
<script src="__TROOT__/admin/js/jquery.min.js"></script>
<button class="wechat-login">登录</button>
<img src="" class="wechat-url" >
<script>
// 方便清除轮询
var timer = null;
var flag = '';
$(document).on('click', '.wechat-login', function () {
// 请求登录二维码见 接口详细文章第3步
$.get('/admin/login/wx_code', function (data, status) {
console.log(data)
$('.wechat-url').attr('src', data.qr_code_url);
// 显示二维码
flag = data.scene_value;
// 轮询登录状态 接口详细文章第7步
timer = setInterval(function () {
// 请求参数是二维码中的场景值
$.get('/admin/login/loginCheck?wechat_flag='+flag, function (data, status) {
if(data.code==20000){
clearInterval(timer);
alert('登录成功')
}
});
}, 2000);
});
});
// 返回时清除轮询
$('.wechat-back').click(function () {
clearInterval(timer)
});
function closeLoginPan() {
clearInterval(timer);
}
</script>
5.服务端接受扫码事件接受处理
/**
* 接受微信的xml请求
*/
public function callback()
{
//这里做了个每次请求写入日志,方便后期排查问题,这里如果不需要可以删除
$file = fopen('log/wxgzhlog.txt', 'a');
fwrite($file, "\n".date('Y-m-d H:i:s', time()) . "请求了接口 \n");
$callbackXml = file_get_contents('php://input');
$dataje = json_encode(simplexml_load_string($callbackXml, 'SimpleXMLElement', LIBXML_NOCDATA)); //将返回的xml转为数组
$data = json_decode($dataje, true); //将返回的xml转为数组
fwrite($file, 'callbackXml-data-json='.$dataje );
fclose($file);
if (empty($data)) {
return false;
}
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;
}
}
/**
* 事件引导处理方法(事件有许多,拆分处理)见文章第2步
*
* @param $data
* @return mixed
* @internal param $request
* @internal param $event
*/
static function handleEvent($data)
{
$method = strtolower($data['Event']);
$event = new WeChatEvent();
if (method_exists($event, $method)) {
return call_user_func_array([$event, $method], [$data]);
}
}
6.注册登录逻辑标记登录状态
public static function markLogin($key,$val)
{
$user = db('admin_user')->where('wx_openid',$val)->find();
$time = time();
if(empty($user)){
$WeChat = new WeChat();
$wxUser = json_decode($WeChat->getUserInfo($val));
$user_data = [
'wx_openid'=>$val,
'name'=>$WeChat->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
];
db('admin_user')->insert($user_data);
}
// 写入缓存
Cache::set($key, $val,60*60);
}
7.前端定时任务登录状态获取方法
public function loginCheck()
{
// 判断请求是否有微信登录标识
if (!$flag = $this->request->get('wechat_flag')) {
$data = [
'message' => '参数错误.....',
'code' => 422,
];
return json_encode($data);
}
// 根据微信标识在缓存中获取需要登录用户的 UID
$uid = Cache::get($flag);
if(empty($uid)){
$data = [
'message' => '参数错误.....',
'code' => 422,
];
return json_encode($data);
}
$user = db('admin_user')->where('wx_openid',$uid)->find();
if(empty($user)){
$data = [
'message' => '用户未注册.....',
'code' => 422,
];
}else{
$data = [
'message' => '登录成功',
'code' => 20000
];
}
return $data;
}