97 lines
3.8 KiB
PHP
97 lines
3.8 KiB
PHP
<?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');
|
|
}
|
|
}
|