QQ 互聯(QQ Connect)採用 OAuth 2.0 授權碼模式。官方 PHP SDK 代碼陳舊、耦合度高。本文提供輕量、現代、可直接用於生產的 QqSdk 類別,涵蓋授權導向、Access Token 獲取、OpenID 解析、用戶資料拉取、錯誤處理與安全最佳實務。
<?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;
}
}
?>
<?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;
?>
<?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 Token | access_token, expires_in, refresh_token | access_token 有效期通常 3 個月,refresh_token 用於刷新 |
| OpenID | client_id (appid), openid | 用戶在該應用下的唯一標識 |
| 用戶資料 | nickname, figureurl_qq_1/2, gender, province, city, year | figureurl_qq_2 為 100x100 大頭像 |