您好,欢迎来到 猿书

QQ等第三方登录

易大师 2016-07-02 1782次浏览 1条评论 1 打赏作者 0 0
通过 composer 安装该应用 php composer.phar require --prefer-dist yiisoft/yii2-authclient "*" 更改Yii2的配置文件config/main.php,在components中增加如下内容 'components' =>...

通过 composer 安装 authclient 扩展

php composer.phar require --prefer-dist yiisoft/yii2-authclient "*"

更改Yii2的配置文件config/main.php,在components中增加如下内容

'components' => [
    'authClientCollection' => [
        'class' => 'yii\authclient\Collection',
        'clients' => [
            'facebook' => [
                #这里以faceBook为例(Yii2本身已实现)
                'class' => 'yii\authclient\clients\Facebook',
                'clientId' => 'facebook_client_id', #你的第三方平台申请的AppId
                'clientSecret' => 'facebook_client_secret',#你的第三方平台申请的AppKey
            ],
            'qq' => [
                'class'=>'yii\authclient\clients\QqOAuth',
                'clientId'=>'',
                'clientSecret'=>'',
            ],
        ],
    ]
]

如果我们要将用户信息存入数据库,则数据库模式可以是:

class m??????_??????_auth extends \yii\db\Migration
{
    public function up()
    {
        $this->createTable('user', [
            'id' => $this->primaryKey(),
            'username' => $this->string()->notNull(),
            'auth_key' => $this->string()->notNull(),
            'password_hash' => $this->string()->notNull(),
            'password_reset_token' => $this->string()->notNull(),
            'email' => $this->string()->notNull(),
            'status' => $this->smallInteger()->notNull()->defaultValue(10),
            'created_at' => $this->integer()->notNull(),
            'updated_at' => $this->integer()->notNull(),
        ]);

        $this->createTable('auth', [
            'id' => $this->primaryKey(),
            'user_id' => $this->integer()->notNull(),
            'source' => $this->string()->notNull(),
            'source_id' => $this->string()->notNull(),
        ]);

        $this->addForeignKey('fk-auth-user_id-user-id', 'auth', 'user_id', 'user', 'id', 'CASCADE', 'CASCADE');
    }

    public function down()
    {
        $this->dropTable('auth');
        $this->dropTable('user');
    }
}

上述 SQL 中 user 表在高级项目模板中用于保存用户信息。
每个用户可以由多重外部服务验证,因此每个 user 记录可以关联多个 auth 记录。
auth 表中 source 是验证提供商的名称 source_id 是外部服务在该用户成功登录后提供的唯一 ID。

使用上述创建的数据表用 gii 生成 Auth 模型

因为 authclient 是外国佬写的扩展,所有里边只有 github google 等外国佬的第三方登录,所以我们要自己扩展QQ登录。

以下是QqOAuth类代码

<?php
namespace yii\authclient\clients;

use yii\authclient\OAuth2;
use yii\base\Exception;
use yii\helpers\Json;

class QqOAuth extends OAuth2
{
    public $authUrl = 'https://graph.qq.com/oauth2.0/authorize';
    public $tokenUrl = 'https://graph.qq.com/oauth2.0/token';
    public $apiBaseUrl = 'https://graph.qq.com';

    public function init()
    {
        parent::init();
        if ($this->scope === null) {
            $this->scope = implode(',', [
                'get_user_info',
            ]);
        }
    }

    protected function initUserAttributes()
    {
        $openid =  $this->api('oauth2.0/me', 'GET');
        $qquser = $this->api("user/get_user_info", 'GET', ['oauth_consumer_key'=>$openid['client_id'], 'openid'=>$openid['openid']]);
        $qquser['openid'] = $openid['openid'];
        $qquser['id'] = $qquser['openid'];
        $qquser['login'] = $qquser['nickname'];
        $qquser['email'] = $qquser['nickname'] . '@qq.com';
        return $qquser;
    }

