true, 'enterprise' => true]; } public function validate_config() { $cfg = SettingsRuntime::get(); foreach (['access_key_id', 'access_key_secret'] as $key) { if (empty($cfg['credentials'][$key])) { return new \WP_Error('aksrt_identity_aliyun_credentials', __('阿里云实人认证凭证不完整', 'aksrt')); } } if (empty($cfg['personal_flow'])) { return new \WP_Error('aksrt_identity_aliyun_scene', __('请配置阿里云实人认证 SceneId', 'aksrt')); } if (!empty($cfg['enterprise_flow']) && preg_match('/[^A-Za-z0-9_-]/', (string) $cfg['enterprise_flow'])) { return new \WP_Error('aksrt_identity_aliyun_enterprise_scene', __('阿里云企业认证 SceneCode 格式无效', 'aksrt')); } return true; } public function start_personal(array $request): ProviderResult { return $this->init_face_verify($request); } public function start_enterprise(array $request): ProviderResult { $cfg = SettingsRuntime::get(); // Ref: https://help.aliyun.com/zh/id-verification/enterprise-factor-validation-v2 foreach (['company_name', 'company_code', 'person_name', 'id_number'] as $key) { if (empty($request[$key])) { return ProviderResult::failed('aliyun_enterprise_fields', __('企业认证资料不完整', 'aksrt')); } } $response = $this->rpc('cloudauth.aliyuncs.com', '2022-11-25', 'EntElementVerifyV2', [ 'SceneCode' => (string) ($cfg['enterprise_flow'] ?: $cfg['personal_flow']), 'MerchantBizId' => preg_replace('/[^A-Za-z0-9]/', '', (string) $request['application_uuid']), 'MerchantUserId' => preg_replace('/[^A-Za-z0-9]/', '', 'U' . (string) ($request['user_id'] ?? '0')), 'UserAuthorization' => '1', 'InfoVerifyType' => 'ENT_4META', 'LicenseNo' => preg_replace('/\s+/', '', (string) $request['company_code']), 'EntName' => (string) $request['company_name'], 'LegalPersonName' => (string) $request['person_name'], 'LegalPersonCertNo' => preg_replace('/\s+/', '', (string) $request['id_number']), ]); if (is_wp_error($response)) { return ProviderResult::failed($response->get_error_code(), $response->get_error_message()); } if (!$this->enterprise_passed($response)) { return ProviderResult::failed( sanitize_text_field((string) ($response['Code'] ?? $response['ResultCode'] ?? 'enterprise_failed')), sanitize_text_field((string) ($response['Message'] ?? __('企业要素或法人关系核验未通过', 'aksrt'))) ); } return $this->init_face_verify($request); } public function query(array $application): ProviderResult { // Ref: https://api.aliyun.com/document/Cloudauth/2019-03-07/DescribeFaceVerify $response = $this->rpc('cloudauth.aliyuncs.com', '2019-03-07', 'DescribeFaceVerify', [ 'SceneId' => (string) ($application['provider_flow'] ?? ''), 'CertifyId' => (string) ($application['provider_session_id'] ?? ''), ]); if (is_wp_error($response)) { return ProviderResult::pending(['result_code' => $response->get_error_code(), 'message' => __('认证结果暂未取回,请稍后重试', 'aksrt')]); } $code = sanitize_text_field((string) ($response['Code'] ?? '')); if ($code === '424') { return ProviderResult::pending([ 'request_id' => sanitize_text_field((string) ($response['RequestId'] ?? '')), 'result_code' => $code, 'message' => __('用户尚未在阿里云核身页面提交认证,请完成后再刷新结果', 'aksrt'), ]); } if ($code !== '' && $code !== '200') { return ProviderResult::failed($code, sanitize_text_field((string) ($response['Message'] ?? ''))); } $object = $this->normalize_result_object($response['ResultObject'] ?? []); $passed = $object['Passed'] ?? $object['passed'] ?? $response['Passed'] ?? null; $success = $object['Success'] ?? $object['success'] ?? $response['Success'] ?? null; if ((string) ($response['Code'] ?? '') === '200' && ($success === true || $success === 'True' || $success === 'true') && $passed === 'T') { return ProviderResult::passed(['request_id' => (string) ($response['RequestId'] ?? ''), 'result_code' => 'passed']); } $status = strtoupper((string) ($object['SubCode'] ?? $object['Status'] ?? $object['subCode'] ?? $object['status'] ?? '')); if ((string) ($response['Code'] ?? '') === '200' && $status === '200') { return ProviderResult::passed(['request_id' => (string) ($response['RequestId'] ?? ''), 'result_code' => 'passed']); } $message = $object['MaterialInfo'] ?? $object['Message'] ?? $object['message'] ?? __('阿里云仍在处理认证结果', 'aksrt'); if ($passed === 'F') { $debug = $this->response_debug_summary($response, $object, ['passed' => 'F']); $this->log_debug('DescribeFaceVerify not passed', $debug); return ProviderResult::failed($status ?: 'not_passed', sanitize_text_field((string) $message)); } if ($object === []) { $debug = $this->response_debug_summary($response, $object); $this->log_debug('DescribeFaceVerify empty ResultObject', $debug); return ProviderResult::pending([ 'request_id' => sanitize_text_field((string) ($response['RequestId'] ?? '')), 'result_code' => 'processing', 'message' => sprintf(__('阿里云已返回成功状态,认证明细仍在生成中:%s', 'aksrt'), $debug), ]); } if ($status === '' || in_array($status, ['PROCESSING', 'INIT', 'PENDING'], true)) { return ProviderResult::pending(['result_code' => $status ?: 'processing', 'message' => sanitize_text_field((string) $message)]); } return ProviderResult::failed($status, sanitize_text_field((string) $message)); } private function is_public_https_url(string $url): bool { if ($url === '') { return false; } $parts = wp_parse_url($url); if (!is_array($parts) || strtolower((string) ($parts['scheme'] ?? '')) !== 'https') { return false; } $host = strtolower((string) ($parts['host'] ?? '')); return $host !== '' && !in_array($host, ['localhost', '127.0.0.1', '::1'], true) && !str_ends_with($host, '.local'); } public function verify_callback(\WP_REST_Request $request): bool { $token = sanitize_text_field((string) ($request->get_param('CallbackToken') ?: $request->get_param('callbackToken'))); $session = sanitize_text_field((string) ($request->get_param('certifyId') ?: $request->get_param('CertifyId'))); if ($token === '' || $session === '') { return false; } global $wpdb; $table = \AKSRT\Core\DBMigrator::table('identity_application'); $uuid = (string) $wpdb->get_var($wpdb->prepare( "SELECT application_uuid FROM {$table} WHERE provider = 'aliyun' AND provider_session_id = %s LIMIT 1", $session )); return $uuid !== '' && hash_equals(hash_hmac('sha256', $uuid, wp_salt('nonce')), $token); } public function parse_callback(\WP_REST_Request $request): ProviderResult { $session = sanitize_text_field((string) ($request->get_param('certifyId') ?: $request->get_param('CertifyId'))); $outer_order = sanitize_text_field((string) ($request->get_param('outerOrderNo') ?: $request->get_param('OuterOrderNo'))); return ProviderResult::pending(['provider_session_id' => $session, 'outer_order_no' => $outer_order, 'result_code' => 'callback_received']); } private function init_face_verify(array $request): ProviderResult { // Ref: https://api.aliyun.com/document/Cloudauth/2019-03-07/InitFaceVerify $cfg = SettingsRuntime::get(); $callback_token = hash_hmac('sha256', (string) ($request['application_uuid'] ?? ''), wp_salt('nonce')); $params = [ 'SceneId' => (string) $cfg['personal_flow'], 'OuterOrderNo' => (string) ($request['application_uuid'] ?? wp_generate_uuid4()), 'ProductCode' => (string) $cfg['product_code'], 'CertType' => 'IDENTITY_CARD', 'CertName' => (string) ($request['person_name'] ?? ''), 'CertNo' => preg_replace('/\s+/', '', (string) ($request['id_number'] ?? '')), ]; $meta_info = trim((string) ($request['meta_info'] ?? '')); if ($meta_info !== '') { $params['MetaInfo'] = $meta_info; } $return_url = esc_url_raw((string) ($request['return_url'] ?? '')); if ($this->is_public_https_url($return_url)) { $params['ReturnUrl'] = $return_url; } $callback_url = esc_url_raw(rest_url('aksrt/v1/identity/callback/aliyun')); if ($this->is_public_https_url($callback_url)) { $params['CallbackUrl'] = $callback_url; $params['CallbackToken'] = $callback_token; } $response = $this->rpc('cloudauth.aliyuncs.com', '2019-03-07', 'InitFaceVerify', $params); if (is_wp_error($response)) { return ProviderResult::failed($response->get_error_code(), $response->get_error_message()); } $code = sanitize_text_field((string) ($response['Code'] ?? '')); if ($code !== '' && $code !== '200') { return ProviderResult::failed($code, sanitize_text_field((string) ($response['Message'] ?? ''))); } $object = $this->normalize_result_object($response['ResultObject'] ?? []); $certify_id = sanitize_text_field((string) $this->first_response_value($response, [ ['ResultObject', 'CertifyId'], ['ResultObject', 'certifyId'], ['CertifyId'], ['certifyId'], ['Data', 'CertifyId'], ['Data', 'certifyId'], ])); $url = esc_url_raw((string) $this->first_response_value(array_merge($response, ['ResultObject' => $object]), [ ['ResultObject', 'CertifyUrl'], ['ResultObject', 'certifyUrl'], ['ResultObject', 'Url'], ['ResultObject', 'url'], ['CertifyUrl'], ['certifyUrl'], ['Url'], ['url'], ['Data', 'CertifyUrl'], ['Data', 'certifyUrl'], ['Data', 'Url'], ['Data', 'url'], ])); if ($certify_id === '') { $debug = $this->response_debug_summary($response, $object, [ 'has_certify_id' => false, 'has_certify_url' => $url !== '', ]); $this->log_debug('InitFaceVerify missing CertifyId', $debug); return ProviderResult::failed('aliyun_invalid_response', sprintf( __('阿里云未返回有效认证会话:%s', 'aksrt'), $debug )); } if ($url === '') { $debug = $this->response_debug_summary($response, $object, [ 'has_certify_id' => true, 'has_certify_url' => false, ]); $this->log_debug('InitFaceVerify returned CertifyId without URL', $debug); return ProviderResult::failed('aliyun_missing_certify_url', sprintf( __('阿里云只返回了 CertifyId,未返回可打开的核身链接。请确认 SceneId/ProductCode 对应的是支持 H5 链接的实人认证方案;调试信息:%s', 'aksrt'), $debug )); } return ProviderResult::pending([ 'provider_session_id' => $certify_id, 'request_id' => sanitize_text_field((string) ($response['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)), ]); } /** @return array|\WP_Error */ private function rpc(string $host, string $version, string $action, array $business) { $cfg = SettingsRuntime::get(); $endpoint = 'https://' . $host . '/'; // The official PHP SDK uses ACS3-HMAC-SHA256 by default. This zero-dependency // fallback deliberately uses the documented RPC v2 signature until the SDK // packages can be installed and locked in this deployment. $params = array_merge([ 'Format' => 'JSON', 'Version' => $version, 'AccessKeyId' => (string) $cfg['credentials']['access_key_id'], 'SignatureMethod' => 'HMAC-SHA1', 'SignatureNonce' => wp_generate_uuid4(), 'SignatureVersion' => '1.0', 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), 'Action' => $action, ], $business); ksort($params); $canonical = http_build_query($params, '', '&', PHP_QUERY_RFC3986); $string_to_sign = 'POST&%2F&' . rawurlencode($canonical); $params['Signature'] = base64_encode(hash_hmac('sha1', $string_to_sign, (string) $cfg['credentials']['access_key_secret'] . '&', true)); $response = wp_remote_post($endpoint, [ 'timeout' => 20, 'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'], 'body' => $params, ]); if (is_wp_error($response)) { return new \WP_Error('aliyun_network_error', __('阿里云接口请求失败', 'aksrt')); } $status_code = (int) wp_remote_retrieve_response_code($response); $body = (string) wp_remote_retrieve_body($response); $decoded = json_decode($body, true); if (!is_array($decoded)) { $this->log_debug('Aliyun invalid JSON response', [ 'http_status' => $status_code, 'body_preview' => $this->truncate_debug($body, 800), ]); return new \WP_Error('aliyun_invalid_json', __('阿里云返回格式无效', 'aksrt')); } if (defined('WP_DEBUG') && WP_DEBUG) { $this->log_debug('Aliyun RPC response', [ 'action' => $action, 'http_status' => $status_code, 'summary' => $this->response_debug_summary($decoded), ]); } return $decoded; } private function enterprise_passed(array $response): bool { $result = is_array($response['Result'] ?? null) ? $response['Result'] : []; return (string) ($response['Code'] ?? '') === 'Success' && (string) ($result['BizCode'] ?? '') === '1' && (string) ($result['Status'] ?? '') === '1'; } /** @param mixed $value @return array */ private function normalize_result_object($value): array { if (is_array($value)) { return $value; } if (is_string($value) && $value !== '') { $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } return []; } /** @param array $response @param array> $paths */ private function first_response_value(array $response, array $paths): string { foreach ($paths as $path) { $value = $response; foreach ($path as $segment) { if (!is_array($value) || !array_key_exists($segment, $value)) { $value = null; break; } $value = $value[$segment]; } if (is_scalar($value) && (string) $value !== '') { return (string) $value; } } return ''; } /** @param array $response @param array $object @param array $extra */ private function response_debug_summary(array $response, array $object = [], array $extra = []): string { $summary = array_merge([ 'code' => (string) ($response['Code'] ?? ''), 'message' => (string) ($response['Message'] ?? ''), 'request_id' => (string) ($response['RequestId'] ?? ''), 'top_keys' => implode(',', array_slice(array_keys($response), 0, 20)), 'result_keys' => $object !== [] ? implode(',', array_slice(array_keys($object), 0, 20)) : '', ], $extra); return wp_json_encode($summary, JSON_UNESCAPED_UNICODE) ?: ''; } /** @param mixed $data */ private function log_debug(string $message, $data): void { if (!defined('WP_DEBUG') || !WP_DEBUG) { return; } error_log('[AKSRT Identity Aliyun] ' . $message . ': ' . (is_string($data) ? $data : (wp_json_encode($data, JSON_UNESCAPED_UNICODE) ?: ''))); } private function truncate_debug(string $value, int $length): string { return function_exists('mb_substr') ? mb_substr($value, 0, $length) : substr($value, 0, $length); } }