首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP QQ 第三方登入 SDK 完整實作:OAuth 2.0 流程、OpenID 獲取、用戶資料同步

     編輯:PHP 相關知識

QQ 互聯(QQ Connect)採用 OAuth 2.0 授權碼模式。官方 PHP SDK 代碼陳舊、耦合度高。本文提供輕量、現代、可直接用於生產的 QqSdk 類別,涵蓋授權導向、Access Token 獲取、OpenID 解析、用戶資料拉取、錯誤處理與安全最佳實務。

核心類別:QqSdk.php

<?php
namespace App\Services;

class QqSdk {
    private string $appId;
    private string $appSecret;
    private string $redirectUri;
    private const AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize';
    private const TOKEN_URL = 'https://graph.qq.com/oauth2.0/token';
    private const OPENID_URL = 'https://graph.qq.com/oauth2.0/me';
    private const USER_INFO_URL = 'https://graph.qq.com/user/get_user_info';

    public function __construct(string $appId, string $appSecret, string $redirectUri) {
        $this->appId = $appId;
        $this->appSecret = $appSecret;
        $this->redirectUri = $redirectUri;
    }

    /** 獲取授權頁面 URL */
    public function getAuthUrl(string $state = ''): string {
        $params = [
            'response_type' => 'code',
            'client_id' => $this->appId,
            'redirect_uri' => $this->redirectUri,
            'scope' => 'get_user_info', // 可加多 scope,逗號分隔
        ];
        if ($state) $params['state'] = $state;
        return self::AUTH_URL . '?' . http_build_query($params);
    }

    /** 用授權碼換取 Access Token */
    public function getAccessToken(string $code): array {
        $params = [
            'grant_type' => 'authorization_code',
            'client_id' => $this->appId,
            'client_secret' => $this->appSecret,
            'code' => $code,
            'redirect_uri' => $this->redirectUri,
        ];
        $url = self::TOKEN_URL . '?' . http_build_query($params);
        $response = $this->httpGet($url);
        parse_str($response, $result);
        if (empty($result['access_token'])) {
            throw new \Exception('獲取 Access Token 失敗: ' . $response);
        }
        return $result; // ['access_token'=>..., 'expires_in'=>..., 'refresh_token'=>...]
    }

    /** 獲取用戶 OpenID */
    public function getOpenId(string $accessToken): array {
        $url = self::OPENID_URL . '?access_token=' . $accessToken;
        $response = $this->httpGet($url);
        // 回應格式:callback( {"client_id":"...","openid":"..."} );
        if (strpos($response, 'callback') !== false) {
            $lpos = strpos($response, '(');
            $rpos = strrpos($response, ')');
            $response = substr($response, $lpos + 1, $rpos - $lpos - 1);
        }
        $data = json_decode($response, true);
        if (empty($data['openid'])) {
            throw new \Exception('獲取 OpenID 失敗: ' . $response);
        }
        return $data; // ['client_id'=>..., 'openid'=>...]
    }

    /** 獲取用戶詳細資料 */
    public function getUserInfo(string $accessToken, string $openId): array {
        $params = [
            'access_token' => $accessToken,
            'oauth_consumer_key' => $this->appId,
            'openid' => $openId,
            'format' => 'json',
        ];
        $url = self::USER_INFO_URL . '?' . http_build_query($params);
        $response = $this->httpGet($url);
        $data = json_decode($response, true);
        if (($data['ret'] ?? -1) !== 0) {
            throw new \Exception('獲取用戶資料失敗: ' . ($data['msg'] ?? $response));
        }
        return $data; // 包含 nickname, figureurl, gender, province, city, year 等
    }

    /** 統一 HTTP GET 請求(含超時、錯誤處理) */
    private function httpGet(string $url): string {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true, // 生產環境必須驗證
            CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; QqSdk/1.0)',
        ]);
        $result = curl_exec($ch);
        $errno = curl_errno($ch);
        $error = curl_error($ch);
        curl_close($ch);
        if ($errno) {
            throw new \Exception("HTTP 請求失敗 ($errno): $error");
        }
        return $result;
    }
}
?>

使用流程範例