    protected function defaultName()
    {
        return 'qq';
    }

    protected function defaultTitle()
    {
        return 'Qq';
    }
    
    
    /**
     * 该扩展初始的处理方法似乎QQ互联不能用,应此改写了方法
     * @see \yii\authclient\BaseOAuth::processResponse()
     */
    protected function processResponse($rawResponse, $contentType = self::CONTENT_TYPE_AUTO)
    {
        if (empty($rawResponse)) {
            return [];
        }
        switch ($contentType) {
            case self::CONTENT_TYPE_AUTO: {
                $contentType = $this->determineContentTypeByRaw($rawResponse);
                if ($contentType == self::CONTENT_TYPE_AUTO) {
                    //以下代码是特别针对QQ互联登录的,也是与原方法不一样的地方 
                    if(strpos($rawResponse, "callback") !== false){
                        $lpos = strpos($rawResponse, "(");
                        $rpos = strrpos($rawResponse, ")");
                        $rawResponse = substr($rawResponse, $lpos + 1, $rpos - $lpos -1);
                        $response = $this->processResponse($rawResponse, self::CONTENT_TYPE_JSON);
                        break;
                    }
                    //代码添加结束
                    throw new Exception('Unable to determine response content type automatically.');
                }
                $response = $this->processResponse($rawResponse, $contentType);
                break;
            }
            case self::CONTENT_TYPE_JSON: {
                $response = Json::decode($rawResponse, true);
                if (isset($response['error'])) {
                    throw new Exception('Response error: ' . $response['error']);
                }
                break;
            }
            case self::CONTENT_TYPE_URLENCODED: {
                $response = [];
                parse_str($rawResponse, $response);
                break;
            }
            case self::CONTENT_TYPE_XML: {
                $response = $this->convertXmlToArray($rawResponse);
                break;
            }
            default: {
                throw new Exception('Unknown response type "' . $contentType . '".');
            }
        }
       
        return $response;
    }
    
    
}

下一步是向 Web 控制器中添加 [[yii\authclient\AuthAction]],然后实现 successCallback 方法,
该方法与你的实际需要保持一致。典型的最终控制器类似如下代码:

class SiteController extends Controller
{
    public function actions()
    {
        return [
            'auth' => [
                'class' => 'yii\authclient\AuthAction',
                'successCallback' => [$this, 'onAuthSuccess'],
            ],
        ];
    }

    public function onAuthSuccess($client)
    {
        $attributes = $client->getUserAttributes();

        /* @var $auth Auth */
        $auth = Auth::find()->where([
            'source' => $client->getId(),
            'source_id' => $attributes['id'],
        ])->one();
        
        if (Yii::$app->user->isGuest) {
            if ($auth) { // 登录
                $user = $auth->user;
                Yii::$app->user->login($user);
            } else { // 注册
                if (isset($attributes['email']) && User::find()->where(['email' => $attributes['email']])->exists()) {
                    Yii::$app->getSession()->setFlash('error', [
                        Yii::t('app', "User with the same email as in {client} account already exists but isn't linked to it. Login using email first to link it.", ['client' => $client->getTitle()]),
                    ]);
                } else {
                    $password = Yii::$app->security->generateRandomString(6);
                    $user = new User([
                        'username' => $attributes['login'],
                        'email' => $attributes['email'],
                        'password' => $password,
                    ]);
                    $user->generateAuthKey();
                    $user->generatePasswordResetToken();
                    $transaction = $user->getDb()->beginTransaction();
                    if ($user->save()) {
                        $auth = new Auth([
                            'user_id' => $user->id,
                            'source' => $client->getId(),
                            'source_id' => (string)$attributes['id'],
                        ]);
                        if ($auth->save()) {
                            $transaction->commit();
                            Yii::$app->user->login($user);
                        } else {
                            print_r($auth->getErrors());
                        }
                    } else {
                        print_r($user->getErrors());
                    }
                }
            }
        } else { // 用户已经登陆
            if (!$auth) { // 添加验证提供商(向验证表中添加记录)
                $auth = new Auth([
                    'user_id' => Yii::$app->user->id,
                    'source' => $client->getId(),
                    'source_id' => $attributes['id'],
                ]);
                $auth->save();
            }
        }
    }
}

