feat: 支持用户级别IP黑白名单 - 可为单个用户设置独立的IP访问规则

This commit is contained in:
Your Name
2026-05-07 11:17:20 +08:00
parent d92cf341b8
commit dec263312a
6 changed files with 141 additions and 53 deletions
+15 -3
View File
@@ -213,15 +213,23 @@
<p style="color:#666;font-size:13px;line-height:1.8">
<strong>规则说明:</strong><br>
- 支持<strong>单IP</strong>(如 192.168.1.1)、<strong>CIDR</strong>(如 192.168.1.0/24)、<strong>IP范围</strong>(如 192.168.1.1-192.168.1.100<br>
- <strong>白名单</strong>启用后只有白名单中的IP才能连接,黑名单中的IP会被拒绝<br>
- <strong>黑名单</strong>黑名单中的IP将被禁止连接<br>
- 如果没有白名单规则,则所有IP默认允许(除非在黑名单中)
- <strong>全局规则</strong>对所有用户生效,在连接时即检查<br>
- <strong>用户规则</strong>仅对指定用户生效,在用户登录时检查<br>
- <strong>优先级</strong>:全局黑名单 > 全局白名单 > 用户黑名单 > 用户白名单
</p>
</div>
<div class="filter-bar" style="margin-bottom:12px">
<select id="ip-rule-filter" class="input-sm" onchange="loadIPRules()">
<option value="">全部规则</option>
<option value="global">仅全局规则</option>
<option value="user">仅用户规则</option>
</select>
</div>
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>作用范围</th>
<th>IP地址/网段</th>
<th>类型</th>
<th>备注</th>
@@ -376,6 +384,10 @@
</div>
<form id="ip-rule-form">
<input type="hidden" id="ip-rule-edit-id" value="">
<div class="form-group">
<label>作用用户(留空为全局规则)</label>
<input type="text" id="ip-rule-username" placeholder="留空表示全局规则,填入用户名表示仅对该用户生效">
</div>
<div class="form-group">
<label>IP地址/网段</label>
<input type="text" id="ip-rule-ip" placeholder="如: 192.168.1.1 或 192.168.1.0/24 或 10.0.0.1-10.0.0.255" required>
+17 -7
View File
@@ -463,13 +463,20 @@ if (token) {
// --- IP规则管理 ---
async function loadIPRules() {
try {
const rules = await api('GET', '/api/ip-rules');
const filter = document.getElementById('ip-rule-filter').value;
let url = '/api/ip-rules';
if (filter === 'global') url += '?username=__empty__';
else if (filter === 'user') url += '?username=__has__';
const rules = await api('GET', url);
const tbody = document.getElementById('ip-rules-tbody');
if (!rules || !rules.length) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;padding:40px">暂无IP规则,所有IP默认允许连接</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:#999;padding:40px">暂无IP规则,所有IP默认允许连接</td></tr>';
return;
}
tbody.innerHTML = rules.map(r => {
const scopeLabel = r.username
? `<span style="color:#e67e22;font-weight:600">用户: ${r.username}</span>`
: '<span style="color:#667eea;font-weight:600">全局</span>';
const typeLabel = r.type === 'whitelist'
? '<span style="color:#667eea;font-weight:600">白名单</span>'
: '<span style="color:#ff4d4f;font-weight:600">黑名单</span>';
@@ -478,14 +485,15 @@ async function loadIPRules() {
: '<span class="status-disabled">禁用</span>';
return `<tr>
<td>${r.id}</td>
<td>${scopeLabel}</td>
<td><code style="background:#f5f5f5;padding:2px 6px;border-radius:3px">${r.ip}</code></td>
<td>${typeLabel}</td>
<td>${r.note || '-'}</td>
<td>${statusLabel}</td>
<td>${formatTime(r.created_at)}</td>
<td class="action-btns">
<button class="btn btn-sm" onclick="editIPRule(${r.id}, '${r.ip}', '${r.type}', '${(r.note||'').replace(/'/g, "\\'")}', ${r.enabled})">编辑</button>
<button class="btn btn-sm" onclick="toggleIPRule(${r.id}, '${r.ip}', '${r.type}', '${(r.note||'').replace(/'/g, "\\'")}', ${r.enabled})">${r.enabled ? '禁用' : '启用'}</button>
<button class="btn btn-sm" onclick="editIPRule(${r.id}, '${r.username||''}', '${r.ip}', '${r.type}', '${(r.note||'').replace(/'/g, "\\'")}', ${r.enabled})">编辑</button>
<button class="btn btn-sm" onclick="toggleIPRule(${r.id}, '${r.username||''}', '${r.ip}', '${r.type}', '${(r.note||'').replace(/'/g, "\\'")}', ${r.enabled})">${r.enabled ? '禁用' : '启用'}</button>
<button class="btn btn-sm btn-danger" onclick="deleteIPRule(${r.id})">删除</button>
</td>
</tr>`;
@@ -503,9 +511,10 @@ function showAddIPRule() {
document.getElementById('ip-rule-modal').style.display = 'flex';
}
function editIPRule(id, ip, type, note, enabled) {
function editIPRule(id, username, ip, type, note, enabled) {
document.getElementById('ip-rule-modal-title').textContent = '编辑IP规则';
document.getElementById('ip-rule-edit-id').value = id;
document.getElementById('ip-rule-username').value = username;
document.getElementById('ip-rule-ip').value = ip;
document.getElementById('ip-rule-type').value = type;
document.getElementById('ip-rule-note').value = note;
@@ -521,6 +530,7 @@ document.getElementById('ip-rule-form').addEventListener('submit', async (e) =>
e.preventDefault();
const editId = document.getElementById('ip-rule-edit-id').value;
const data = {
username: document.getElementById('ip-rule-username').value,
ip: document.getElementById('ip-rule-ip').value,
type: document.getElementById('ip-rule-type').value,
note: document.getElementById('ip-rule-note').value,
@@ -541,10 +551,10 @@ document.getElementById('ip-rule-form').addEventListener('submit', async (e) =>
}
});
async function toggleIPRule(id, ip, type, note, enabled) {
async function toggleIPRule(id, username, ip, type, note, enabled) {
try {
await api('PUT', '/api/ip-rules/' + id, {
ip, type, note, enabled: !enabled
username, ip, type, note, enabled: !enabled
});
showToast(!enabled ? '规则已启用' : '规则已禁用');
loadIPRules();