Files
wordpress-theme-aksrt/src/Identity/Provider/TencentProvider.php
T
kate522 8c5b1555c9 refactor: flatten src/AKSRT → src (drop theme-identifier dir)
Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
2026-08-02 22:06:06 +08:00

203 lines
10 KiB
PHP

<?php
/**
* Tencent Cloud FaceId provider.
*
* @package AKSRT
*/
namespace AKSRT\Identity\Provider;
use AKSRT\Identity\SettingsRuntime;
defined('ABSPATH') || exit;
final class TencentProvider implements ProviderInterface {
private const VERSION = '2018-03-01';
private const SERVICE = 'faceid';
private const ENDPOINT = 'faceid.tencentcloudapi.com';
public function key(): string { return 'tencent'; }
public function capabilities(): array { return ['personal' => true, 'enterprise' => true]; }
public function validate_config() {
$cfg = SettingsRuntime::get();
foreach (['secret_id', 'secret_key'] as $key) {
if (empty($cfg['credentials'][$key])) {
return new \WP_Error('aksrt_identity_tencent_credentials', __('腾讯云人脸核身凭证不完整', 'aksrt'));
}
}
if (empty($cfg['personal_flow'])) {
return new \WP_Error('aksrt_identity_tencent_rule', __('请配置腾讯云人脸核身 RuleId', 'aksrt'));
}
return true;
}
public function start_personal(array $request): ProviderResult {
// Ref: https://cloud.tencent.com/document/product/1007/31816
$cfg = SettingsRuntime::get();
$payload = [
'RuleId' => (string) $cfg['personal_flow'],
'TerminalType' => $this->terminal_type(),
'RedirectUrl' => esc_url_raw((string) ($request['return_url'] ?? '')),
'Extra' => (string) ($request['application_uuid'] ?? ''),
];
if (!empty($request['person_name'])) {
$payload['Name'] = (string) $request['person_name'];
}
if (!empty($request['id_number'])) {
$payload['IdCard'] = preg_replace('/\s+/', '', (string) $request['id_number']);
}
$response = $this->request('DetectAuth', $payload);
if (is_wp_error($response)) {
return ProviderResult::failed($response->get_error_code(), $response->get_error_message());
}
$data = is_array($response['Response'] ?? null) ? $response['Response'] : [];
if (!empty($data['Error'])) {
return ProviderResult::failed((string) ($data['Error']['Code'] ?? 'tencent_error'), (string) ($data['Error']['Message'] ?? ''));
}
$biz_token = sanitize_text_field((string) ($data['BizToken'] ?? ''));
$url = esc_url_raw((string) ($data['Url'] ?? ''));
if ($biz_token === '' || $url === '') {
return ProviderResult::failed('tencent_invalid_response', __('腾讯云未返回有效认证会话', 'aksrt'));
}
return ProviderResult::pending([
'provider_session_id' => $biz_token,
'request_id' => sanitize_text_field((string) ($data['RequestId'] ?? '')),
'redirect_url' => $url,
'result_code' => 'session_created',
'expires_at' => gmdate('Y-m-d H:i:s', time() + ((int) $cfg['session_ttl_minutes'] * MINUTE_IN_SECONDS)),
]);
}
public function start_enterprise(array $request): ProviderResult {
// Ref: https://cloud.tencent.com/document/product/866/129072
foreach (['company_name', 'company_code', 'person_name', 'id_number'] as $key) {
if (empty($request[$key])) {
return ProviderResult::failed('tencent_enterprise_fields', __('企业认证资料不完整', 'aksrt'));
}
}
$response = $this->request_service('ocr', 'ocr.tencentcloudapi.com', '2018-11-19', 'VerifyBizLicenseEnterprise4', [
'CreditCode' => preg_replace('/\s+/', '', (string) $request['company_code']),
'EntName' => (string) $request['company_name'],
'LrName' => (string) $request['person_name'],
'IdNum' => preg_replace('/\s+/', '', (string) $request['id_number']),
]);
if (is_wp_error($response)) {
return ProviderResult::failed($response->get_error_code(), $response->get_error_message());
}
$data = is_array($response['Response'] ?? null) ? $response['Response'] : [];
if (!empty($data['Error'])) {
return ProviderResult::failed((string) ($data['Error']['Code'] ?? 'tencent_ocr_error'), (string) ($data['Error']['Message'] ?? ''));
}
if (!$this->enterprise_passed($data)) {
return ProviderResult::failed('tencent_enterprise_mismatch', __('企业工商四要素或法人关系核验未通过', 'aksrt'));
}
return $this->start_personal($request);
}
public function query(array $application): ProviderResult {
// Ref: https://cloud.tencent.com/document/product/1007/41957
$response = $this->request('GetDetectInfoEnhanced', [
'BizToken' => (string) ($application['provider_session_id'] ?? ''),
'RuleId' => (string) ($application['provider_flow'] ?? ''),
'InfoType' => '0',
]);
if (is_wp_error($response)) {
return ProviderResult::pending(['result_code' => $response->get_error_code(), 'message' => __('认证结果暂未取回,请稍后重试', 'aksrt')]);
}
$data = is_array($response['Response'] ?? null) ? $response['Response'] : [];
if (!empty($data['Error'])) {
return ProviderResult::failed((string) ($data['Error']['Code'] ?? 'tencent_error'), (string) ($data['Error']['Message'] ?? ''));
}
$text = $data['Text'] ?? ($data['DetectInfo']['Text'] ?? []);
if (is_string($text)) {
$decoded_text = json_decode($text, true);
$text = is_array($decoded_text) ? $decoded_text : [];
} elseif (!is_array($text)) {
$text = [];
}
$result = (string) ($text['ErrCode'] ?? '');
$live_status = (string) ($text['LiveStatus'] ?? '');
$compare_status = (string) ($text['Comparestatus'] ?? '');
if ($result === '0' && ($live_status === '' || $live_status === '0') && ($compare_status === '' || $compare_status === '0')) {
return ProviderResult::passed(['request_id' => (string) ($data['RequestId'] ?? ''), 'result_code' => '0']);
}
$message = sanitize_text_field((string) ($text['ErrMsg'] ?? $text['LiveMsg'] ?? $text['Comparemsg'] ?? ''));
if ($result === '') {
return ProviderResult::pending(['result_code' => 'processing', 'message' => __('腾讯云仍在处理认证结果', 'aksrt')]);
}
return ProviderResult::failed($result, $message);
}
public function verify_callback(\WP_REST_Request $request): bool { unset($request); return false; }
public function parse_callback(\WP_REST_Request $request): ProviderResult { unset($request); return ProviderResult::unsupported(__('腾讯云人脸核身使用服务端主动查询,不接收结果回调', 'aksrt')); }
/** @return array<string,mixed>|\WP_Error */
private function request(string $action, array $payload) {
return $this->request_service(self::SERVICE, self::ENDPOINT, self::VERSION, $action, $payload);
}
/** @return array<string,mixed>|\WP_Error */
private function request_service(string $service, string $host, string $version, string $action, array $payload) {
$cfg = SettingsRuntime::get();
$endpoint = 'https://' . $host;
$timestamp = time();
$date = gmdate('Y-m-d', $timestamp);
$body = (string) wp_json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$canonical_headers = "content-type:application/json; charset=utf-8\nhost:{$host}\nx-tc-action:" . strtolower($action) . "\n";
$signed_headers = 'content-type;host;x-tc-action';
$canonical_request = "POST\n/\n\n{$canonical_headers}\n{$signed_headers}\n" . hash('sha256', $body);
$scope = $date . '/' . $service . '/tc3_request';
$string_to_sign = "TC3-HMAC-SHA256\n{$timestamp}\n{$scope}\n" . hash('sha256', $canonical_request);
$secret_key = (string) $cfg['credentials']['secret_key'];
$secret_date = hash_hmac('sha256', $date, 'TC3' . $secret_key, true);
$secret_service = hash_hmac('sha256', $service, $secret_date, true);
$secret_signing = hash_hmac('sha256', 'tc3_request', $secret_service, true);
$signature = hash_hmac('sha256', $string_to_sign, $secret_signing);
$authorization = 'TC3-HMAC-SHA256 Credential=' . $cfg['credentials']['secret_id'] . '/' . $scope . ', SignedHeaders=' . $signed_headers . ', Signature=' . $signature;
$headers = [
'Authorization' => $authorization,
'Content-Type' => 'application/json; charset=utf-8',
'Host' => $host,
'X-TC-Action' => $action,
'X-TC-Timestamp' => (string) $timestamp,
'X-TC-Version' => $version,
];
if ($service !== self::SERVICE && !empty($cfg['region'])) {
$headers['X-TC-Region'] = (string) $cfg['region'];
}
$response = wp_remote_post($endpoint, [
'timeout' => 20,
'headers' => $headers,
'body' => $body,
]);
if (is_wp_error($response)) {
return new \WP_Error('tencent_network_error', __('腾讯云接口请求失败', 'aksrt'));
}
$decoded = json_decode((string) wp_remote_retrieve_body($response), true);
return is_array($decoded) ? $decoded : new \WP_Error('tencent_invalid_json', __('腾讯云返回格式无效', 'aksrt'));
}
private function enterprise_passed(array $data): bool {
if ((string) ($data['StatusCode'] ?? '') !== '0' || (string) ($data['VerifyResult'] ?? '') !== '1') {
return false;
}
foreach (['IsCreditCodeConsistent', 'IsEntNameConsistent', 'IsLrNameConsistent', 'IsIdNumConsistent'] as $field) {
$value = $data[$field] ?? null;
if (!in_array($value, [true, 1, '1', 'true'], true)) {
return false;
}
}
$status = strtoupper((string) ($data['OperatingStatus'] ?? $data['RegStatus'] ?? ''));
return !in_array($status, ['吊销', '注销', 'REVOKED', 'CANCELLED'], true);
}
private function terminal_type(): string {
$ua = strtolower((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''));
return str_contains($ua, 'mobile') ? '1' : '2';
}
}