中國電信天翼開放平台提供短信驗證碼下發能力,流程包含:獲取 access_token → 申請信任碼 → 下發驗證碼。本文提供完整的 PHP 實作代碼,涵蓋簽名生成、HMAC-SHA1 加密、cURL 請求封裝與錯誤處理。(註:平台政策可能變更,請以最新官方文檔為準)
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 1.獲取 │────▶│ 2.申請信任碼 │────▶│ 3.下發驗證碼 │
│ access_token│ │ (token) │ │ (send) │
└─────────────┘ └──────────────┘ └─────────────┘
POST form GET + 簽名 POST + 簽名
OAuth2 client_cred HMAC-SHA1 HMAC-SHA1
<?php
/**
* 天翼開放平台短信客戶端
* 文檔參考:https://open.189.cn/
*/
class CtccSmsClient {
private $appId;
private $appSecret;
private $accessToken = '';
private $tokenExpire = 0;
// API 端點
const URL_TOKEN = 'https://oauth.api.189.cn/emp/oauth2/v2/access_token';
const URL_RANDCODE_TOKEN = 'https://api.189.cn/v2/dm/randcode/token';
const URL_RANDCODE_SEND = 'https://api.189.cn/v2/dm/randcode/send';
public function __construct($appId, $appSecret) {
$this->appId = $appId;
$this->appSecret = $appSecret;
}
/** 獲取 access_token(client_credentials 模式) */
private function ensureAccessToken() {
if ($this->accessToken && time() < $this->tokenExpire - 300) {
return true; // 仍在有效期內(提前 5 分鐘刷新)
}
$data = http_build_query([
'app_id' => $this->appId,
'app_secret' => $this->appSecret,
'grant_type' => 'client_credentials',
]);
$ch = curl_init(self::URL_TOKEN);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_TIMEOUT => 10,
]);
$resp = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
throw new Exception("獲取 access_token 失敗: $err");
}
$json = json_decode($resp, true);
if (empty($json['access_token'])) {
throw new Exception("access_token 回應異常: $resp");
}
$this->accessToken = $json['access_token'];
$this->tokenExpire = time() + ($json['expires_in'] ?? 3600);
return true;
}
/** 產生簽名:參數按 key 排序 → 拼接 → HMAC-SHA1 → Base64 → URL編碼 */
private function buildSign(array $params, $secret = null) {
$secret = $secret ?? $this->appSecret;
ksort($params);
$plaintext = implode('&', $params);
$hash = hash_hmac('sha1', $plaintext, $secret, true);
return rawurlencode(base64_encode($hash));
}
/** 步驟 2:申請信任碼 */
private function getRandcodeToken() {
$this->ensureAccessToken();
$timestamp = date('Y-m-d H:i:s');
$params = [
'app_id' => "app_id={$this->appId}",
'access_token' => "access_token={$this->accessToken}",
'timestamp' => "timestamp={$timestamp}",
];
$sign = $this->buildSign($params);
$url = self::URL_RANDCODE_TOKEN . '?' . http_build_query([
'app_id' => $this->appId,
'access_token' => $this->accessToken,
'timestamp' => $timestamp,
'sign' => $sign,
]);
$resp = @file_get_contents($url);
$json = json_decode($resp, true);
if (empty($json['token'])) {
throw new Exception("獲取信任碼失敗: $resp");
}
return $json['token'];
}
/** 步驟 3:下發驗證碼 */
public function sendVerificationCode($phone, $callbackUrl, $expMinutes = 2) {
$randcodeToken = $this->getRandcodeToken();
$timestamp = date('Y-m-d H:i:s');
$params = [
'app_id' => "app_id={$this->appId}",
'access_token' => "access_token={$this->accessToken}",
'token' => "token={$randcodeToken}",
'phone' => "phone={$phone}",
'url' => "url={$callbackUrl}",
'exp_time' => "exp_time={$expMinutes}",
'timestamp' => "timestamp={$timestamp}",
];
$sign = $this->buildSign($params);
$postData = http_build_query([
'app_id' => $this->appId,
'access_token' => $this->accessToken,
'token' => $randcodeToken,
'phone' => $phone,
'url' => $callbackUrl,
'exp_time' => $expMinutes,
'timestamp' => $timestamp,
'sign' => $sign,
]);
$ch = curl_init(self::URL_RANDCODE_SEND);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
throw new Exception("發送請求失敗: $err");
}
return json_decode($resp, true);
}
}
// ===== 使用範例 =====
try {
$client = new CtccSmsClient('YOUR_APP_ID', 'YOUR_APP_SECRET');
$result = $client->sendVerificationCode(
'15091421612', // 手機號
'https://wx.example.com/callback', // 回調地址
2 // 有效期(分鐘)
);
var_dump($result);
// 成功範例: ['res_code'=>0, 'res_message'=>'成功', ...]
} catch (Exception $e) {
error_log('[CtccSms] ' . $e->getMessage());
// 業務降級:記錄日誌、嘗試備用通道、通知運營
}
?>
app_id=xxx),簽名時需保持此格式並按 key 排序。hash_hmac(..., true) 產生原始二進制,再 Base64,最後 rawurlencode。Y-m-d H:i:s(含空格),URL 傳輸時會自動編碼為 %20 或 +。CURLOPT_SSL_VERIFYPEER,生產環境建議配置 CA 憑證啟用驗證。access_token 與 randcode_token,避免併發重複申請。url 參數指定地址,需驗證來源 IP 與簽名。appSecret 僅存於環境變數或加密配置中心,嚴禁寫入代碼倉庫。| res_code | 含義 | 處理建議 |
|---|---|---|
| 0 | 成功 | — |
| 400 | 參數錯誤 | 檢查必填欄位、格式、簽名 |
| 401 | 未授權/Token 失效 | 強制刷新 access_token 重試 |
| 403 | 頻率/額度超限 | 觸發熔斷、走備用通道、告警運營 |
| 500 | 平台內部錯誤 | 記錄日誌、延遲重試、聯繫平台技術支援 |
將上述類封裝為 Composer 套件或服務容器服務,即可在 Laravel/ThinkPHP/Symfony 等框架中優雅注入使用,實現穩健的短信驗證碼下發能力。