【微信公众号】微信集成功能--扫描二维码完成用户登录操作

目录

需求来源

实现思路

1、进入登录页面,生成微信公众号的临时二维码;

2、用户通过微信扫一扫二维码;

3、登录页面定时查询扫码结果;

代码实现(基于Laravel框架前后端混合)

HTML

PHP-路由文件

PHP-控制器

PHP-模型

数据表(请根据实际业务定义表结构)

感谢阅读,欢迎交流


需求来源

业务系统的PC端增加微信二维码扫码登录功能

实现思路

1、进入登录页面,生成微信公众号的临时二维码;

传入二维码的场景值(开发者接收微信服务器推送的数据包用以区分业务场景)、过期时间(用户无操作时多久刷新一次二维码)

2、用户通过微信扫一扫二维码;

用户扫描二维码,微信服务器推送扫描事件的xml数据包给开发者服务器,开发者通过xml数据包处理用户的扫码登录逻辑,更新用户成功登录的标识;

3、登录页面定时查询扫码结果;

定义轮询方法,通过生成二维码的场景值定时查询用户扫码的结果,成功则处理登录成功的逻辑,反之继续轮询,本文自动更新二维码(可不做自动刷新二维码,二维码到期时停止轮询);

代码实现(基于Laravel框架前后端混合)

HTML




    
    
    
    
    {{$title}}
    
    


@include('plugins.izi-modal') @include('plugins.loading') @if(!\Session::get('user'))

打开微信扫一扫

@else

用户已登录

@endif

PHP-路由文件

 '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');
        });
    });
});

PHP-控制器

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 .= "";
        }
        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;
    }
}

PHP-模型

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='微信公众号用户表';

感谢阅读,欢迎交流

你可能感兴趣的:(PHP,微信,后端,微信)