当用户由外部服务验证通过后调用 successCallback 方法。通过 $client 实例我们可以检索收到的信息。
在我们的例子中,我们可以:

  • 若用户是访客,且在验证信息中找到该用户,则登录该用户。
  • 若用户是访客,却在验证信息中找不到该用户,则创建一个新用户,并添加记录到验证表中,然后登录。
  • 若用户已登录,却在验证信息中找不到该用户,则尝试额外账户建立连接(将该用户的数据保存到验证表中)。

注意:不同的验证客户端在验证通过时可能需要不同的方法。例如:Twitter 不返回用户电子邮件,
所以你需要单独处理此种情况。

验证客户端基本结构

尽管每个客户端不尽相同,但它们都共享同一个基础接口 [[yii\authclient\ClientInterface]],
该接口包含了通用的 API。

每个客户端都有一些描述性的数据,分别用于不同的目的:

  • id - 唯一客户 ID,用于与其它客户端区分,它可以用于 URL 和日志等。
  • name - 外部验证提供商名称,与该客户端匹配。不同的客户端可以使用相同的名称,
    即它们指的是同一个外部验证提供商。
    例如:谷歌 Google 客户端和 Google Hybrid 客户端有一个相同的名称 “google”。
    该树形可以用在数据库内部、CSS 样式中等等。
  • title - 用户友好的外部验证提供商名称,用于在验证客户端的视图层展示。

每个验证客户端都有不同的验证流程,但是它们都支持 getUserAttributes() 方法,
可在验证通过后调用。

该方法允许你获取外部用户账户的信息,如 ID、电子邮件账户、全名、首选语言等等。
注意:对于每个提供商,可用域的名称和存在性可能不同。

定义属性列表,用于通知外部验证提供商应当返回列表,根据不同的客户端类型:

  • [[yii\authclient\OpenId]]: 同时定义 requiredAttributesoptionalAttributes.
  • [[yii\authclient\OAuth1]] 和 [[yii\authclient\OAuth2]]: 定义 scope 域。注意,
    不同的提供商对于范围的格式定义可能不同。

提示:如果你正在使用若干个不同的客户端,你可以使用 [[yii\authclient\BaseClient::normalizeUserAttributeMap]] 统一返回属性的列表。

通过补充 API 调用获取额外数据

[[yii\authclient\OAuth1]] 和 [[yii\authclient\OAuth2]] 均提供了 api() 方法,
可以用于访问外部服务提供商的 REST API。然而该方法比较基础,可能并不足以访问
所有外部 API 功能。该方法主要用于取回外部用户账户数据。

要使用 API 调用,你需要根据 API 说明设置 [[yii\authclient\BaseOAuth::apiBaseUrl]]。
之后就可以调用 [[yii\authclient\BaseOAuth::api()]] 方法了:

use yii\authclient\OAuth2;

$client = new OAuth2;

// ...

$client->apiBaseUrl = 'https://www.googleapis.com/oauth2/v1';
$userInfo = $client->api('userinfo', 'GET');

向登录视图添加小部件

[[yii\authclient\widgets\AuthChoice]] 小部件用于视图中:

<?= yii\authclient\widgets\AuthChoice::widget([
     'baseAuthUrl' => ['site/auth'],
     'popupMode' => false,
]) ?>

最后需要在第三方登录的回调地址里填 http://www.xxx.com/site/auth?authclient=qq

具体可以参考本站源码~

0

1 条评论

  • @相濡々 发表于 2017-11-14 09:46

    填写QQ互联时的回调地址提示不能带问号呢??