第三方登录(一)QQ登录

用QQ登录第三方网站几乎成为了必不可少的功能,用起来也确实很方便。下面介绍一下如何为网站添加QQ登录功能。

成为开发者

具体操作详情:
http://wiki.connect.qq.com/%E6%88%90%E4%B8%BA%E5%BC%80%E5%8F%91%E8%80%85

创建应用

开发者身份审核通过后,就可以创建应用了,操作详情:
https://wiki.connect.qq.com/__trashed-2

下图是我创建一个网站应用填写信息的示例:

原理介绍

首先调用QQ提供的授权接口,这一步会要求用户登录QQ并进行授权,授权成功后,QQ服务器会携带code值返回开发者的回调地址,然后通过code获取access_token,再通过access_token获取用户的openid,这里可通过openid判断用户是否存在,如果存在可进行登录成功的逻辑,不存在,则通过openid获取用户信息,存入数据库。

编写代码

如果从官网下载下来PHP SDK的话,我们可以看一下代码,不是很复杂,但是要把SDK引入框架,需要做一些修改。由于这篇文章主要是介绍QQ登录的原理,因此我把SDK的几个类的代码进行了整合,把主要用到的方法放在了Api模块下的Qqlogin控制器,这样方便查看各个方法,而不用各个类之间来回切换。

假设前台有个QQ登录的按钮,跳转到Home模块下的Qqlogin控制器qq_login方法。

Home模块Qqlogin控制器:

<?php
namespace Home\Controller;

use Api\Controller\QqloginController as Qqloginapi;
use Vendor\Page;

class QqloginController extends ComController
{
    public function qq_login()
    {
        $Oauth = new Qqloginapi();
        //第一步:授权并获取code
        $Oauth -> qq_login();
    }

