feat(security): 新增网站安全扩展模块并支持修改手机脱敏验证

This commit is contained in:
kate522 committed 2026-10-06 09:26:55 +08:00
1 parent 224f86ec08
commit af72364078
30 files changed
+1389 -73

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-avatar.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-domain-model.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-template-tags.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-frontend-tags.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-phone.php';
// Shared comment kernel.
require_once AKSRT_DIR . '/inc/helpers/aksrt-ip-geo.php';
require_once AKSRT_DIR . '/inc/helpers/comments/content.php';
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* Phone number masking helpers shared by the account UI and the
* phone-match verification endpoint.
*
* The hidden middle segment adapts to the number: domestic 11-digit
* numbers hide 4 digits (138****1234), longer international numbers
* hide length-7 digits (min 4), so the digit-entry UI renders one box
* per hidden digit.
*
* @package AKSRT
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Split a bound phone number into visible prefix / hidden middle / visible suffix.
*
* Works on the digit core of the number (a leading "+" is ignored for the split
* but reported back). Returns null when the number holds fewer than 8 digits —
* such numbers cannot be verified by middle-digit entry.
*
* @return array{prefix:string,hidden:string,suffix:string,hidden_length:int,leading_plus:bool}|null
*/
function aksrt_phone_mask_parts(string $phone): ?array {
$phone = trim($phone);
if ($phone === '') {
return null;
}
$leading_plus = $phone[0] === '+';
$core = preg_replace('/\D+/', '', $phone);
$length = strlen($core);
if ($length < 8) {
return null;
}
$hidden_length = min(11, max(4, $length - 7));
$suffix_length = $length - 3 - $hidden_length;
if ($suffix_length < 1) {
return null;
}
return [
'prefix' => substr($core, 0, 3),
'hidden' => substr($core, 3, $hidden_length),
'suffix' => substr($core, $length - $suffix_length),
'hidden_length' => $hidden_length,
'leading_plus' => $leading_plus,
];
}
/**
* Masked display form of a phone number, e.g. 138****1234 or +861******1234.
* Falls back to the raw number when it cannot be masked.
*/
function aksrt_phone_mask_display(string $phone): string {
$parts = aksrt_phone_mask_parts($phone);
if ($parts === null) {
return $phone;
}
$mask = str_repeat('*', (int) $parts['hidden_length']);
$display = $parts['prefix'] . $mask . $parts['suffix'];
return $parts['leading_plus'] ? '+' . $display : $display;
}
@@ -11,7 +11,7 @@ $account_user = isset($target_user) && $target_user instanceof WP_User ? $target
$account_user_id = $account_user instanceof WP_User ? (int) $account_user->ID : (int) ($user_id ?? 0);
$account_email = $account_user instanceof WP_User ? (string) $account_user->user_email : '';
$account_phone = $account_user_id > 0 ? (string) get_user_meta($account_user_id, 'phone_number', true) : '';
$account_phone_display = strlen($account_phone) > 10 ? substr_replace($account_phone, '****', 3, 4) : $account_phone;
$account_phone_display = function_exists('aksrt_phone_mask_display') ? aksrt_phone_mask_display($account_phone) : $account_phone;
$account_oauth_new = $account_user_id > 0 && get_user_meta($account_user_id, 'oauth_new', true);
$user_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$sms_settings = class_exists('\AKSRT\Sms\SMS') ? \AKSRT\Sms\SMS::get_instance()->get_provider_config() : [];
@@ -147,7 +147,29 @@ $render_phone_form = static function () use ($agreement): void {
<?php if ($account_phone !== '') : ?>
<ul class="mb20 step-simple account-step-list" data-account-step-list="phone"><li class="active"><span><?php esc_html_e('验证老手机', 'aksrt'); ?></span></li><li><span><?php esc_html_e('验证新手机', 'aksrt'); ?></span></li><li><span><?php esc_html_e('修改成功', 'aksrt'); ?></span></li></ul>
<div class="account-step-panels">
<div class="account-step is-active" data-account-step="phone-old"><p class="muted-2-color"><?php printf(esc_html__('请在下方获取验证码以验证您的老手机号 %s', 'aksrt'), '<br><span class="badg">' . esc_html($account_phone_display) . '</span>'); ?></p><form id="phone-old-verify-form"><input type="hidden" name="phone" value="<?php echo esc_attr($account_phone); ?>"><div class="mb20"><?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('bind_phone'); ?><?php endif; ?><div class="relative line-form mb10 modal-line-field modal-line-field--code"><input type="text" name="code" class="line-form-input" maxlength="6" inputmode="numeric" autocomplete="one-time-code" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><span class="yztx abs-right"><button type="button" class="but c-blue code-send-btn account-code-send" data-aksrt-sms-trigger data-aksrt-sms-scene="verify_old_phone"><?php esc_html_e('发送验证码', 'aksrt'); ?></button></span><i class="line-form-line"></i></div></div><button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-shield-alt" aria-hidden="true"></i> <?php esc_html_e('立即验证', 'aksrt'); ?></button></form></div>
<div class="account-step is-active" data-account-step="phone-old">
<?php $phone_mask = function_exists('aksrt_phone_mask_parts') ? aksrt_phone_mask_parts($account_phone) : null; ?>
<?php if ($phone_mask !== null) : ?>
<p class="muted-2-color"><?php printf(esc_html__('为确认是您本人操作,请补全绑定手机号 %s 中缺失的数字', 'aksrt'), '<br><span class="badg">' . esc_html($account_phone_display) . '</span>'); ?></p>
<?php else : ?>
<p class="muted-2-color"><?php printf(esc_html__('请在下方获取验证码以验证您的老手机号 %s', 'aksrt'), '<br><span class="badg">' . esc_html($account_phone_display) . '</span>'); ?></p>
<?php endif; ?>
<form id="phone-old-verify-form" novalidate>
<?php if ($phone_mask !== null) : ?>
<div class="mb20 aksrt-phone-digits" data-aksrt-phone-digits data-hidden-length="<?php echo esc_attr((string) $phone_mask['hidden_length']); ?>" role="group" aria-label="<?php esc_attr_e('请补全手机号中缺失的数字', 'aksrt'); ?>">
<?php for ($phone_digit_index = 0; $phone_digit_index < (int) $phone_mask['hidden_length']; $phone_digit_index++) : ?>
<input type="text" class="aksrt-phone-digit" inputmode="numeric" maxlength="1" autocomplete="one-time-code" data-digit-index="<?php echo esc_attr((string) $phone_digit_index); ?>" aria-label="<?php echo esc_attr(sprintf(__('手机号第 %d 位数字', 'aksrt'), 4 + $phone_digit_index)); ?>">
<?php endfor; ?>
</div>
<p class="aksrt-phone-digits__error" data-aksrt-phone-digits-error hidden></p>
<?php endif; ?>
<div class="mb20" data-aksrt-phone-code-area<?php echo $phone_mask !== null ? ' hidden' : ''; ?>>
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('bind_phone'); ?><?php endif; ?>
<div class="relative line-form mb10 modal-line-field modal-line-field--code"><input type="text" name="code" class="line-form-input" maxlength="6" inputmode="numeric" autocomplete="one-time-code" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><span class="yztx abs-right"><button type="button" class="but c-blue code-send-btn account-code-send" data-aksrt-sms-trigger data-aksrt-sms-scene="verify_old_phone"><?php esc_html_e('发送验证码', 'aksrt'); ?></button></span><i class="line-form-line"></i></div>
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-shield-alt" aria-hidden="true"></i> <?php esc_html_e('立即验证', 'aksrt'); ?></button>
</div>
</form>
</div>
<div class="account-step" data-account-step="phone-new"><p class="muted-2-color"><?php esc_html_e('请输入您需要修改的新手机号码', 'aksrt'); ?></p><?php $render_phone_form(); ?></div>
<div class="account-step" data-account-step="phone-success"><div class="text-center padding-h10"><i class="fa fas fa-check-circle c-green em3x mb10" aria-hidden="true"></i><p><?php esc_html_e('手机号修改成功', 'aksrt'); ?></p></div></div>
</div>
+27 -24
View File
@@ -92,21 +92,6 @@ function aksrt_account_consume_proof(int $user_id, string $kind, string $target,
return true;
}
function aksrt_account_rate_limited(int $user_id, string $scene): bool {
$key = 'aksrt_account_rate_' . sanitize_key($scene) . '_' . $user_id;
$count = (int) get_transient($key);
if ($count >= 8) {
return true;
}
set_transient($key, $count + 1, 10 * MINUTE_IN_SECONDS);
return false;
}
function aksrt_account_reset_rate(int $user_id, string $scene): void {
delete_transient('aksrt_account_rate_' . sanitize_key($scene) . '_' . $user_id);
}
function aksrt_account_email_verification_required(): bool {
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
return !empty($settings['email_set_captch']);
@@ -155,11 +140,12 @@ add_action('wp_ajax_aksrt_send_account_email_code', function (): void {
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
}
}
$rate_key = 'aksrt_account_email_rate_' . $user_id . '_' . $scene . '_' . md5(strtolower($target));
if (get_transient($rate_key)) {
wp_send_json_error(['message' => __('操作过于频繁,请稍后再试', 'aksrt')]);
if (class_exists('\AKSRT\Extensions\SendLimit')) {
$limit = \AKSRT\Extensions\SendLimit::check($target);
if (empty($limit['ok'])) {
wp_send_json_error(['message' => (string) $limit['message']]);
}
}
set_transient($rate_key, true, 60);
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$sent = class_exists('\AKSRT\Settings\Settings')
@@ -170,16 +156,16 @@ add_action('wp_ajax_aksrt_send_account_email_code', function (): void {
])
: false;
if (!$sent) {
delete_transient($rate_key);
wp_send_json_error(['message' => __('验证码邮件发送失败,请稍后重试', 'aksrt')]);
}
$cooldown = class_exists('\AKSRT\Extensions\SendLimit') ? (int) \AKSRT\Extensions\SendLimit::record($target) : 0;
aksrt_account_store_code($user_id, $scene, $target, $code);
wp_send_json_success([
'message' => $scene === 'verify_email'
? __('验证码已发送到您的老邮箱', 'aksrt')
: __('验证码已发送到新邮箱', 'aksrt'),
'countdown' => 60,
'countdown' => $cooldown > 0 ? $cooldown : 60,
]);
});
@@ -208,9 +194,15 @@ add_action('wp_ajax_aksrt_verify_account_email', function (): void {
$code = sanitize_text_field((string) wp_unslash($_POST['code'] ?? ''));
if (!aksrt_account_consume_code($user_id, 'verify_email', (string) $user->user_email, $code)) {
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
\AKSRT\Extensions\SecurityHardening::error_frequency_record();
}
wp_send_json_error(['message' => __('验证码错误或已过期', 'aksrt')]);
}
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
\AKSRT\Extensions\SecurityHardening::error_frequency_clear();
}
aksrt_account_set_proof($user_id, 'email_old', (string) $user->user_email);
wp_send_json_success(['message' => __('验证成功', 'aksrt')]);
});
@@ -250,10 +242,13 @@ add_action('wp_ajax_aksrt_change_password', function (): void {
if ($old_password === $password) {
wp_send_json_error(['message' => __('新密码和原密码不能相同', 'aksrt')]);
}
if (aksrt_account_rate_limited($user_id, 'change_password')) {
wp_send_json_error(['message' => __('错误次数过多,请稍后再试', 'aksrt')]);
if (class_exists('\AKSRT\Extensions\SecurityHardening') && ($wait = \AKSRT\Extensions\SecurityHardening::error_frequency_wait()) > 0) {
wp_send_json_error(['message' => sprintf(__('验证错误次数过多,请 %d 秒后再试', 'aksrt'), $wait)]);
}
if (!wp_check_password($old_password, $user->user_pass, $user_id)) {
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
\AKSRT\Extensions\SecurityHardening::error_frequency_record();
}
wp_send_json_error(['message' => __('原密码错误', 'aksrt')]);
}
}
@@ -271,7 +266,9 @@ add_action('wp_ajax_aksrt_change_password', function (): void {
}
delete_user_meta($user_id, 'oauth_new');
aksrt_account_reset_rate($user_id, 'change_password');
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
\AKSRT\Extensions\SecurityHardening::error_frequency_clear();
}
wp_send_json_success([
'message' => $oauth_new
? __('密码设置成功', 'aksrt')
@@ -335,6 +332,9 @@ add_action('wp_ajax_aksrt_change_email', function (): void {
false
)
) {
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
\AKSRT\Extensions\SecurityHardening::error_frequency_record();
}
wp_send_json_error(['message' => __('验证码错误或已过期', 'aksrt')]);
}
@@ -349,6 +349,9 @@ add_action('wp_ajax_aksrt_change_email', function (): void {
if ($verification_required) {
delete_transient(aksrt_account_code_key($user_id, 'change_email', $new_email));
}
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
\AKSRT\Extensions\SecurityHardening::error_frequency_clear();
}
update_user_meta($user_id, 'aksrt_email_verified', 1);
if ($old_email !== '' && is_email($old_email) && class_exists('\AKSRT\Settings\Settings')) {
\AKSRT\Settings\Settings::send_event_email('change_notice', $old_email, [