文件
2026-08-16 17:03:10 +08:00

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);
}