Files
wordpress-theme-aksrt/src/AKSRT/Identity/Provider/AliyunProvider.php
T
2026-08-01 22:28:03 +08:00

333 lines
16 KiB
PHP

<?php
/**
* Alibaba Cloud Cloudauth provider.
*
* @package AKSRT
*/
namespace AKSRT\Identity\Provider;
use AKSRT\Identity\SettingsRuntime;
defined('ABSPATH') || exit;
final class AliyunProvider implements ProviderInterface {
public function key(): string { return 'aliyun'; }
public function capabilities(): array { return ['personal' => 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'] ?? '')));
}
$passed = $response['ResultObject']['Passed'] ?? $response['Passed'] ?? null;
$success = $response['ResultObject']['Success'] ?? $response['Success'] ?? null;
if ((string) ($response['Code'] ?? '') === '200' && ($success === true || $success === 'True') && $passed === 'T') {
return ProviderResult::passed(['request_id' => (string) ($response['RequestId'] ?? ''), 'result_code' => 'passed']);
}
$status = strtoupper((string) ($response['ResultObject']['SubCode'] ?? $response['ResultObject']['Status'] ?? ''));
if ($status === '' || in_array($status, ['PROCESSING', 'INIT', 'PENDING'], true)) {
return ProviderResult::pending(['result_code' => $status ?: 'processing', 'message' => __('阿里云仍在处理认证结果', 'aksrt')]);
}
return ProviderResult::failed($status, sanitize_text_field((string) ($response['ResultObject']['MaterialInfo'] ?? __('实人认证未通过', 'aksrt'))));
}
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<string,mixed>|\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<string,mixed> */
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<string,mixed> $response @param array<int,array<int,string>> $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<string,mixed> $response @param array<string,mixed> $object @param array<string,mixed> $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);
}
}