feat(security): 新增网站安全扩展模块并支持修改手机脱敏验证
This commit is contained in:
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%;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in new issue
Block a user