233 行
7.7 KiB
PHP
233 行
7.7 KiB
PHP
<?php
|
|
if (!defined('IN_APP')) {
|
|
http_response_code(403);
|
|
exit('Forbidden');
|
|
}
|
|
function rnKey()
|
|
{
|
|
static $k = null;
|
|
if ($k === null) {
|
|
$seed = defined('SITE_KEY') ? (string) SITE_KEY : 'free-cloud-default-seed';
|
|
$k = hash('sha256', $seed . ':realname:v2', true);
|
|
}
|
|
return $k;
|
|
}
|
|
function rnEncrypt($plain)
|
|
{
|
|
if ($plain === '' || $plain === null) {
|
|
return '';
|
|
}
|
|
$iv = random_bytes(16);
|
|
$c = openssl_encrypt((string) $plain, 'AES-256-CBC', rnKey(), OPENSSL_RAW_DATA, $iv);
|
|
if ($c === false) {
|
|
return '';
|
|
}
|
|
return base64_encode($iv . $c);
|
|
}
|
|
function rnDecrypt($data)
|
|
{
|
|
if (empty($data)) {
|
|
return '';
|
|
}
|
|
$raw = base64_decode((string) $data, true);
|
|
if ($raw === false || strlen($raw) < 17) {
|
|
return '';
|
|
}
|
|
$iv = substr($raw, 0, 16);
|
|
$c = substr($raw, 16);
|
|
$p = openssl_decrypt($c, 'AES-256-CBC', rnKey(), OPENSSL_RAW_DATA, $iv);
|
|
return $p === false ? '' : $p;
|
|
}
|
|
function validateChineseId($id)
|
|
{
|
|
$id = strtoupper(trim((string) $id));
|
|
if (!preg_match('/^\d{17}[\dX]$/', $id)) {
|
|
return false;
|
|
}
|
|
$weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
|
$codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
|
$sum = 0;
|
|
for ($i = 0; $i < 17; $i++) {
|
|
$sum += (int) $id[$i] * $weights[$i];
|
|
}
|
|
if ($codes[$sum % 11] !== $id[17]) {
|
|
return false;
|
|
}
|
|
$yy = (int) substr($id, 6, 4);
|
|
$mm = (int) substr($id, 10, 2);
|
|
$dd = (int) substr($id, 12, 2);
|
|
if ($yy < 1900 || $yy > (int) date('Y')) {
|
|
return false;
|
|
}
|
|
if (!checkdate($mm, $dd, $yy)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
function validateRealName($name)
|
|
{
|
|
$name = trim((string) $name);
|
|
if ($name === '') {
|
|
return false;
|
|
}
|
|
if (mb_strlen($name, 'UTF-8') < 2 || mb_strlen($name, 'UTF-8') > 20) {
|
|
return false;
|
|
}
|
|
if (!preg_match('/^[\x{4e00}-\x{9fa5}a-zA-Z·\s]+$/u', $name)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
function verifyRealNameTwoFactor($name, $idno)
|
|
{
|
|
if (!validateRealName($name)) {
|
|
return ['ok' => false, 'error' => '姓名格式不合法(仅支持 2-20 位中文/字母)'];
|
|
}
|
|
if (!validateChineseId($idno)) {
|
|
return ['ok' => false, 'error' => '身份证号校验失败,请核对号码与最后一位校验位'];
|
|
}
|
|
if (getSetting('realname_mode', 'local') === 'sdk') {
|
|
return sdkVerifyRealName($name, $idno);
|
|
}
|
|
return ['ok' => true];
|
|
}
|
|
function sdkVerifyRealName($name, $idno)
|
|
{
|
|
$url = trim((string) getSetting('realname_api_url', ''));
|
|
if ($url === '') {
|
|
$url = '';
|
|
}
|
|
$appcode = trim((string) getSetting('realname_api_key', ''));
|
|
if ($appcode === '') {
|
|
return ['ok' => false, 'error' => '实名 SDK 未配置:请在后台「设置 → 实名认证配置」将接口密钥填写为阿里云市场的 AppCode'];
|
|
}
|
|
$bodys = http_build_query([
|
|
'idNo' => $idno,
|
|
'name' => $name,
|
|
], '', '&', PHP_QUERY_RFC3986);
|
|
$headers = [
|
|
'Authorization: APPCODE ' . $appcode,
|
|
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
|
|
];
|
|
$resp = null;
|
|
$httpCode = 0;
|
|
$curlErr = '';
|
|
if (function_exists('curl_init')) {
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_POSTFIELDS => $bodys,
|
|
CURLOPT_TIMEOUT => 15,
|
|
CURLOPT_CONNECTTIMEOUT => 10,
|
|
CURLOPT_FAILONERROR => false,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => 0,
|
|
]);
|
|
$raw = curl_exec($ch);
|
|
if ($raw !== false) {
|
|
$resp = (string) $raw;
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
}
|
|
$curlErr = curl_error($ch);
|
|
curl_close($ch);
|
|
} elseif (function_exists('stream_context_create')) {
|
|
$ctx = stream_context_create(['http' => [
|
|
'method' => 'POST',
|
|
'header' => "Authorization: APPCODE {$appcode}\r\nContent-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n",
|
|
'content' => $bodys,
|
|
'timeout' => 15,
|
|
'ignore_errors' => true,
|
|
], 'ssl' => [
|
|
'verify_peer' => false,
|
|
'verify_peer_name' => false,
|
|
]]);
|
|
$r = @file_get_contents($url, false, $ctx);
|
|
if ($r !== false) {
|
|
$resp = (string) $r;
|
|
if (isset($http_response_header)) {
|
|
foreach ($http_response_header as $h) {
|
|
if (preg_match('
|
|
$httpCode = (int) $m[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($resp === null || $resp === '') {
|
|
$detail = trim($curlErr !== '' ? $curlErr : ('HTTP ' . $httpCode));
|
|
return ['ok' => false, 'error' => '实名接口请求失败(网络异常):' . ($detail ?: '空响应')];
|
|
}
|
|
$data = json_decode($resp, true);
|
|
if (!is_array($data)) {
|
|
$preview = mb_substr(trim($resp), 0, 300);
|
|
return ['ok' => false, 'error' => '实名接口返回格式异常(HTTP ' . $httpCode . '):' . ($preview ?: '(空内容)')];
|
|
}
|
|
$respCode = $data['respCode'] ?? null;
|
|
$respMessage = $data['respMessage'] ?? ($data['msg'] ?? '');
|
|
$codeOk = ((string) $respCode === '0000');
|
|
$matchMsg = preg_match('/匹配|一致|成功|通过/', (string) $respMessage) === 1;
|
|
$body = $data['showapi_res_body'] ?? $data;
|
|
$legacyCode = $body['code'] ?? null;
|
|
$legacyOk = in_array((string) $legacyCode, ['0', '1', '00'], true);
|
|
if ($codeOk || $matchMsg || $legacyOk) {
|
|
return ['ok' => true];
|
|
}
|
|
$rawPreview = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
if (mb_strlen($rawPreview) > 500) {
|
|
$rawPreview = mb_substr($rawPreview, 0, 500) . '...(截断)';
|
|
}
|
|
$errReason = $respMessage !== '' ? $respMessage : ('respCode=' . ($respCode ?? '无'));
|
|
return ['ok' => false, 'error' => '实名核验未通过(' . $errReason . ')。原始响应:' . $rawPreview];
|
|
}
|
|
function submitRealname($uid, $name, $idno)
|
|
{
|
|
$uid = (int) $uid;
|
|
$existing = getUserRealname($uid);
|
|
$nameEnc = rnEncrypt($name);
|
|
$idnoEnc = rnEncrypt($idno);
|
|
if ($existing === null) {
|
|
db()->prepare(
|
|
'INSERT INTO ' . tn('user_realname') . ' (user_id, real_name_enc, idno_enc, status, modify_times, created_at) VALUES (?,?,?,1,0,NOW())'
|
|
)->execute([$uid, $nameEnc, $idnoEnc]);
|
|
return true;
|
|
}
|
|
$used = (int) ($existing['modify_times'] ?? 0);
|
|
if ($used >= 1) {
|
|
return false;
|
|
}
|
|
db()->prepare(
|
|
'UPDATE ' . tn('user_realname') . ' SET real_name_enc=?, idno_enc=?, status=1, modify_times=modify_times+1, updated_at=NOW() WHERE user_id=?'
|
|
)->execute([$nameEnc, $idnoEnc, $uid]);
|
|
return true;
|
|
}
|
|
function getUserRealname($uid)
|
|
{
|
|
$stmt = db()->prepare('SELECT * FROM ' . tn('user_realname') . ' WHERE user_id = ? ORDER BY id DESC LIMIT 1');
|
|
$stmt->execute([(int) $uid]);
|
|
return $stmt->fetch() ?: null;
|
|
}
|
|
function isRealnameVerified($uid)
|
|
{
|
|
$r = getUserRealname($uid);
|
|
return $r !== null && (int) $r['status'] === 1;
|
|
}
|
|
function maskIdno($idno)
|
|
{
|
|
$idno = (string) $idno;
|
|
$len = mb_strlen($idno, 'UTF-8');
|
|
if ($len <= 8) {
|
|
return str_repeat('*', $len);
|
|
}
|
|
return mb_substr($idno, 0, 4, 'UTF-8') . str_repeat('*', $len - 8) . mb_substr($idno, -4, null, 'UTF-8');
|
|
}
|
|
function maskName($name)
|
|
{
|
|
$name = (string) $name;
|
|
if ($name === '') {
|
|
return '';
|
|
}
|
|
return mb_substr($name, 0, 1, 'UTF-8') . str_repeat('*', mb_strlen($name, 'UTF-8') - 1);
|
|
}
|