异地登录检测/账号冻结解冻 + 公共邮件辅助函数
- core.php: 新增smtpSendEmail/queryLoginIpLocation/getBaseUrl/sendLocationAlertEmail - security_alert.php: 接收Java通知比对IP归属并发送提醒邮件(含冻结/改密链接) - freeze.php: 验证freeze_token冻结账号并写freeze指令 - sync.php: 冻结账号禁止Web登录; smtpSendEmail上移至core.php统一复用 - reset_password.php: 改密成功后解冻(清frozen+写unfreeze)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* 异地登录检测回调(Java 插件在玩家加入服务器时异步调用)
|
||||
*
|
||||
* 调用方式:GET api/security_alert.php
|
||||
* ?action=login_location_alert&secret=...&name=<玩家>&ip=<登录IP>
|
||||
*
|
||||
* 职责:
|
||||
* 1. 用 ip9.com.cn 查询本次登录 IP 归属
|
||||
* 2. 与上次登录归属比较,不一致且非首次 → 向绑定邮箱发送提醒邮件
|
||||
* 3. 邮件内含「冻结账号」与「修改密码」两个链接
|
||||
* 4. 更新 users.last_login_location / last_login_ip
|
||||
*/
|
||||
require_once __DIR__ . '/../core.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
if ($action !== 'login_location_alert') {
|
||||
echo json_encode(['success' => false, 'message' => '未知操作']);
|
||||
exit;
|
||||
}
|
||||
$secret = $_GET['secret'] ?? '';
|
||||
if (!$secret || $secret !== SECRET_KEY) {
|
||||
echo json_encode(['success' => false, 'message' => '密钥错误']);
|
||||
exit;
|
||||
}
|
||||
$name = $_GET['name'] ?? '';
|
||||
$ip = $_GET['ip'] ?? '';
|
||||
if (!$name || !$ip) {
|
||||
echo json_encode(['success' => false, 'message' => '参数缺失']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// 查询账号信息
|
||||
$stmt = $db->prepare(
|
||||
"SELECT email, last_login_location, last_login_ip, frozen
|
||||
FROM users WHERE player_name = :name");
|
||||
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
// 查询本次 IP 归属
|
||||
$location = queryLoginIpLocation($ip);
|
||||
|
||||
if ($location === null) {
|
||||
// 归属查询失败:仅更新 IP,不阻断、不提醒
|
||||
$upd = $db->prepare(
|
||||
"UPDATE users SET last_login_ip = :ip WHERE player_name = :name");
|
||||
$upd->bindValue(':ip', $ip, SQLITE3_TEXT);
|
||||
$upd->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$upd->execute();
|
||||
echo json_encode(['success' => true, 'message' => 'IP归属查询失败,仅更新IP']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
// 理论上 Java 已 syncUserOnJoin,不会走到这里
|
||||
echo json_encode(['success' => true, 'message' => '无用户记录']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$lastLoc = $row['last_login_location'] ?? '';
|
||||
$lastIp = $row['last_login_ip'] ?? '';
|
||||
$email = $row['email'] ?? '';
|
||||
|
||||
// 更新归属与 IP
|
||||
$upd = $db->prepare(
|
||||
"UPDATE users SET last_login_ip = :ip,
|
||||
last_login_location = :loc WHERE player_name = :name");
|
||||
$upd->bindValue(':ip', $ip, SQLITE3_TEXT);
|
||||
$upd->bindValue(':loc', $location, SQLITE3_TEXT);
|
||||
$upd->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$upd->execute();
|
||||
|
||||
// 首次登录(无任何历史记录)不提醒
|
||||
if ($lastLoc === '' && $lastIp === '') {
|
||||
echo json_encode(['success' => true, 'message' => '首次登录,已记录']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 归属变化才发送提醒邮件
|
||||
if ($lastLoc !== $location && !empty($email)) {
|
||||
// 生成冻结 token 并持久化,供邮件中的冻结链接使用
|
||||
$freezeToken = bin2hex(random_bytes(16));
|
||||
$ut = $db->prepare("UPDATE users SET freeze_token = :t WHERE player_name = :name");
|
||||
$ut->bindValue(':t', $freezeToken, SQLITE3_TEXT);
|
||||
$ut->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$ut->execute();
|
||||
sendLocationAlertEmail($name, $email, $ip, $location, $lastLoc, $freezeToken);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'ok']);
|
||||
+9
-79
@@ -50,85 +50,7 @@ set_exception_handler(function($e) {
|
||||
|
||||
// debugLog() 函数已在 core.php 中定义,无需重复声明
|
||||
|
||||
// ===== SMTP邮件发送函数 =====
|
||||
function smtpSendEmail($host, $port, $user, $pass, $to, $subject, $htmlBody, $headers, $useSSL = true) {
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
|
||||
// 创建socket连接
|
||||
if ($useSSL) {
|
||||
$socket = @fsockopen('ssl://' . $host, $port, $errno, $errstr, 30);
|
||||
} else {
|
||||
$socket = @fsockopen($host, $port, $errno, $errstr, 30);
|
||||
}
|
||||
|
||||
if (!$socket) {
|
||||
throw new Exception("SMTP连接失败: $errstr ($errno)");
|
||||
}
|
||||
|
||||
// 读取服务器响应
|
||||
$response = fgets($socket);
|
||||
if (strpos($response, '220') !== 0) {
|
||||
fclose($socket);
|
||||
throw new Exception("SMTP服务器拒绝连接: $response");
|
||||
}
|
||||
|
||||
// 发送EHLO
|
||||
fwrite($socket, "EHLO localhost\r\n");
|
||||
$response = fread($socket, 1024);
|
||||
|
||||
// 登录认证
|
||||
fwrite($socket, "AUTH LOGIN\r\n");
|
||||
$response = fgets($socket);
|
||||
if (strpos($response, '334') !== 0) {
|
||||
fclose($socket);
|
||||
throw new Exception("SMTP认证失败: $response");
|
||||
}
|
||||
|
||||
// 发送用户名
|
||||
fwrite($socket, base64_encode($user) . "\r\n");
|
||||
$response = fgets($socket);
|
||||
if (strpos($response, '334') !== 0) {
|
||||
fclose($socket);
|
||||
throw new Exception("SMTP用户名错误: $response");
|
||||
}
|
||||
|
||||
// 发送密码
|
||||
fwrite($socket, base64_encode($pass) . "\r\n");
|
||||
$response = fgets($socket);
|
||||
if (strpos($response, '235') !== 0) {
|
||||
fclose($socket);
|
||||
throw new Exception("SMTP密码错误: $response");
|
||||
}
|
||||
|
||||
// 发送邮件
|
||||
fwrite($socket, "MAIL FROM:<$user>\r\n");
|
||||
fgets($socket);
|
||||
|
||||
fwrite($socket, "RCPT TO:<$to>\r\n");
|
||||
fgets($socket);
|
||||
|
||||
fwrite($socket, "DATA\r\n");
|
||||
fgets($socket);
|
||||
|
||||
// 发送邮件头和正文
|
||||
$emailData = "Subject: $subject\r\n";
|
||||
$emailData .= $headers;
|
||||
$emailData .= "\r\n";
|
||||
$emailData .= $htmlBody;
|
||||
$emailData .= "\r\n.\r\n";
|
||||
|
||||
fwrite($socket, $emailData);
|
||||
$response = fgets($socket);
|
||||
|
||||
// 退出
|
||||
fwrite($socket, "QUIT\r\n");
|
||||
fgets($socket);
|
||||
|
||||
fclose($socket);
|
||||
|
||||
return true;
|
||||
}
|
||||
// ===== SMTP邮件发送函数(已统一移至 core.php,通过 require_once core.php 复用,避免重复定义) =====
|
||||
|
||||
|
||||
/**
|
||||
@@ -2440,6 +2362,14 @@ function completeWebLoginRequest() {
|
||||
|
||||
// 如果验证成功,写入web_login_confirmations和web_login_verified
|
||||
if ($status === 'success') {
|
||||
// ★ 冻结账号禁止 Web 登录(异地登录自冻结保护)
|
||||
$fz = $db->prepare("SELECT frozen FROM users WHERE player_name = :name");
|
||||
$fz->bindValue(':name', $player, SQLITE3_TEXT);
|
||||
$fzr = $fz->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if ($fzr && (int)$fzr['frozen'] === 1) {
|
||||
error('账号已被冻结,请修改密码后重试');
|
||||
}
|
||||
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS web_login_confirmations (player_name TEXT PRIMARY KEY, confirmed_at INTEGER NOT NULL, consumed INTEGER DEFAULT 0)");
|
||||
$confirmStmt = $db->prepare("INSERT OR REPLACE INTO web_login_confirmations (player_name, confirmed_at, consumed) VALUES (:player, :time, 0)");
|
||||
$confirmStmt->bindValue(':player', $player, SQLITE3_TEXT);
|
||||
|
||||
@@ -411,6 +411,24 @@ function initTables(SQLite3 $db) {
|
||||
total_online_time INTEGER DEFAULT 0
|
||||
)");
|
||||
|
||||
// ===== 风控 / 封禁相关列(安全迁移,避免 ALTER 锁表) =====
|
||||
$userCols = [];
|
||||
$ci = $db->query("PRAGMA table_info(users)");
|
||||
while ($c = $ci->fetchArray(SQLITE3_ASSOC)) {
|
||||
$userCols[] = $c['name'];
|
||||
}
|
||||
$addCols = [
|
||||
'last_login_location' => "TEXT DEFAULT ''",
|
||||
'last_login_ip' => "TEXT DEFAULT ''",
|
||||
'frozen' => "INTEGER DEFAULT 0",
|
||||
'freeze_token' => "TEXT DEFAULT ''",
|
||||
];
|
||||
foreach ($addCols as $col => $type) {
|
||||
if (!in_array($col, $userCols)) {
|
||||
$db->exec("ALTER TABLE users ADD COLUMN $col $type");
|
||||
}
|
||||
}
|
||||
|
||||
// Web登录Token表
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS weblogin_tokens (
|
||||
player_name TEXT PRIMARY KEY,
|
||||
@@ -1588,3 +1606,197 @@ function validateWebAccess($webToken, $action = 'view', $password = null, $ipAdd
|
||||
debugLog("validateWebAccess: 验证成功", ['player' => $playerName, 'mode' => 'full']);
|
||||
return ['ok' => true, 'mode' => 'full', 'player' => $playerName, 'session' => $sessionToken, 'registered' => $isRegistered, 'online' => $isOnline, 'message' => '验证成功'];
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 异地登录检测 / 账号冻结 公共函数(security_alert.php / freeze.php 复用)
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* SMTP 邮件发送(已从 api/sync.php 上移至此处,统一复用,避免重复定义)
|
||||
*/
|
||||
function smtpSendEmail($host, $port, $user, $pass, $to, $subject, $htmlBody, $headers, $useSSL = true) {
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
|
||||
// 创建socket连接
|
||||
if ($useSSL) {
|
||||
$socket = @fsockopen('ssl://' . $host, $port, $errno, $errstr, 30);
|
||||
} else {
|
||||
$socket = @fsockopen($host, $port, $errno, $errstr, 30);
|
||||
}
|
||||
|
||||
if (!$socket) {
|
||||
throw new Exception("SMTP连接失败: $errstr ($errno)");
|
||||
}
|
||||
|
||||
// 读取服务器响应
|
||||
$response = fgets($socket);
|
||||
if (strpos($response, '220') !== 0) {
|
||||
fclose($socket);
|
||||
throw new Exception("SMTP服务器拒绝连接: $response");
|
||||
}
|
||||
|
||||
// 发送EHLO
|
||||
fwrite($socket, "EHLO localhost\r\n");
|
||||
$response = fread($socket, 1024);
|
||||
|
||||
// 登录认证
|
||||
fwrite($socket, "AUTH LOGIN\r\n");
|
||||
$response = fgets($socket);
|
||||
if (strpos($response, '334') !== 0) {
|
||||
fclose($socket);
|
||||
throw new Exception("SMTP认证失败: $response");
|
||||
}
|
||||
|
||||
// 发送用户名
|
||||
fwrite($socket, base64_encode($user) . "\r\n");
|
||||
$response = fgets($socket);
|
||||
if (strpos($response, '334') !== 0) {
|
||||
fclose($socket);
|
||||
throw new Exception("SMTP用户名错误: $response");
|
||||
}
|
||||
|
||||
// 发送密码
|
||||
fwrite($socket, base64_encode($pass) . "\r\n");
|
||||
$response = fgets($socket);
|
||||
if (strpos($response, '235') !== 0) {
|
||||
fclose($socket);
|
||||
throw new Exception("SMTP密码错误: $response");
|
||||
}
|
||||
|
||||
// 发送邮件
|
||||
fwrite($socket, "MAIL FROM:<$user>\r\n");
|
||||
fgets($socket);
|
||||
|
||||
fwrite($socket, "RCPT TO:<$to>\r\n");
|
||||
fgets($socket);
|
||||
|
||||
fwrite($socket, "DATA\r\n");
|
||||
fgets($socket);
|
||||
|
||||
// 发送邮件头和正文
|
||||
$emailData = "Subject: $subject\r\n";
|
||||
$emailData .= $headers;
|
||||
$emailData .= "\r\n";
|
||||
$emailData .= $htmlBody;
|
||||
$emailData .= "\r\n.\r\n";
|
||||
|
||||
fwrite($socket, $emailData);
|
||||
$response = fgets($socket);
|
||||
|
||||
// 退出
|
||||
fwrite($socket, "QUIT\r\n");
|
||||
fgets($socket);
|
||||
|
||||
fclose($socket);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询登录 IP 归属(ip9.com.cn)
|
||||
* 注意:与 admin.php 的 queryIpLocation 区分命名,避免重复定义冲突。
|
||||
* @return string|null 归属字符串(如 "广东深圳"),失败或内网返回 null
|
||||
*/
|
||||
function queryLoginIpLocation($ip) {
|
||||
if (empty($ip) || $ip === '-' || $ip === '127.0.0.1'
|
||||
|| strpos($ip, '10.') === 0 || strpos($ip, '192.168.') === 0
|
||||
|| !filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return null;
|
||||
}
|
||||
$url = 'https://ip9.com.cn/get?ip=' . urlencode($ip);
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'timeout' => 3,
|
||||
'ignore_errors' => true,
|
||||
'header' => "Accept: application/json, text/plain, */*\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\nReferer: https://ip9.com.cn/\r\nOrigin: https://ip9.com.cn"
|
||||
],
|
||||
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false]
|
||||
]);
|
||||
$response = @file_get_contents($url, false, $ctx);
|
||||
if ($response === false) return null;
|
||||
$response = trim($response);
|
||||
if ($response === '') return null;
|
||||
$data = json_decode($response, true);
|
||||
if (!$data || !is_array($data)) return null;
|
||||
$item = null;
|
||||
if (isset($data['data'][0]) && is_array($data['data'][0])) {
|
||||
$item = $data['data'][0];
|
||||
} elseif (isset($data['data']) && is_array($data['data']) && isset($data['data']['prov'])) {
|
||||
$item = $data['data'];
|
||||
}
|
||||
if (!$item) return null;
|
||||
$loc = '';
|
||||
if (isset($item['prov'])) $loc .= $item['prov'];
|
||||
if (isset($item['city'])) $loc .= $item['city'];
|
||||
if (isset($item['area']) && ($item['area'] !== ($item['city'] ?? '')) && ($item['area'] !== ($item['prov'] ?? ''))) {
|
||||
$loc .= $item['area'];
|
||||
}
|
||||
$loc = trim($loc);
|
||||
return $loc === '' ? null : $loc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建站点绝对基址(用于邮件链接)
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
if ($host === '' || $host === '127.0.0.1' || $host === 'localhost') {
|
||||
$host = 'caoyuan.ypshidifu.cn';
|
||||
}
|
||||
// 站点统一使用 HTTPS
|
||||
$base = 'https://' . $host;
|
||||
$sub = trim(WEBSUB_DIR, '/');
|
||||
if ($sub !== '') $base .= '/' . $sub;
|
||||
return rtrim($base, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送异地登录提醒邮件(附带冻结 / 改密链接)
|
||||
* @param string $freezeToken 已持久化到 users.freeze_token 的令牌
|
||||
*/
|
||||
function sendLocationAlertEmail($name, $email, $ip, $location, $lastLoc, $freezeToken) {
|
||||
$db = getDB();
|
||||
// 生成改密 token(30分钟内有效)
|
||||
$resetToken = bin2hex(random_bytes(16));
|
||||
$expire = time() + 1800;
|
||||
$ins = $db->prepare("INSERT INTO password_reset_tokens (token, player_name, expire_at) VALUES (:t, :n, :e)");
|
||||
$ins->bindValue(':t', $resetToken, SQLITE3_TEXT);
|
||||
$ins->bindValue(':n', $name, SQLITE3_TEXT);
|
||||
$ins->bindValue(':e', $expire, SQLITE3_INTEGER);
|
||||
$ins->execute();
|
||||
|
||||
$base = getBaseUrl();
|
||||
$freezeLink = $base . '/freeze.php?token=' . urlencode($freezeToken);
|
||||
$resetLink = $base . '/reset_password.php?token=' . urlencode($resetToken);
|
||||
|
||||
$subject = '[Sdf1] 异地登录提醒:您的账号于 ' . $location . ' 登录';
|
||||
|
||||
$html = '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">'
|
||||
. '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
|
||||
. '<title>异地登录提醒</title></head><body style="margin:0;padding:0;background:#f4f6f9;font-family:Segoe UI,Tahoma,sans-serif;">'
|
||||
. '<div style="max-width:560px;margin:0 auto;padding:24px;">'
|
||||
. '<div style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 6px 24px rgba(0,0,0,0.08);">'
|
||||
. '<div style="background:linear-gradient(135deg,#e0533d,#c0392b);padding:28px 32px;color:#fff;">'
|
||||
. '<h1 style="margin:0;font-size:22px;">⚠️ 异地登录提醒</h1>'
|
||||
. '<p style="margin:8px 0 0;opacity:.9;font-size:14px;">Sdf1 Minecraft 账号安全中心</p></div>'
|
||||
. '<div style="padding:32px;">'
|
||||
. '<p style="margin:0 0 16px;font-size:15px;color:#333;">亲爱的 <b>' . htmlspecialchars($name) . '</b>:</p>'
|
||||
. '<p style="margin:0 0 16px;font-size:15px;color:#555;line-height:1.7;">我们检测到您的账号于 <b style="color:#e0533d;">' . htmlspecialchars($location) . '</b> 发起了登录,'
|
||||
. '而您上一次的登录地点为 <b>' . htmlspecialchars($lastLoc ?: '未知') . '</b>。如果该登录并非您本人操作,账号可能已泄露。</p>'
|
||||
. '<div style="background:#fff7f5;border:1px solid #f3c4ba;border-radius:8px;padding:14px 16px;margin:0 0 20px;font-size:14px;color:#8a4b3a;">'
|
||||
. '本次登录 IP:' . htmlspecialchars($ip) . '<br>登录时间:' . date('Y-m-d H:i:s') . '</div>'
|
||||
. '<p style="margin:0 0 12px;font-size:14px;color:#555;">如确认存在风险,请点击下方按钮<b>冻结账号</b>(将立即封禁游戏登录,需改密后解冻):</p>'
|
||||
. '<div style="text-align:center;margin:0 0 24px;"><a href="' . htmlspecialchars($freezeLink) . '" style="display:inline-block;background:linear-gradient(135deg,#e0533d,#c0392b);color:#fff;text-decoration:none;padding:13px 36px;border-radius:8px;font-size:15px;font-weight:600;">立即冻结账号</a></div>'
|
||||
. '<p style="margin:0 0 12px;font-size:14px;color:#555;">如确认是本人操作,也建议您定期修改密码以保障安全:</p>'
|
||||
. '<div style="text-align:center;margin:0 0 8px;"><a href="' . htmlspecialchars($resetLink) . '" style="display:inline-block;background:#fff;color:#e0533d;border:1px solid #e0533d;text-decoration:none;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:600;">修改密码</a></div>'
|
||||
. '<p style="margin:24px 0 0;font-size:12px;color:#999;border-top:1px solid #eee;padding-top:16px;">本邮件由系统自动发送,请勿直接回复。若您未触发任何登录,请尽快冻结账号并修改密码。</p>'
|
||||
. '</div></div></div></body></html>';
|
||||
|
||||
$headers = "From: " . SMTP_SENDER_NAME . " <" . SMTP_USER . ">\r\n"
|
||||
. "Reply-To: " . SMTP_USER . "\r\n"
|
||||
. "MIME-Version: 1.0\r\n"
|
||||
. "Content-Type: text/html; charset=UTF-8\r\n";
|
||||
smtpSendEmail(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, $email, $subject, $html, $headers, SMTP_USE_SSL);
|
||||
}
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* 账号冻结页面(玩家从「异地登录提醒」邮件点击进入)
|
||||
* 验证 freeze_token → 标记账号冻结(frozen=1) → 写入 web_admin_changes(freeze)
|
||||
* 由 Java 插件 pollAdminChanges 拉取后执行 Bukkit 原生封禁。
|
||||
* 解冻需在 reset_password.php 修改密码后由 PHP 写 web_admin_changes(unfreeze)。
|
||||
*/
|
||||
require_once __DIR__ . '/core.php';
|
||||
|
||||
$token = $_GET['token'] ?? '';
|
||||
$error = '';
|
||||
$success = false;
|
||||
$player = '';
|
||||
$resetLink = '';
|
||||
|
||||
if (!$token) {
|
||||
die('无效链接');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare(
|
||||
"SELECT player_name, frozen FROM users WHERE freeze_token = :t");
|
||||
$stmt->bindValue(':t', $token, SQLITE3_TEXT);
|
||||
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
die('链接无效或已使用');
|
||||
}
|
||||
|
||||
$player = $row['player_name'];
|
||||
|
||||
if ($row['frozen'] == 1) {
|
||||
// 已冻结,直接展示已冻结页
|
||||
$success = true;
|
||||
} else {
|
||||
// 执行冻结
|
||||
$db->exec("UPDATE users SET frozen = 1, freeze_token = '' WHERE player_name = "
|
||||
. SQLite3::escapeString($player));
|
||||
// 通知 Java 插件封禁该玩家(Bukkit 原生 ban)
|
||||
$now = time();
|
||||
$stmt2 = $db->prepare(
|
||||
"INSERT INTO web_admin_changes
|
||||
(change_type, target_id, target_name, change_data, created_at)
|
||||
VALUES ('freeze', 0, :name, '{}', :now)");
|
||||
$stmt2->bindValue(':name', $player, SQLITE3_TEXT);
|
||||
$stmt2->bindValue(':now', $now, SQLITE3_INTEGER);
|
||||
$stmt2->execute();
|
||||
$success = true;
|
||||
}
|
||||
|
||||
// 取一个有效改密 token 构造「去改密」链接
|
||||
$stmt3 = $db->prepare(
|
||||
"SELECT token FROM password_reset_tokens
|
||||
WHERE player_name = :name AND expire_at > :now
|
||||
ORDER BY expire_at DESC LIMIT 1");
|
||||
$stmt3->bindValue(':name', $player, SQLITE3_TEXT);
|
||||
$stmt3->bindValue(':now', time(), SQLITE3_INTEGER);
|
||||
$r3 = $stmt3->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if ($r3) {
|
||||
$resetLink = getBaseUrl() . '/reset_password.php?token=' . $r3['token'];
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>账号已冻结 - Sdf1 Minecraft</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Tahoma, sans-serif;
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #4b6584 100%);
|
||||
min-height: 100vh; display: flex; align-items: center;
|
||||
justify-content: center; padding: 20px; }
|
||||
.container { background: white; border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
max-width: 440px; width: 100%; padding: 40px; text-align: center; }
|
||||
h1 { color: #e0533d; font-size: 26px; margin-bottom: 16px; }
|
||||
.player { background: #f5f5f5; border-radius: 8px; padding: 14px;
|
||||
margin: 20px 0; font-size: 18px; }
|
||||
.player strong { color: #e0533d; }
|
||||
.tip { color: #666; font-size: 14px; line-height: 1.7; margin: 12px 0; }
|
||||
.btn { display: inline-block; width: 100%; padding: 14px;
|
||||
background: linear-gradient(135deg, #e0533d, #c0392b);
|
||||
color: white; border: none; border-radius: 8px;
|
||||
font-size: 16px; font-weight: 600; text-decoration: none;
|
||||
transition: transform 0.2s; }
|
||||
.btn:hover { transform: translateY(-2px); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✅ 账号已冻结</h1>
|
||||
<p class="tip">为防范账号被盗用,玩家 <b><?php echo htmlspecialchars($player); ?></b> 的账号已被临时冻结。</p>
|
||||
<div class="player">玩家:<strong><?php echo htmlspecialchars($player); ?></strong></div>
|
||||
<p class="tip">冻结期间将无法进入游戏。如需解冻,请<b>修改密码</b>,系统将自动解除冻结。</p>
|
||||
<?php if ($resetLink): ?>
|
||||
<a class="btn" href="<?php echo htmlspecialchars($resetLink); ?>">前往修改密码并解冻</a>
|
||||
<?php else: ?>
|
||||
<p class="tip">未找到有效的改密链接,请返回异地登录提醒邮件点击「修改密码」。</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+10
-2
@@ -55,14 +55,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// 更新密码凭证
|
||||
storeWebLoginCredentials($player, $passwordHash, $salt);
|
||||
|
||||
|
||||
// ★ 解冻:清除 frozen 标记并通知 Java 解除 Bukkit 原生封禁
|
||||
$db->exec("UPDATE users SET frozen = 0, freeze_token = '' WHERE player_name = " . SQLite3::escapeString($player));
|
||||
$now = time();
|
||||
$unf = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at) VALUES ('unfreeze', 0, :name, '{}', :now)");
|
||||
$unf->bindValue(':name', $player, SQLITE3_TEXT);
|
||||
$unf->bindValue(':now', $now, SQLITE3_INTEGER);
|
||||
$unf->execute();
|
||||
|
||||
// 删除重置token
|
||||
$stmt = $db->prepare("DELETE FROM password_reset_tokens WHERE token = :token");
|
||||
$stmt->bindValue(':token', $token, SQLITE3_TEXT);
|
||||
$stmt->execute();
|
||||
|
||||
$success = true;
|
||||
$message = '密码重置成功!请在游戏中使用新密码登录';
|
||||
$message = '密码重置成功!账号已解冻,请在游戏中使用新密码登录';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user