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).
203 lines
10 KiB
PHP
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';
|
|
}
|
|
}
|