    //回调
    public function callback()
    {
        $Oauth = new Qqloginapi();
        //第二步:通过code获取access_token
        $access_token = $Oauth -> qq_callback();
        //第三步:通过access_token获取登录者的openid
        $openid = $Oauth -> get_openid();
//此处为演示,不建议直接通过openid验证登录,应进行第四步 $exist = M('user') -> where(['openid' => $openid]) -> getField('id'); if($exist){ session('uid',$exist); $this -> redirect('Home/Index/index'); }else{ // 第四步:通过openid获取用户信息 $user_info = $Oauth -> get_user_info(); // dump($userinfo); if($user_info['gender'] == '男'){ $data['gender'] = 1; }elseif($user_info['gender'] == '女'){ $data['gender'] = 2; }else{ $data['gender'] = 3; } $data['nickname'] = $user_info['nickname']; $data['avatar'] = $user_info['figureurl_1']; $data['addtime'] = time(); $data['openid'] = $openid; if($user_info['province']){ $data['province'] = $user_info['province']; } if($user_info['city']){ $data['city'] = $user_info['city']; } $res = M('user') -> data($data) -> add(); if($res){ session('uid',$res); $this -> redirect('Home/Index/index'); } } } }

Api模块Qqlogin:

<?php

namespace Api\Controller;

use Vendor\Page;

class QqloginController extends ComController
{

    const GET_AUTH_CODE_URL = "https://graph.qq.com/oauth2.0/authorize"; //登录页面授权,获取code
    const GET_ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token"; //获取Token
    const GET_OPENID_URL = "https://graph.qq.com/oauth2.0/me"; //获得用户OpenId

    private $APIMap = array(
        /*                       qzone                    */
        "add_blog" => array(
            "https://graph.qq.com/blog/add_one_blog",
            array("title", "format" => "json", "content" => null),
            "POST"
        ),
        "add_topic" => array(
            "https://graph.qq.com/shuoshuo/add_topic",
            array("richtype","richval","con","#lbs_nm","#lbs_x","#lbs_y","format" => "json", "#third_source"),
            "POST"
        ),
        "get_user_info" => array(
            "https://graph.qq.com/user/get_user_info",
            array("format" => "json"),
            "GET"
        ),
        "add_one_blog" => array(
            "https://graph.qq.com/blog/add_one_blog",
            array("title", "content", "format" => "json"),
            "GET"
        ),
        "add_album" => array(
            "https://graph.qq.com/photo/add_album",
            array("albumname", "#albumdesc", "#priv", "format" => "json"),
            "POST"
        ),
        "upload_pic" => array(
            "https://graph.qq.com/photo/upload_pic",
            array("picture", "#photodesc", "#title", "#albumid", "#mobile", "#x", "#y", "#needfeed", "#successnum", "#picnum", "format" => "json"),
            "POST"
        ),
        "list_album" => array(
            "https://graph.qq.com/photo/list_album",
            array("format" => "json")
        ),
        "add_share" => array(
            "https://graph.qq.com/share/add_share",
            array("title", "url", "#comment","#summary","#images","format" => "json","#type","#playurl","#nswb","site","fromurl"),
            "POST"
        ),
        "check_page_fans" => array(
            "https://graph.qq.com/user/check_page_fans",
            array("page_id" => "314416946","format" => "json")
        ),
        /*                    wblog                             */

        "add_t" => array(
            "https://graph.qq.com/t/add_t",
            array("format" => "json", "content","#clientip","#longitude","#compatibleflag"),
            "POST"
        ),
        "add_pic_t" => array(
            "https://graph.qq.com/t/add_pic_t",
            array("content", "pic", "format" => "json", "#clientip", "#longitude", "#latitude", "#syncflag", "#compatiblefalg"),
            "POST"
        ),
        "del_t" => array(
            "https://graph.qq.com/t/del_t",
            array("id", "format" => "json"),
            "POST"
        ),
        "get_repost_list" => array(
            "https://graph.qq.com/t/get_repost_list",
            array("flag", "rootid", "pageflag", "pagetime", "reqnum", "twitterid", "format" => "json")
        ),
        "get_info" => array(
            "https://graph.qq.com/user/get_info",
            array("format" => "json")
        ),
        "get_other_info" => array(
            "https://graph.qq.com/user/get_other_info",
            array("format" => "json", "#name", "fopenid")
        ),
        "get_fanslist" => array(
            "https://graph.qq.com/relation/get_fanslist",
            array("format" => "json", "reqnum", "startindex", "#mode", "#install", "#sex")
        ),
        "get_idollist" => array(
            "https://graph.qq.com/relation/get_idollist",
            array("format" => "json", "reqnum", "startindex", "#mode", "#install")
        ),
        "add_idol" => array(
            "https://graph.qq.com/relation/add_idol",
            array("format" => "json", "#name-1", "#fopenids-1"),
            "POST"
        ),
        "del_idol" => array(
            "https://graph.qq.com/relation/del_idol",
            array("format" => "json", "#name-1", "#fopenid-1"),
            "POST"
        ),
        /*                           pay                          */

        "get_tenpay_addr" => array(
            "https://graph.qq.com/cft_info/get_tenpay_addr",
            array("ver" => 1,"limit" => 5,"offset" => 0,"format" => "json")
        )
    );
    private $keysArr;

    public function _initialize()
    {
        parent::_initialize();
        if(session('openid')){
            $this->keysArr = array(
                "oauth_consumer_key" => C('QQ_APPID'),
                "access_token" => session('access_token'),
                "openid" => session('openid')
            );
        }else{
            $this->keysArr = array(
                "oauth_consumer_key" => C('QQ_APPID')
            );
        }
    }

    public function qq_login(){
        //-------生成唯一随机串防CSRF攻击
        $state = md5(uniqid(rand(), TRUE));
        session('state',$state);
        //-------构造请求参数列表
        $keysArr = array(
            "response_type" => "code",
            "client_id" => C('QQ_APPID'),
            "redirect_uri" => C('QQ_CALLBACK'),
            "state" => session('state'),
            "scope" => C('QQ_SCOPE')
        );
        $login_url =  $this->combineURL(self::GET_AUTH_CODE_URL, $keysArr);
        header("Location:$login_url");
    }

    public function qq_callback(){
        $state = session('state');
        //--------验证state防止CSRF攻击
        if(!$state || $_GET['state'] != $state){
            return false;
        }
        //-------请求参数列表
        $keysArr = array(
            "grant_type" => "authorization_code",
            "client_id" => C('QQ_APPID'),
            "redirect_uri" => C('QQ_CALLBACK'),
            "client_secret" => C('QQ_APPKEY'),
            "code" => $_GET['code']
        );
        //------构造请求access_token的url
        $token_url = $this->combineURL(self::GET_ACCESS_TOKEN_URL, $keysArr);
        $response = $this->get_contents($token_url);

        if(strpos($response, "callback") !== false){
            $lpos = strpos($response, "(");
            $rpos = strrpos($response, ")");
            $response  = substr($response, $lpos + 1, $rpos - $lpos -1);
            $msg = json_decode($response);
            if(isset($msg->error)){
                $this->showError($msg->error, $msg->error_description);
            }
        }
        $params = array();
        parse_str($response, $params);

        session('access_token',$params["access_token"]);
        return $params["access_token"];
    }

    public function get_openid(){
        //-------请求参数列表
        $keysArr = array(
            "access_token" => session('access_token')
        );
        $graph_url = $this->combineURL(self::GET_OPENID_URL, $keysArr);
        $response = $this->get_contents($graph_url);
        //--------检测错误是否发生
        if(strpos($response, "callback") !== false){
            $lpos = strpos($response, "(");
            $rpos = strrpos($response, ")");
            $response = substr($response, $lpos + 1, $rpos - $lpos -1);
        }
        $user = json_decode($response);
        if(isset($user->error)){
            $this->showError($user->error, $user->error_description);
        }
        //------记录openid
        session('openid',$user -> openid);
        return $user->openid;
    }

    /**
     * showError
     * 显示错误信息
     * @param int $code 错误代码
     * @param string $description 描述信息(可选)
     */
    public function showError($code, $description = '
){
        echo "<meta charset=\"UTF-8\">";
        echo "<h3>error:</h3>$code";
        echo "<h3>msg :</h3>$description";
        exit();
    }

    //调用相应api
    private function _applyAPI($arr, $argsList, $baseUrl, $method){
        $pre = "#";
        $keysArr = $this->keysArr;
        $optionArgList = array();//一些多项选填参数必选一的情形
        foreach($argsList as $key => $val){
            $tmpKey = $key;
            $tmpVal = $val;
            if(!is_string($key)){
                $tmpKey = $val;

                if(strpos($val,$pre) === 0){
                    $tmpVal = $pre;
                    $tmpKey = substr($tmpKey,1);
                    if(preg_match("/-(\d$)/", $tmpKey, $res)){
                        $tmpKey = str_replace($res[0], "", $tmpKey);
                        $optionArgList[$res[1]][] = $tmpKey;
                    }
                }else{
                    $tmpVal = null;
                }
            }
            //-----如果没有设置相应的参数
            if(!isset($arr[$tmpKey]) || $arr[$tmpKey] === ""){
                if($tmpVal == $pre){//则使用默认的值
                    continue;
                }else if($tmpVal){
                    $arr[$tmpKey] = $tmpVal;
                }else{
                    if($v = $_FILES[$tmpKey]){
                        $filename = dirname($v['tmp_name'])."/".$v['name'];
                        move_uploaded_file($v['tmp_name'], $filename);
                        $arr[$tmpKey] = "@$filename";
                    }else{
                        $this->showError("api调用参数错误","未传入参数$tmpKey");
                    }
                }
            }
            $keysArr[$tmpKey] = $arr[$tmpKey];
        }
        //检查选填参数必填一的情形
        foreach($optionArgList as $val){
            $n = 0;
            foreach($val as $v){
                if(in_array($v, array_keys($keysArr))){
                    $n ++;
                }
            }
            if(! $n){
                $str = implode(",",$val);
                $this->showError("api调用参数错误",$str."必填一个");
            }
        }
        if($method == "POST"){
            if($baseUrl == "https://graph.qq.com/blog/add_one_blog") $response = $this->urlUtils->post($baseUrl, $keysArr, 1);
            else $response = $this->post($baseUrl, $keysArr, 0);
        }else if($method == "GET"){
            $response = $this->get($baseUrl, $keysArr);
        }
        return $response;
    }

    /**
     * _call
     * 魔术方法,做api调用转发
     * @param string $name    调用的方法名称
     * @param array $arg      参数列表数组
     * @since 5.0
     * @return array          返加调用结果数组
     */
    public function __call($name,$arg){
        //如果APIMap不存在相应的api
        if(empty($this->APIMap[$name])){
            $this->showError("api调用名称错误","不存在的API: <span style='color:red;'>$name</span>");
        }
        //从APIMap获取api相应参数
        $baseUrl = $this->APIMap[$name][0];
        $argsList = $this->APIMap[$name][1];
        $method = isset($this->APIMap[$name][2]) ? $this->APIMap[$name][2] : "GET";
        if(empty($arg)){
            $arg[0] = null;
        }
        //对于get_tenpay_addr,特殊处理,php json_decode对\xA312此类字符支持不好
        if($name != "get_tenpay_addr"){
            $response = json_decode($this->_applyAPI($arg[0], $argsList, $baseUrl, $method));
            $responseArr = $this->objToArr($response);
        }else{
            $responseArr = $this->simple_json_parser($this->_applyAPI($arg[0], $argsList, $baseUrl, $method));
        }
        //检查返回ret判断api是否成功调用
        if($responseArr['ret'] == 0){
            return $responseArr;
        }else{
            $this->showError($response->ret, $response->msg);
        }
    }

    //php 对象到数组转换
    private function objToArr($obj){
        if(!is_object($obj) && !is_array($obj)) {
            return $obj;
        }
        $arr = array();
        foreach($obj as $k => $v){
            $arr[$k] = $this->objToArr($v);
        }
        return $arr;
    }

    /**
     * get
     * get方式请求资源
     * @param string $url     基于的baseUrl
     * @param array $keysArr  参数列表数组
     * @return string         返回的资源内容
     */
    public function get($url, $keysArr){
        $combined = $this->combineURL($url, $keysArr);
        return $this->get_contents($combined);
    }

    /**
     * post
     * post方式请求资源
     * @param string $url       基于的baseUrl
     * @param array $keysArr    请求的参数列表
     * @param int $flag         标志位
     * @return string           返回的资源内容
     */
    public function post($url, $keysArr, $flag = 0){
        $ch = curl_init();
        if(! $flag) curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $keysArr);
        curl_setopt($ch, CURLOPT_URL, $url);
        $ret = curl_exec($ch);
        curl_close($ch);
        return $ret;
    }

    //简单实现json到php数组转换功能
    private function simple_json_parser($json){
        $json = str_replace("{","",str_replace("}","", $json));
        $jsonValue = explode(",", $json);
        $arr = array();
        foreach($jsonValue as $v){
            $jValue = explode(":", $v);
            $arr[str_replace('"',"", $jValue[0])] = (str_replace('"', "", $jValue[1]));
        }
        return $arr;
    }

    /**
     * combineURL
     * 拼接url
     * @param string $baseURL   基于的url
     * @param array  $keysArr   参数列表数组
     * @return string           返回拼接的url
     */
    public function combineURL($baseURL,$keysArr){
        $combined = $baseURL."?";
        $valueArr = array();
        foreach($keysArr as $key => $val){
            $valueArr[] = "$key=$val";
        }
        $keyStr = implode("&",$valueArr);
        $combined .= ($keyStr);
        return $combined;
    }

    public function get_contents($url){
        if (ini_get("allow_url_fopen") == "1") {
            $response = file_get_contents($url);
        }else{
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
            curl_setopt($ch, CURLOPT_URL, $url);
            $response = curl_exec($ch);
            curl_close($ch);
        }
        if(empty($response)){
            return false;
        }
        return $response;
    }
}

config.php:

<?php
return array(//'配置项'=>'配置值'
    //QQ登录配置项
    'QQ_APPID' => '应用的appid',
    'QQ_APPKEY' => '应用的appkey',
    'QQ_CALLBACK' => 'https://example.acier.cn/index.php/Home/Qqlogin/callback',    //回调地址
    'QQ_SCOPE' => 'get_user_info,list_album,add_album,upload_pic,add_topic,add_weibo', //授权接口列表
);

发表评论

发表回复

沙发空缺中,还不快抢~