1. 登入頁:導向 QQ 授權

<?php
require 'vendor/autoload.php';
use App\Services\QqSdk;

$qq = new QqSdk('YOUR_APP_ID', 'YOUR_APP_SECRET', 'https://yoursite.com/qq/callback');
// 可選:生成隨機 state 防 CSRF,存入 Session
$state = bin2hex(random_bytes(16));
$_SESSION['qq_oauth_state'] = $state;

$authUrl = $qq->getAuthUrl($state);
header('Location: ' . $authUrl);
exit;
?>

2. 回調頁:處理授權回調

<?php
require 'vendor/autoload.php';
use App\Services\QqSdk;

session_start();
$qq = new QqSdk('YOUR_APP_ID', 'YOUR_APP_SECRET', 'https://yoursite.com/qq/callback');

// 1. 驗證 state 防 CSRF
if (empty($_GET['code']) || empty($_GET['state']) || $_GET['state'] !== ($_SESSION['qq_oauth_state'] ?? '')) {
    exit('非法請求:state 驗證失敗');
}
unset($_SESSION['qq_oauth_state']);

try {
    // 2. 換取 Access Token
    $token = $qq->getAccessToken($_GET['code']);
    // 3. 獲取 OpenID
    $openIdData = $qq->getOpenId($token['access_token']);
    // 4. 獲取用戶資料
    $userInfo = $qq->getUserInfo($token['access_token'], $openIdData['openid']);

    // 5. 業務邏輯:綁定/註冊/登入
    $user = findOrCreateUser([
        'provider' => 'qq',
        'openid' => $openIdData['openid'],
        'nickname' => $userInfo['nickname'],
        'avatar' => $userInfo['figureurl_qq_2'] ?? $userInfo['figureurl_2'],
        'gender' => $userInfo['gender'],
        'location' => ($userInfo['province'] ?? '') . ($userInfo['city'] ?? ''),
        'access_token' => $token['access_token'],
        'expires_at' => time() + (int)$token['expires_in'],
    ]);

    // 6. 登入會話
    $_SESSION['user_id'] = $user->id;
    header('Location: /dashboard');
} catch (Exception $e) {
    logError('QQ 登入失敗: ' . $e->getMessage());
    echo '登入失敗,請稍後重試: ' . $e->getMessage();
}
?>

關鍵資料結構對照

步驟關鍵回傳欄位說明
Access Tokenaccess_token, expires_in, refresh_tokenaccess_token 有效期通常 3 個月,refresh_token 用於刷新
OpenIDclient_id (appid), openid用戶在該應用下的唯一標識
用戶資料nickname, figureurl_qq_1/2, gender, province, city, yearfigureurl_qq_2 為 100x100 大頭像

安全最佳實務

  1. State 參數:每次授權生成隨機 `state` 存 Session,回調驗證,防 CSRF。
  2. HTTPS 強制:`redirect_uri` 必須 HTTPS,生產環境全站 SSL。
  3. Token 儲存:`access_token`、`refresh_token` 加密儲存資料庫,定期刷新。
  4. 錯誤碼處理:常見 `100010`(參數錯誤)、`100016`(redirect_uri 不匹配)、`100019`(code 已用過/過期)。
  5. 最小權限:`scope` 僅請求必要權限(`get_user_info`、`get_simple_userinfo`)。

現代框架整合建議

  • Laravel:使用 `laravel/socialite` 或 `socialiteproviders/qq`,零配置支援。
  • Symfony:`knpuniversity/oauth2-client-bundle` + `league/oauth2-qq`。
  • ThinkPHP / Yii:對應擴展包或自行封裝為組件/服務。

測試檢查清單

  • [ ] 授權頁正常跳轉、顯示應用名稱
  • [ ] 同意授權後正確回調、code 獲取成功
  • [ ] Access Token / OpenID / 用戶資料三步驟全通
  • [ ] 新用戶自動註冊、舊用戶自動登入、綁定邏輯正確
  • [ ] 取消授權、拒絕授權、網路異常、Token 過期等異常流程友善提示
  • [ ] HTTPS 環境下全流程無混合內容警告
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved