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

@@ -36,6 +36,16 @@ export const extensionsSection = {
canonicalDomain: 'extensions',
section: 'push',
}),
schemaPage({
key: 'extensions-security',
name: 'ExtensionsSecurity',
label: '网站安全',
route: '/extensions/security',
title: '网站安全',
desc: '防刷限制、验证码发送限制与站点安全加固。',
canonicalDomain: 'extensions',
section: 'security',
}),
customPage({
key: 'extensions-ai',
name: 'ExtensionAI',
@@ -6,9 +6,11 @@
import { ipSchema } from './ip.js'
import { auditSchema } from './audit.js'
import { pushSchema } from './push.js'
import { securitySchema } from './security.js'
export const extensionsSchemas = {
ip: ipSchema,
audit: auditSchema,
push: pushSchema,
security: securitySchema,
}
@@ -0,0 +1,105 @@
export const securitySchema = {
key: 'extensions.security',
title: '网站安全',
icon: 'fa fas fa-umbrella',
description: '防刷限制、验证码发送限制与站点安全加固。',
defaults: {
security_brush_post_10min: 20,
security_brush_post_day: 50,
security_brush_comment_10min: 20,
security_brush_comment_day: 100,
security_brush_upload_10min: 50,
security_brush_upload_day: 200,
security_brush_register_10min: 20,
security_brush_register_day: 100,
security_brush_order_10min: 30,
security_brush_order_day: 100,
security_captcha_send_cooldown: 60,
security_captcha_to_hour: 15,
security_captcha_to_day: 30,
security_captcha_ip_10min: 20,
security_captcha_ip_day: 50,
security_error_frequency_limit: true,
security_disabled_pingback: true,
security_disabled_xmlrpc: true,
security_user_disable_admin: true,
close_attachment_page: true,
},
groups: [
{
key: 'brush_post',
title: '[防刷机制]发布限制',
icon: 'fa fas fa-pen',
description: '限制单个用户发布文章、帖子、板块的总数量(为0则不限制)。',
fields: [
{ key: 'security_brush_post_10min', label: '10分钟内最多发布 (篇)', type: 'number', default: 20, min: 0, max: 200 },
{ key: 'security_brush_post_day', label: '一天内最多发布 (篇)', type: 'number', default: 50, min: 0, max: 2000 },
],
},
{
key: 'brush_comment',
title: '[防刷机制]评论限制',
icon: 'fa fas fa-comments',
description: '限制单个用户发布评论的总数量(为0则不限制)。',
fields: [
{ key: 'security_brush_comment_10min', label: '10分钟内最多评论 (条)', type: 'number', default: 20, min: 0, max: 200 },
{ key: 'security_brush_comment_day', label: '一天内最多评论 (条)', type: 'number', default: 100, min: 0, max: 2000, help: '如果启用了免登陆可评论,则根据用户IP地址判断' },
],
},
{
key: 'brush_upload',
title: '[防刷机制]上传限制',
icon: 'fa fas fa-upload',
description: '限制单个用户上传内容的总数量(为0则不限制)。防刷机制主要为了防止用户恶意多次提交内容,请根据网站的用户量合理设置,此机制不会限制管理员。',
fields: [
{ key: 'security_brush_upload_10min', label: '10分钟内最多上传 (个)', type: 'number', default: 50, min: 0, max: 200 },
{ key: 'security_brush_upload_day', label: '一天内最多上传 (个)', type: 'number', default: 200, min: 0, max: 2000 },
],
},
{
key: 'brush_register',
title: '[防刷机制]注册限制',
icon: 'fa fas fa-user-plus',
description: '限制每个IP地址用户注册数量,此项无法判断管理员身份,所以管理员也会受到限制。',
fields: [
{ key: 'security_brush_register_10min', label: '10分钟内最多注册 (个)', type: 'number', default: 20, min: 0, max: 200 },
{ key: 'security_brush_register_day', label: '一天内最多注册 (个)', type: 'number', default: 100, min: 0, max: 2000, help: '如果您的服务器不能正确的获取用户真实IP地址,请关闭此项(0为不限制)' },
],
},
{
key: 'brush_order',
title: '[防刷机制]下单限制',
icon: 'fa fas fa-shopping-cart',
description: '限制每个用户下单数量,未登录用户按IP地址判断。',
fields: [
{ key: 'security_brush_order_10min', label: '10分钟内最多下单 (个订单)', type: 'number', default: 30, min: 0, max: 200 },
{ key: 'security_brush_order_day', label: '一天内最多下单 (个订单)', type: 'number', default: 100, min: 0, max: 2000, help: '如果您的服务器不能正确的获取用户真实IP地址,请关闭此项(0为不限制)' },
],
},
{
key: 'brush_captcha',
title: '[防刷机制]验证码发送限制',
icon: 'fa fas fa-paper-plane',
description: '限制验证码邮件/短信的发送频率,防止恶意程序批量轰炸特定邮箱或手机号。',
fields: [
{ key: 'security_captcha_send_cooldown', label: '同一收件人/IP发送冷却 (秒)', type: 'number', default: 60, min: 0, max: 600, help: '同一邮箱或手机号、同一IP地址发送验证码后,需间隔此时间才可再次发送(0为不限制)' },
{ key: 'security_captcha_to_hour', label: '同一收件人1小时内最多 (次)', type: 'number', default: 15, min: 0, max: 500 },
{ key: 'security_captcha_to_day', label: '同一收件人一天内最多 (次)', type: 'number', default: 30, min: 0, max: 1000 },
{ key: 'security_captcha_ip_10min', label: '同一IP 10分钟内最多 (次)', type: 'number', default: 20, min: 0, max: 500 },
{ key: 'security_captcha_ip_day', label: '同一IP一天内最多 (次)', type: 'number', default: 50, min: 0, max: 2000, help: '如果您的服务器不能正确的获取用户真实IP地址,请关闭此项(0为不限制)' },
],
},
{
key: 'harden',
title: '安全加固',
icon: 'fa fas fa-shield-alt',
fields: [
{ key: 'security_error_frequency_limit', label: '错误频率限制', type: 'switch', default: true, help: '用户进行密码、邮箱、手机号、验证码等验证时,验证错误次数超过5次后限制30秒验证一次,错误超过10次后,限制3分钟验证一次,有效防止暴力破解' },
{ key: 'security_disabled_pingback', label: '防pingback攻击', type: 'switch', default: true },
{ key: 'security_disabled_xmlrpc', label: '禁用XML-RPC', type: 'switch', default: true, help: '如果无需使用xmlrpc功能,建议直接删除WordPress目录下的xmlrpc.php文件' },
{ key: 'security_user_disable_admin', label: '非管理员禁止进入后台', type: 'switch', default: true, help: '为了安全,如非必要请勿允许非管理员进入后台' },
{ key: 'close_attachment_page', label: '非管理员屏蔽附件页面', type: 'switch', default: true, help: '非管理员前台不显示附件(图片、文件等)页面,直接显示404,能有效提高文件的安全性' },
],
},
],
}
@@ -240,7 +240,6 @@ export const permissionsSchemas = {
],
newfilename: false,
newfilename_type: 'time',
close_attachment_page: true,
},
groups: [
{
@@ -333,7 +332,6 @@ export const permissionsSchemas = {
{ label: '随机字符串', value: 'random' },
],
},
{ key: 'close_attachment_page', label: '非管理员屏蔽附件页面', type: 'switch', help: '开启后非管理员访问附件(图片、文件等)页面将直接显示 404,可有效提高文件的安全性。' },
],
},
],
@@ -716,6 +716,48 @@
.aksrt-settings-modal .code-send-btn.is-counting { pointer-events: none; opacity: .6; }
.aksrt-settings-modal .aksrt-captcha-wrap { margin: 12px 0 8px; }
/* 修改手机:脱敏中间位数字框 */
.aksrt-settings-modal .aksrt-phone-digits {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
.aksrt-settings-modal .aksrt-phone-digit {
width: 48px;
height: 52px;
padding: 0;
border: 1px solid var(--color-line-soft, rgba(0, 0, 0, 0.12));
border-radius: 8px;
background: transparent;
color: var(--color-ink, #333);
font-size: 22px;
font-weight: 600;
line-height: 1;
text-align: center;
caret-color: var(--color-primary, #2a7de1);
outline: none;
transition: border-color .15s ease, box-shadow .15s ease;
}
.aksrt-settings-modal .aksrt-phone-digit:focus {
border-color: var(--color-primary, #2a7de1);
box-shadow: 0 0 0 3px rgba(42, 125, 225, 0.15);
}
.aksrt-settings-modal .aksrt-phone-digits.has-error .aksrt-phone-digit {
border-color: var(--color-danger, #e02020);
box-shadow: 0 0 0 3px rgba(224, 32, 32, 0.12);
}
.aksrt-settings-modal .aksrt-phone-digits__error {
margin: 0 0 12px;
color: var(--color-danger, #e02020);
font-size: 13px;
text-align: center;
}
@media (max-width: 640px) {
.aksrt-modal-runtime.aksrt-user-settings-modal.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog {
width: 100%;
+137 -3
View File
@@ -824,16 +824,30 @@
function sendPhoneCode(button, form, stage) {
if (!form || button.disabled) return;
var phone = String((form.querySelector('[name="phone"]') || {}).value || '').trim();
if (!phone) { notyf('请输入手机号', 'warning'); return; }
var payload = { stage: stage };
if (stage === 'verify_old_phone') {
var dialog = form.closest('[data-account-dialog="phone"]');
var boxes = dialog ? phoneDigitBoxes(dialog) : [];
if (boxes.length) {
var hiddenLength = parseInt(dialog.querySelector('[data-aksrt-phone-digits]').getAttribute('data-hidden-length'), 10) || boxes.length;
var middle = phoneDigitsValue(dialog);
if (middle.length !== hiddenLength) { notyf('请先补全手机号缺失的数字', 'warning'); return; }
payload.middle = middle;
}
} else {
var phone = String((form.querySelector('[name="phone"]') || {}).value || '').trim();
if (!phone) { notyf('请输入手机号', 'warning'); return; }
payload.phone = phone;
}
runCaptcha('bind_phone', form).then(function (captchaPayload) {
if (captchaPayload === null) return null;
var restRoot = settingsRestRoot();
var nonce = settingsRestNonce();
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
var request = fetch(restRoot + '/user/phone-code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
body: JSON.stringify(Object.assign({ phone: phone, stage: stage }, captchaPayload || {}))
body: JSON.stringify(payload)
});
button.disabled = true;
return request;
@@ -902,6 +916,116 @@
}).finally(function () { resetCaptcha('bind_phone', form); });
}
/* ---- 修改手机:脱敏中间位数字框(格数随号码长度变化) ---- */
function phoneDigitBoxes(dialog) {
return Array.prototype.slice.call(dialog.querySelectorAll('[data-aksrt-phone-digits] .aksrt-phone-digit'));
}
function phoneDigitsValue(dialog) {
return phoneDigitBoxes(dialog).map(function (input) { return String(input.value || '').replace(/\D/g, ''); }).join('');
}
function setPhoneDigitsError(dialog, message) {
var group = dialog.querySelector('[data-aksrt-phone-digits]');
var error = dialog.querySelector('[data-aksrt-phone-digits-error]');
if (error) {
error.textContent = message || '';
error.hidden = !message;
}
if (group) group.classList.toggle('has-error', !!message);
}
function hidePhoneCodeArea(dialog, options) {
var area = dialog.querySelector('[data-aksrt-phone-code-area]');
if (!area) return;
var wasVisible = !area.hidden;
area.hidden = true;
area.querySelectorAll('.account-code-send,[data-aksrt-sms-trigger]').forEach(resetCountdown);
if (wasVisible && options && options.destroyCode) {
// 中间位被修改:销毁服务端已发放的老号验证码状态。
fetch(settingsRestRoot() + '/user/phone-code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
body: JSON.stringify({ stage: 'reset_old_phone' })
}).catch(function () {});
}
}
function verifyPhoneMiddle(dialog) {
var group = dialog.querySelector('[data-aksrt-phone-digits]');
var boxes = phoneDigitBoxes(dialog);
if (!group || !boxes.length) return;
var hiddenLength = parseInt(group.getAttribute('data-hidden-length'), 10) || boxes.length;
var middle = phoneDigitsValue(dialog);
if (middle.length !== hiddenLength) return;
dialog.__aksrtPhoneMatchTicket = (dialog.__aksrtPhoneMatchTicket || 0) + 1;
var ticket = dialog.__aksrtPhoneMatchTicket;
fetch(settingsRestRoot() + '/user/phone-match', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
body: JSON.stringify({ middle: middle })
}).then(function (response) { return response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }); }).then(function (result) {
if (ticket !== dialog.__aksrtPhoneMatchTicket) return;
if (result.ok && result.payload.success) {
var area = dialog.querySelector('[data-aksrt-phone-code-area]');
if (area) area.hidden = false;
setPhoneDigitsError(dialog, '');
} else {
hidePhoneCodeArea(dialog, { destroyCode: true });
setPhoneDigitsError(dialog, (result.payload && result.payload.message) || '填写的号码与绑定手机号不一致');
}
}).catch(function () {
if (ticket === dialog.__aksrtPhoneMatchTicket) {
hidePhoneCodeArea(dialog, { destroyCode: false });
setPhoneDigitsError(dialog, '网络错误,请重试');
}
});
}
function initPhoneDigits(dialog) {
var boxes = phoneDigitBoxes(dialog);
if (!boxes.length || dialog.dataset.aksrtDigitsBound === 'true') return;
dialog.dataset.aksrtDigitsBound = 'true';
boxes.forEach(function (input, index) {
input.addEventListener('input', function () {
var value = String(input.value || '').replace(/\D/g, '');
input.value = value === '' ? '' : value.charAt(value.length - 1);
hidePhoneCodeArea(dialog, { destroyCode: true });
setPhoneDigitsError(dialog, '');
if (input.value !== '' && index < boxes.length - 1) boxes[index + 1].focus();
verifyPhoneMiddle(dialog);
});
input.addEventListener('keydown', function (event) {
if (event.key === 'Backspace' && !input.value && index > 0) {
event.preventDefault();
boxes[index - 1].value = '';
boxes[index - 1].focus();
hidePhoneCodeArea(dialog, { destroyCode: true });
setPhoneDigitsError(dialog, '');
} else if (event.key === 'ArrowLeft' && index > 0) {
boxes[index - 1].focus();
} else if (event.key === 'ArrowRight' && index < boxes.length - 1) {
boxes[index + 1].focus();
}
});
input.addEventListener('paste', function (event) {
var text = String((event.clipboardData || window.clipboardData).getData('text') || '');
var digits = text.replace(/\D/g, '');
if (!digits) return;
event.preventDefault();
for (var offset = 0; offset < boxes.length - index && offset < digits.length; offset++) {
boxes[index + offset].value = digits.charAt(offset);
}
boxes[Math.min(index + digits.length, boxes.length - 1)].focus();
hidePhoneCodeArea(dialog, { destroyCode: true });
setPhoneDigitsError(dialog, '');
verifyPhoneMiddle(dialog);
});
});
}
function sendRecoveryCode(button, form) {
if (!form || button.disabled) return;
var target = String((form.querySelector('[name="target"]') || {}).value || '').trim();
@@ -1031,6 +1155,7 @@
});
});
} else if (type === 'phone') {
initPhoneDigits(dialog);
dialog.querySelectorAll('[data-aksrt-sms-trigger]').forEach(function (button) {
button.addEventListener('click', function () { sendPhoneCode(button, button.closest('form'), button.getAttribute('data-aksrt-sms-scene')); });
});
@@ -1054,6 +1179,15 @@
if (!shell || !shell.classList.contains('aksrt-user-settings-modal')) return;
shell.querySelectorAll('.account-code-send,[data-aksrt-sms-trigger],[data-account-recovery-send]').forEach(resetCountdown);
shell.querySelectorAll('form').forEach(function (form) { form.reset(); });
shell.querySelectorAll('[data-account-dialog="phone"]').forEach(function (dialog) {
dialog.querySelectorAll('.aksrt-phone-digit').forEach(function (input) { input.value = ''; });
var group = dialog.querySelector('[data-aksrt-phone-digits]');
if (group) group.classList.remove('has-error');
var error = dialog.querySelector('[data-aksrt-phone-digits-error]');
if (error) { error.hidden = true; error.textContent = ''; }
var area = dialog.querySelector('[data-aksrt-phone-code-area]');
if (area) area.hidden = !!group;
});
}
if (document.documentElement.dataset.aksrtSettingsModalCleanupInitialized !== 'true') {