feat(security): 新增网站安全扩展模块并支持修改手机脱敏验证
This commit is contained in:
1 parent
224f86ec08
commit
af72364078
30 files changed
+1389
-73
No files matched your search
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* 网站安全:安全加固开关、内容防刷限制与验证错误频率限制。
|
||||
*
|
||||
* 对应后台「扩展&增强 → 网站安全」设置(ExtensionRuntime::get_security_settings()),
|
||||
* 行为对齐参考主题 zib-theme.php / action/function.php 的安全套件。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Extensions;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SecurityHardening {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$settings = \AKSRT\Settings\ExtensionRuntime::get_security_settings();
|
||||
|
||||
if (!empty($settings['security_disabled_xmlrpc'])) {
|
||||
add_filter('xmlrpc_enabled', '__return_false');
|
||||
add_filter('xmlrpc_methods', '__return_empty_array');
|
||||
}
|
||||
|
||||
if (!empty($settings['security_disabled_pingback'])) {
|
||||
add_action('pre_ping', [$this, 'disable_self_pingback']);
|
||||
}
|
||||
|
||||
if (!empty($settings['security_user_disable_admin'])) {
|
||||
add_action('admin_init', [$this, 'restrict_admin_access']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防 pingback 攻击:移除指向自己站点的 pingback。
|
||||
*/
|
||||
public function disable_self_pingback(array &$links): void {
|
||||
$home = home_url();
|
||||
foreach ($links as $index => $link) {
|
||||
if (0 === strpos((string) $link, $home)) {
|
||||
unset($links[$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 非管理员禁止进入后台(放行 admin-ajax 与定时任务)。
|
||||
*/
|
||||
public function restrict_admin_access(): void {
|
||||
if (wp_doing_ajax() || wp_doing_cron()) {
|
||||
return;
|
||||
}
|
||||
if (!is_user_logged_in() || current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
wp_safe_redirect(home_url('/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容防刷限制:检查并计数(10 分钟窗口 + 当天两个维度,0 = 不限制)。
|
||||
*
|
||||
* @param string $scope post|comment|upload|register|order
|
||||
* @param string $subject 用户 ID 或 IP 等限流主体
|
||||
* @param int $cap_10 10 分钟上限
|
||||
* @param int $cap_day 一天上限
|
||||
* @return true|string true = 放行(并计数),字符串 = 拒绝原因
|
||||
*/
|
||||
public static function brush_allow(string $scope, string $subject, int $cap_10, int $cap_day) {
|
||||
if ($cap_10 <= 0 && $cap_day <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope = sanitize_key($scope);
|
||||
$hash = md5((string) $subject);
|
||||
$minute_key = sprintf('aksrt_brush_%s_m_%s_%s%d', $scope, $hash, wp_date('YmdH'), intdiv((int) wp_date('i'), 10));
|
||||
$day_key = sprintf('aksrt_brush_%s_d_%s_%s', $scope, $hash, wp_date('Ymd'));
|
||||
|
||||
if ($cap_10 > 0 && (int) get_transient($minute_key) >= $cap_10) {
|
||||
return __('操作过于频繁,请稍后再试', 'aksrt');
|
||||
}
|
||||
if ($cap_day > 0 && (int) get_transient($day_key) >= $cap_day) {
|
||||
return __('今日操作次数已达上限,请明天再试', 'aksrt');
|
||||
}
|
||||
|
||||
if ($cap_10 > 0) {
|
||||
set_transient($minute_key, (int) get_transient($minute_key) + 1, 10 * MINUTE_IN_SECONDS);
|
||||
}
|
||||
if ($cap_day > 0) {
|
||||
set_transient($day_key, (int) get_transient($day_key) + 1, DAY_IN_SECONDS);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误频率限制:返回下一次验证需等待的秒数(0 = 可立即验证)。
|
||||
* 错误超过 5 次限 30 秒一次,超过 10 次限 3 分钟一次。
|
||||
*/
|
||||
public static function error_frequency_wait(): int {
|
||||
$settings = \AKSRT\Settings\ExtensionRuntime::get_security_settings();
|
||||
if (empty($settings['security_error_frequency_limit'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$state = get_transient(self::error_frequency_key());
|
||||
if (!is_array($state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = (int) ($state['count'] ?? 0);
|
||||
$last = (int) ($state['last'] ?? 0);
|
||||
if ($count < 5) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$remaining = ($count >= 10 ? 180 : 30) - (time() - $last);
|
||||
return $remaining > 0 ? $remaining : 0;
|
||||
}
|
||||
|
||||
/** 记录一次验证失败(滑动 10 分钟窗口)。 */
|
||||
public static function error_frequency_record(): void {
|
||||
$settings = \AKSRT\Settings\ExtensionRuntime::get_security_settings();
|
||||
if (empty($settings['security_error_frequency_limit'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = self::error_frequency_key();
|
||||
$state = get_transient($key);
|
||||
$count = is_array($state) ? (int) ($state['count'] ?? 0) + 1 : 1;
|
||||
set_transient($key, ['count' => $count, 'last' => time()], 10 * MINUTE_IN_SECONDS);
|
||||
}
|
||||
|
||||
/** 验证成功后清除错误计数。 */
|
||||
public static function error_frequency_clear(): void {
|
||||
delete_transient(self::error_frequency_key());
|
||||
}
|
||||
|
||||
private static function error_frequency_key(): string {
|
||||
$ip = function_exists('aksrt_get_client_ip') ? aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
return 'aksrt_err_freq_' . md5((string) get_current_user_id() . '|' . $ip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* 网站安全:验证码邮件/短信发送频率限制([防刷机制]验证码发送限制)。
|
||||
*
|
||||
* 五个维度全部可配置(0 = 不限制):同一收件人冷却、同一收件人 1 小时/一天上限、
|
||||
* 同一 IP 10 分钟/一天上限。短信在 SMS::send_code 内统一接入,邮件在发送验证码
|
||||
* 的调用点接入。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Extensions;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SendLimit {
|
||||
|
||||
/**
|
||||
* 发送前检查。命中任一限制返回 ['ok' => false, 'message' => ..., 'remaining' => 秒]。
|
||||
*
|
||||
* @return array{ok:bool,message:string,remaining:int}
|
||||
*/
|
||||
public static function check(string $target): array {
|
||||
$settings = \AKSRT\Settings\ExtensionRuntime::get_security_settings();
|
||||
$target_hash = md5(strtolower(trim($target)));
|
||||
$ip_hash = md5(self::client_ip());
|
||||
|
||||
if ($settings['security_captcha_send_cooldown'] > 0) {
|
||||
$sent_at = (int) get_transient('aksrt_send_cd_' . $target_hash);
|
||||
if ($sent_at > 0) {
|
||||
$remaining = $settings['security_captcha_send_cooldown'] - (time() - $sent_at);
|
||||
if ($remaining > 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => sprintf(__('请 %d 秒后再试', 'aksrt'), $remaining),
|
||||
'remaining' => $remaining,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$recipient_checks = [
|
||||
['security_captcha_to_hour', 'aksrt_send_to_h_' . $target_hash . '_' . wp_date('YmdH'), __('该邮箱或手机号1小时内验证码发送次数已达上限', 'aksrt')],
|
||||
['security_captcha_to_day', 'aksrt_send_to_d_' . $target_hash . '_' . wp_date('Ymd'), __('该邮箱或手机号今日验证码发送次数已达上限', 'aksrt')],
|
||||
];
|
||||
$ip_checks = [
|
||||
['security_captcha_ip_10min', sprintf('aksrt_send_ip_m_%s_%s%d', $ip_hash, wp_date('YmdH'), intdiv((int) wp_date('i'), 10)), __('当前IP验证码发送过于频繁,请稍后再试', 'aksrt')],
|
||||
['security_captcha_ip_day', 'aksrt_send_ip_d_' . $ip_hash . '_' . wp_date('Ymd'), __('当前IP今日验证码发送次数已达上限', 'aksrt')],
|
||||
];
|
||||
|
||||
foreach (array_merge($recipient_checks, $ip_checks) as [$cap_key, $transient_key, $message]) {
|
||||
if ($settings[$cap_key] > 0 && (int) get_transient($transient_key) >= $settings[$cap_key]) {
|
||||
return ['ok' => false, 'message' => $message, 'remaining' => $settings['security_captcha_send_cooldown']];
|
||||
}
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => '', 'remaining' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送成功后记录计数,返回冷却秒数(用于前端倒计时)。
|
||||
*/
|
||||
public static function record(string $target): int {
|
||||
$settings = \AKSRT\Settings\ExtensionRuntime::get_security_settings();
|
||||
$target_hash = md5(strtolower(trim($target)));
|
||||
$ip = self::client_ip();
|
||||
$ip_hash = md5($ip);
|
||||
|
||||
if ($settings['security_captcha_to_hour'] > 0) {
|
||||
$key = 'aksrt_send_to_h_' . $target_hash . '_' . wp_date('YmdH');
|
||||
set_transient($key, (int) get_transient($key) + 1, HOUR_IN_SECONDS);
|
||||
}
|
||||
if ($settings['security_captcha_to_day'] > 0) {
|
||||
$key = 'aksrt_send_to_d_' . $target_hash . '_' . wp_date('Ymd');
|
||||
set_transient($key, (int) get_transient($key) + 1, DAY_IN_SECONDS);
|
||||
}
|
||||
if ($settings['security_captcha_ip_10min'] > 0) {
|
||||
$key = sprintf('aksrt_send_ip_m_%s_%s%d', $ip_hash, wp_date('YmdH'), intdiv((int) wp_date('i'), 10));
|
||||
set_transient($key, (int) get_transient($key) + 1, 10 * MINUTE_IN_SECONDS);
|
||||
}
|
||||
if ($settings['security_captcha_ip_day'] > 0) {
|
||||
$key = 'aksrt_send_ip_d_' . $ip_hash . '_' . wp_date('Ymd');
|
||||
set_transient($key, (int) get_transient($key) + 1, DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
$cooldown = $settings['security_captcha_send_cooldown'];
|
||||
if ($cooldown > 0) {
|
||||
set_transient('aksrt_send_cd_' . $target_hash, time(), $cooldown);
|
||||
}
|
||||
return $cooldown;
|
||||
}
|
||||
|
||||
private static function client_ip(): string {
|
||||
return function_exists('aksrt_get_client_ip') ? aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user