From aeb0696f457ca85b62d15ab6586c5779cf5cd97b Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Fri, 31 Jul 2026 16:56:21 +0800
Subject: [PATCH] feat(firewall): 添加访问日志功能
---
src/main/java/cc/mrbird/febs/firewall/controller/AdminFirewallController.java | 9 +
src/main/java/cc/mrbird/febs/firewall/mapper/FirewallAccessLogMapper.java | 15 ++
src/main/resources/templates/febs/views/modules/system/firewallAccessLogList.html | 178 +++++++++++++++++++++++++
src/main/java/cc/mrbird/febs/firewall/controller/ApiTrackController.java | 52 +++++++
src/main/java/cc/mrbird/febs/firewall/service/IFirewallRuleService.java | 13 +
sql/firewall.sql | 26 +++
src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java | 22 +++
src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java | 6
src/main/java/cc/mrbird/febs/firewall/entity/FirewallAccessLog.java | 51 +++++++
9 files changed, 372 insertions(+), 0 deletions(-)
diff --git a/sql/firewall.sql b/sql/firewall.sql
index 36d889d..e2419e7 100644
--- a/sql/firewall.sql
+++ b/sql/firewall.sql
@@ -59,6 +59,32 @@
INDEX idx_ip (IP)
) COMMENT = '防火墙IP白名单表';
+-- ============================================================
+-- 访问日志表
+-- ============================================================
+DROP TABLE IF EXISTS firewall_access_log;
+CREATE TABLE firewall_access_log (
+ ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+ REVISION INT DEFAULT 1 COMMENT '乐观锁',
+ CREATED_BY VARCHAR(32) DEFAULT 'system' COMMENT '创建人',
+ CREATED_TIME DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ UPDATED_BY VARCHAR(32) DEFAULT 'system' COMMENT '更新人',
+ UPDATED_TIME DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ SITE_ID BIGINT COMMENT '站点ID',
+ IP VARCHAR(64) NOT NULL COMMENT '访问者IP',
+ COUNTRY_CODE VARCHAR(10) COMMENT '国家编码(ISO 3166-1 alpha-2)',
+ COUNTRY_NAME VARCHAR(50) COMMENT '国家名称',
+ USER_AGENT VARCHAR(500) COMMENT '浏览器UA',
+ REQUEST_URI VARCHAR(500) COMMENT '请求路径',
+ IS_BLOCKED TINYINT DEFAULT 0 COMMENT '是否被拦截:1-是,0-否',
+ PRIMARY KEY (ID),
+ INDEX idx_site_id (SITE_ID),
+ INDEX idx_ip (IP),
+ INDEX idx_country (COUNTRY_CODE),
+ INDEX idx_created_time (CREATED_TIME),
+ INDEX idx_blocked (IS_BLOCKED)
+) COMMENT = '防火墙访问日志表';
+
-- ----------------------------
-- 默认数据
-- ----------------------------
diff --git a/src/main/java/cc/mrbird/febs/firewall/controller/AdminFirewallController.java b/src/main/java/cc/mrbird/febs/firewall/controller/AdminFirewallController.java
index 3727037..ed4888f 100644
--- a/src/main/java/cc/mrbird/febs/firewall/controller/AdminFirewallController.java
+++ b/src/main/java/cc/mrbird/febs/firewall/controller/AdminFirewallController.java
@@ -6,6 +6,7 @@
import cc.mrbird.febs.common.entity.QueryRequest;
import cc.mrbird.febs.firewall.dto.CountryVo;
import cc.mrbird.febs.firewall.dto.SaveCountryRuleDto;
+import cc.mrbird.febs.firewall.entity.FirewallAccessLog;
import cc.mrbird.febs.firewall.entity.FirewallCountryRule;
import cc.mrbird.febs.firewall.entity.FirewallIpWhite;
import cc.mrbird.febs.firewall.entity.FirewallSite;
@@ -186,4 +187,12 @@
public FebsResponse deleteIpWhite(@PathVariable @NotNull Long id) {
return firewallRuleService.deleteIpWhite(id);
}
+
+ // ==================== 访问日志查询 ====================
+
+ @GetMapping("/accessLogList")
+ public FebsResponse accessLogList(FirewallAccessLog log, QueryRequest request) {
+ Map<String, Object> data = getDataTable(firewallRuleService.accessLogList(log, request));
+ return new FebsResponse().success().data(data);
+ }
}
diff --git a/src/main/java/cc/mrbird/febs/firewall/controller/ApiTrackController.java b/src/main/java/cc/mrbird/febs/firewall/controller/ApiTrackController.java
new file mode 100644
index 0000000..9eda9ea
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/firewall/controller/ApiTrackController.java
@@ -0,0 +1,52 @@
+package cc.mrbird.febs.firewall.controller;
+
+import cc.mrbird.febs.common.entity.FebsResponse;
+import cc.mrbird.febs.firewall.entity.FirewallAccessLog;
+import cc.mrbird.febs.firewall.service.IFirewallRuleService;
+import cn.hutool.extra.servlet.ServletUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 公开访问追踪API(无需登录)
+ * <p>
+ * 前端 SPA 页面加载时调用此接口记录访问。
+ * Nginx 需配置 proxy_set_header 传递国家和真实IP:
+ * <pre>
+ * proxy_set_header X-Real-IP $remote_addr;
+ * proxy_set_header X-Geo-Country $geoip_country_code;
+ * </pre>
+ *
+ * @author auto-generated
+ * @date 2026-07-31
+ */
+@Slf4j
+@RestController
+@RequestMapping("api/track")
+@RequiredArgsConstructor
+public class ApiTrackController {
+
+ private final IFirewallRuleService firewallRuleService;
+
+ @GetMapping("visit")
+ public FebsResponse trackVisit(HttpServletRequest request) {
+ try {
+ FirewallAccessLog accessLog = new FirewallAccessLog();
+ accessLog.setIp(ServletUtil.getClientIP(request));
+ accessLog.setCountryCode(request.getHeader("X-Geo-Country"));
+ accessLog.setUserAgent(request.getHeader("User-Agent"));
+ accessLog.setRequestUri(request.getHeader("Referer"));
+ accessLog.setIsBlocked(0);
+ firewallRuleService.recordAccess(accessLog);
+ return new FebsResponse().success();
+ } catch (Exception e) {
+ log.error("访问追踪记录失败", e);
+ return new FebsResponse().fail().message("记录失败");
+ }
+ }
+}
diff --git a/src/main/java/cc/mrbird/febs/firewall/entity/FirewallAccessLog.java b/src/main/java/cc/mrbird/febs/firewall/entity/FirewallAccessLog.java
new file mode 100644
index 0000000..00355a3
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/firewall/entity/FirewallAccessLog.java
@@ -0,0 +1,51 @@
+package cc.mrbird.febs.firewall.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 防火墙访问日志
+ *
+ * @author auto-generated
+ * @date 2026-07-31
+ */
+@Data
+@TableName("firewall_access_log")
+public class FirewallAccessLog implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "ID", type = IdType.AUTO)
+ private Long id;
+
+ private Integer revision;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+ private Date createdTime;
+
+ private Long siteId;
+
+ /** 访问者IP */
+ private String ip;
+
+ /** 国家编码(ISO 3166-1 alpha-2,如 CN/US/JP) */
+ private String countryCode;
+
+ /** 国家名称 */
+ private String countryName;
+
+ /** 浏览器UA */
+ private String userAgent;
+
+ /** 请求路径 */
+ private String requestUri;
+
+ /** 是否被拦截:1-是,0-否 */
+ private Integer isBlocked;
+}
diff --git a/src/main/java/cc/mrbird/febs/firewall/mapper/FirewallAccessLogMapper.java b/src/main/java/cc/mrbird/febs/firewall/mapper/FirewallAccessLogMapper.java
new file mode 100644
index 0000000..759180b
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/firewall/mapper/FirewallAccessLogMapper.java
@@ -0,0 +1,15 @@
+package cc.mrbird.febs.firewall.mapper;
+
+import cc.mrbird.febs.firewall.entity.FirewallAccessLog;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 防火墙访问日志 Mapper
+ *
+ * @author auto-generated
+ * @date 2026-07-31
+ */
+@Mapper
+public interface FirewallAccessLogMapper extends BaseMapper<FirewallAccessLog> {
+}
diff --git a/src/main/java/cc/mrbird/febs/firewall/service/IFirewallRuleService.java b/src/main/java/cc/mrbird/febs/firewall/service/IFirewallRuleService.java
index dd301d2..b278f81 100644
--- a/src/main/java/cc/mrbird/febs/firewall/service/IFirewallRuleService.java
+++ b/src/main/java/cc/mrbird/febs/firewall/service/IFirewallRuleService.java
@@ -3,6 +3,7 @@
import cc.mrbird.febs.common.entity.FebsResponse;
import cc.mrbird.febs.common.entity.QueryRequest;
import cc.mrbird.febs.firewall.dto.CountryVo;
+import cc.mrbird.febs.firewall.entity.FirewallAccessLog;
import cc.mrbird.febs.firewall.entity.FirewallCountryRule;
import cc.mrbird.febs.firewall.entity.FirewallIpWhite;
import cc.mrbird.febs.firewall.entity.FirewallSite;
@@ -83,4 +84,16 @@
* 删除IP白名单(删除后自动生成Nginx配置)
*/
FebsResponse deleteIpWhite(Long id);
+
+ // ==================== 访问日志管理 ====================
+
+ /**
+ * 记录一次访问
+ */
+ void recordAccess(FirewallAccessLog log);
+
+ /**
+ * 访问日志分页列表
+ */
+ IPage<FirewallAccessLog> accessLogList(FirewallAccessLog log, QueryRequest request);
}
diff --git a/src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java b/src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java
index bfddd0e..af37bcf 100644
--- a/src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java
+++ b/src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java
@@ -4,9 +4,11 @@
import cc.mrbird.febs.common.entity.QueryRequest;
import cc.mrbird.febs.common.utils.RedisUtils;
import cc.mrbird.febs.firewall.dto.CountryVo;
+import cc.mrbird.febs.firewall.entity.FirewallAccessLog;
import cc.mrbird.febs.firewall.entity.FirewallCountryRule;
import cc.mrbird.febs.firewall.entity.FirewallIpWhite;
import cc.mrbird.febs.firewall.entity.FirewallSite;
+import cc.mrbird.febs.firewall.mapper.FirewallAccessLogMapper;
import cc.mrbird.febs.firewall.mapper.FirewallCountryRuleMapper;
import cc.mrbird.febs.firewall.mapper.FirewallIpWhiteMapper;
import cc.mrbird.febs.firewall.mapper.FirewallSiteMapper;
@@ -38,6 +40,7 @@
private final FirewallCountryRuleMapper countryRuleMapper;
private final FirewallIpWhiteMapper ipWhiteMapper;
+ private final FirewallAccessLogMapper accessLogMapper;
private final FirewallSiteMapper siteMapper;
private final NginxConfigGenerator generator;
private final IpWhitelistNginxConfigGenerator ipWhitelistGenerator;
@@ -298,4 +301,23 @@
redisUtils.lSet(NGINX_RELOAD_QUEUE_KEY, String.valueOf(siteId));
return new FebsResponse().success().message("删除成功,Nginx 配置已更新");
}
+
+ // ==================== 访问日志 ====================
+
+ @Override
+ public void recordAccess(FirewallAccessLog accessLog) {
+ accessLogMapper.insert(accessLog);
+ }
+
+ @Override
+ public IPage<FirewallAccessLog> accessLogList(FirewallAccessLog log, QueryRequest request) {
+ Page<FirewallAccessLog> page = new Page<>(request.getPageNum(), request.getPageSize());
+ LambdaQueryWrapper<FirewallAccessLog> wrapper = new LambdaQueryWrapper<FirewallAccessLog>()
+ .eq(log.getSiteId() != null, FirewallAccessLog::getSiteId, log.getSiteId())
+ .eq(log.getIsBlocked() != null, FirewallAccessLog::getIsBlocked, log.getIsBlocked())
+ .eq(StrUtil.isNotBlank(log.getCountryCode()), FirewallAccessLog::getCountryCode, log.getCountryCode())
+ .like(StrUtil.isNotBlank(log.getIp()), FirewallAccessLog::getIp, log.getIp())
+ .orderByDesc(FirewallAccessLog::getCreatedTime);
+ return accessLogMapper.selectPage(page, wrapper);
+ }
}
diff --git a/src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java b/src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java
index 829e4f1..c7ac965 100644
--- a/src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java
+++ b/src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java
@@ -187,6 +187,12 @@
return FebsUtil.view("modules/system/firewallIpWhiteList");
}
+ @GetMapping("firewallAccessLogList")
+ @RequiresPermissions("firewallAccessLog:view")
+ public String firewallAccessLogList() {
+ return FebsUtil.view("modules/system/firewallAccessLogList");
+ }
+
@GetMapping("tokenviewConfig")
@RequiresPermissions("tokenviewConfig:update")
public String tokenviewConfig(Model model) {
diff --git a/src/main/resources/templates/febs/views/modules/system/firewallAccessLogList.html b/src/main/resources/templates/febs/views/modules/system/firewallAccessLogList.html
new file mode 100644
index 0000000..778ed8c
--- /dev/null
+++ b/src/main/resources/templates/febs/views/modules/system/firewallAccessLogList.html
@@ -0,0 +1,178 @@
+<div class="layui-fluid layui-anim febs-anim" id="firewall-access-log-list" lay-title="防火墙访问日志">
+ <div class="layui-row febs-container">
+ <div class="layui-col-md12">
+ <div class="layui-card">
+ <div class="layui-card-body febs-table-full">
+ <form class="layui-form layui-table-form" lay-filter="firewall-accesslog-table-form">
+ <div class="layui-row">
+ <div class="layui-col-md10">
+ <div class="layui-form-item">
+ <button class="layui-btn layui-btn-normal" type="button" id="refresh-btn">
+ <i class="layui-icon"></i> 刷新
+ </button>
+ </div>
+ </div>
+ <div class="layui-col-md2 layui-col-sm12 layui-col-xs12 table-action-area">
+ <div class="layui-btn layui-btn-sm layui-btn-primary febs-button-green-plain table-action" id="reset">
+ <i class="layui-icon"></i>
+ </div>
+ </div>
+ </div>
+ <div class="layui-row">
+ <div class="layui-col-md2">
+ <div class="layui-form-item">
+ <label class="layui-form-label">IP地址</label>
+ <div class="layui-input-block">
+ <input type="text" name="ip" class="layui-input" placeholder="搜索IP">
+ </div>
+ </div>
+ </div>
+ <div class="layui-col-md2">
+ <div class="layui-form-item">
+ <label class="layui-form-label">国家</label>
+ <div class="layui-input-block">
+ <input type="text" name="countryCode" class="layui-input" placeholder="如 CN/US">
+ </div>
+ </div>
+ </div>
+ <div class="layui-col-md2">
+ <div class="layui-form-item">
+ <label class="layui-form-label">状态</label>
+ <div class="layui-input-block">
+ <select name="isBlocked" lay-filter="filter-blocked">
+ <option value="">全部</option>
+ <option value="0">放行</option>
+ <option value="1">拦截</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="layui-col-md2">
+ <div class="layui-form-item">
+ <label class="layui-form-label">选择站点</label>
+ <div class="layui-input-block">
+ <select name="siteId" lay-filter="filter-site" lay-search="">
+ <option value="">全部站点</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="layui-col-md2">
+ <button class="layui-btn layui-btn-sm" type="button" id="search-btn">
+ <i class="layui-icon"></i> 搜索
+ </button>
+ </div>
+ </div>
+ </form>
+ <table lay-filter="firewallAccessLogTable" lay-data="{id: 'firewallAccessLogTable'}"></table>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<script data-th-inline="none" type="text/javascript">
+ layui.use(['jquery', 'form', 'table', 'febs'], function () {
+ var $ = layui.jquery,
+ febs = layui.febs,
+ form = layui.form,
+ table = layui.table,
+ $view = $('#firewall-access-log-list'),
+ $reset = $view.find('#reset'),
+ $refreshBtn = $view.find('#refresh-btn'),
+ $searchBtn = $view.find('#search-btn'),
+ tableIns;
+
+ form.render();
+ loadSiteFilter();
+ initTable();
+
+ function loadSiteFilter() {
+ febs.get(ctx + 'admin/firewall/sites', null, function (res) {
+ var sites = res.data || [];
+ var $select = $view.find('select[name="siteId"]');
+ sites.forEach(function (s) {
+ $select.append('<option value="' + s.id + '">' + s.siteName + ' (' + s.domain + ')</option>');
+ });
+ form.render('select');
+ });
+ }
+
+ // 定时自动刷新(每30秒)
+ var autoRefreshTimer = setInterval(function () {
+ tableIns.reload();
+ }, 30000);
+
+ $refreshBtn.on('click', function () {
+ tableIns.reload();
+ });
+
+ $reset.on('click', function () {
+ $view.find('input[name="ip"]').val('');
+ $view.find('input[name="countryCode"]').val('');
+ $view.find('select[name="isBlocked"]').val('');
+ $view.find('select[name="siteId"]').val('');
+ form.render('select');
+ tableIns.reload();
+ });
+
+ $searchBtn.on('click', function () {
+ tableIns.reload();
+ });
+
+ form.on('select(filter-blocked)', function () {
+ tableIns.reload();
+ });
+ form.on('select(filter-site)', function () {
+ tableIns.reload();
+ });
+
+ function initTable() {
+ tableIns = febs.table.init({
+ elem: $view.find('table'),
+ id: 'firewallAccessLogTable',
+ url: ctx + 'admin/firewall/accessLogList',
+ where: function () {
+ return {
+ ip: $view.find('input[name="ip"]').val() || undefined,
+ countryCode: $view.find('input[name="countryCode"]').val() || undefined,
+ isBlocked: $view.find('select[name="isBlocked"]').val() || undefined,
+ siteId: $view.find('select[name="siteId"]').val() || undefined
+ };
+ },
+ cols: [[
+ {field: 'id', title: 'ID', width: 70, align: 'center'},
+ {field: 'ip', title: 'IP地址', width: 160, align: 'center'},
+ {field: 'countryCode', title: '国家', width: 80, align: 'center',
+ templet: function (d) {
+ return d.countryCode
+ ? '<span class="layui-badge layui-bg-blue">' + d.countryCode + '</span>'
+ : '<span style="color:#999;">未知</span>';
+ }
+ },
+ {field: 'countryName', title: '国家名称', width: 120, align: 'center'},
+ {field: 'requestUri', title: '来源页面', minWidth: 180,
+ templet: function (d) {
+ return d.requestUri
+ ? '<span style="color:#5FB878;">' + d.requestUri + '</span>'
+ : '<span style="color:#999;">直接访问</span>';
+ }
+ },
+ {field: 'isBlocked', title: '状态', width: 90, align: 'center',
+ templet: function (d) {
+ return d.isBlocked === 1
+ ? '<span style="color:#FF5722;">● 拦截</span>'
+ : '<span style="color:#5FB878;">● 放行</span>';
+ }
+ },
+ {field: 'userAgent', title: '浏览器', minWidth: 200,
+ templet: function (d) {
+ return d.userAgent ? d.userAgent.substring(0, 60) + '...' : '-';
+ }
+ },
+ {field: 'createdTime', title: '访问时间', width: 170, align: 'center'}
+ ]],
+ page: true
+ });
+ }
+ });
+</script>
--
Gitblit v1.9.1