
简介这是一份WEB访问日志分析与入侵检测可视化系统的完整源码面向计算机相关专业学生适用于课程设计或期末大作业。项目获得98分并获导师认可基于CentOS7构建通过解析服务器访问日志识别入侵行为并直观展示。资源共26个文件以shell运维脚本、cfg和properties配置、myid集群标识、html可视化页面为主并附有README与MD说明文档方便快速部署整包仅27KB但目录结构清晰覆盖日志采集、预处理、异常检测、可视化展示等环节。已有41人学习可参考其高评分实现来完善自身设计。通过研读源码能理解网络协议分析、数据挖掘及可视化技术在实际项目中的落地适合在导师引导下进行二次开发与答辩准备。1. 从「能跑」到「高分」这门课设到底在考察什么如果你的课设题目是「WEB访问日志分析及入侵检测可视化系统」大概率不是让你去复刻一个企业级 SIEM而是考察三件事的闭环日志能不能清洗干净、规则能不能命中攻击、结果能不能让老师一眼看懂。这三点里前两点靠代码功底最后一点反而最拉分。95 分以上的课程设计通常在答辩现场有个共同特征演示页面打开攻击记录、来源 IP、命中规则、时间线一目了然评委不用问「你这个表是什么意思」就已经在心里加分了。这个项目的本质是一条流水线Nginx/Apache 访问日志 → 解析清洗 → 规则匹配或行为分析 → 入库 → Web 可视化。适合有 Python/Java 基础、想走安全方向或后端方向的学生。本文用你最容易拿高分的 Python Flask ECharts 方案讲清楚每一条命令、每一个参数、每一个坑照着做本地十分钟跑通再花半天调规则答辩就有东西可讲了。2. 访问日志里到底藏了哪些攻击线索先学会看日志再写代码2.1 一条正常日志与一条攻击日志的肉眼差别大多数课程设计给的日志文件是 Nginx 默认格式长这样192.168.1.23 - - [12/Sep/2024:08:21:34 0800] GET /index.php HTTP/1.1 200 5321 http://example.com/ Mozilla/5.0 (Windows NT 10.0; Win64; x64)对应字段依次是客户端 IP、远程用户通常为空、时间、请求行、状态码、返回字节数、Referer、User-Agent。攻击日志和正常日志的区别通常藏在三个地方IP 是否在短时间内高频出现、请求路径是否包含特殊字符、User-Agent 是否异常。SQL 注入的日志里路径往往带着、union、select这些关键词扫描器的日志里状态码大面积是 404暴力破解的日志里同一个 IP 会在几秒内反复POST /login.php。2.2 解析正则别用 split 硬切直接上命名捕获组解析日志最忌讳用split( )因为 Referer 和 User-Agent 内部有空格一刀切下去字段全错位。我先写一个兼容 Nginx 默认 combined 格式的解析函数import re from datetime import datetime LOG_PATTERN re.compile( r(?Pip\S) \S \S r\[(?Ptime[^\]])\] r(?Prequest[^]*) r(?Pstatus\d{3}) (?Psize\S) r(?Preferer[^]*) r(?Pua[^]*) ) def parse_line(line): m LOG_PATTERN.match(line.strip()) if not m: return None d m.groupdict() # 解析请求行拆出 method / path / protocol parts d[request].split() d[method] parts[0] if len(parts) 0 else - d[path] parts[1] if len(parts) 1 else - d[protocol] parts[2] if len(parts) 2 else - try: d[time] datetime.strptime(d[time], %d/%b/%Y:%H:%M:%S %z) except ValueError: d[time] None return d这段代码用命名捕获组把每个字段固化下来request内部再拆一次。注意size用\S而不是\d因为某些反代日志里这个字段可能是-。time转成datetime对象是为了后面做时间窗口统计如果你只是做演示不转也行但转了你就能画「攻击次数随时间变化」的折线图这个图在课设答辩里非常加分。2.3 攻击特征建模规则匹配比机器学习更稳课程设计里我不推荐上机器学习原因很现实你手头没有带标签的攻击数据集训练出来的模型准确率没保证答辩时老师问「精确率和召回率怎么算的」容易被问穿。规则匹配的召回率虽然不完美但每一条规则都能说清楚逻辑这就是得分点。常见的规则分四类路径特征请求路径包含select、union、sleep(、../、/etc/passwd扫描特征同一个 IP 短时间内触发大量 404暴力破解特征同一个 IP 对login.php、admin.php高频 POST可疑 User-Agentsqlmap、nmap、Nikto、空 UA 或非浏览器 UA我一般把规则写成 JSON 配置而不是硬编码在 Python 里这样答辩时可以现场改规则演示效果。{ sql_injection: { type: path_contains, keywords: [, union, select, sleep(, benchmark], level: high }, xss_attempt: { type: path_contains, keywords: [script, onerror, javascript:], level: medium }, path_traversal: { type: path_contains, keywords: [../, ..\\, /etc/passwd, c:/windows], level: high }, scanner: { type: status_404_rate, threshold: 30, window_seconds: 60, level: medium } }规则引擎的代码逻辑是先做单条日志的路径匹配再做基于 IP 的聚合统计。聚合的意思是你得维护一个滑动窗口记录每个 IP 在最近 60 秒内产生的 404 数量。这里的threshold和window_seconds是两个核心参数阈值设太低会把正常爬虫误报成扫描器设太高又漏报。我的经验值window_seconds60、threshold30起步如果你的日志是教学用的模拟日志攻击者 IP 通常就那么几个调低到10效果更明显。3. 落地一条流水线Flask 读取日志、SQLite 存储、页面实时刷新3.1 项目目录结构与数据库设计先定目录结构别把所有代码塞在一个app.py里那是课设低于 90 分的典型特征。推荐按功能拆web_log_ids/ ├── app.py # Flask 主入口 ├── parser.py # 日志解析器 ├── detector.py # 规则检测引擎 ├── database.py # SQLite 存取 ├── static/ │ └── echarts.min.js # ECharts 库文件 ├── templates/ │ └── index.html # 可视化页面 ├── logs/ │ └── access.log # 示例日志 └── rules.json # 攻击规则数据库设计上我建议两张表就够了一张存原始日志一张存告警事件。原始日志表字段包括id, timestamp, ip, method, path, status, ua告警表字段包括id, timestamp, ip, rule_name, level, path, raw_line。两张表之间不需要外键课设规模用不上反而拖慢查询。核心查询是两个按时间倒序取最近 N 条告警、按 IP 分组统计攻击次数。CREATE TABLE logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, ip TEXT, method TEXT, path TEXT, status INTEGER, ua TEXT ); CREATE TABLE alerts ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, ip TEXT, rule_name TEXT, level TEXT, path TEXT, raw_line TEXT ); CREATE INDEX idx_alerts_time ON alerts(timestamp); CREATE INDEX idx_alerts_ip ON alerts(ip);索引建在timestamp和ip上因为可视化页面最常做的两个查询就是「最近告警」和「TOP 攻击 IP」。不建索引在几千条日志时无所谓但如果你的日志文件有几十万行没索引的查询会让页面卡好几秒答辩现场卡顿非常尴尬。3.2 数据摄入日志解析 规则检测 入库三步走主流程写成一个函数循环逐行读取日志解析后先入库再检测顺序不能反——先入库保证原始数据不丢再检测生成告警。演示时你的页面要能展示原始日志表和告警表两张视图这能体现「分析」和「检测」两个环节的完整性。from database import insert_log, insert_alert from parser import parse_line from detector import check_rules def process_log_file(filepath): alerts_count 0 with open(filepath, r, encodingutf-8, errorsignore) as f: for line in f: parsed parse_line(line) if parsed is None: continue log_id insert_log(parsed) alerts check_rules(parsed) for alert in alerts: insert_alert(alert, raw_lineline.strip()) alerts_count 1 return alerts_count这里有个细节errorsignore必须加因为访问日志里偶尔会出现乱码字节不加这个参数程序会中途崩掉。check_rules返回列表而不是单个告警因为一行日志可能同时命中 SQL 注入和路径穿越两条规则多条告警都该记录下来。入库后用rowcount或返回的log_id确认插入成功别裸写execute不commit。3.3 API 设计给前端喂三个 JSON 接口可视化页面不直接查数据库而是通过 Flask 接口拿数据。我设计三个接口/api/alerts/recent拿最近 100 条告警/api/stats/top_ips拿 TOP 10 攻击源 IP/api/stats/trend按小时统计攻击次数。这样页面拆成三个图表区每一块都有独立的数据支撑。from flask import Flask, jsonify, render_template from database import query_recent_alerts, query_top_ips, query_trend app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/api/alerts/recent) def recent_alerts(): rows query_recent_alerts(limit100) return jsonify(rows) app.route(/api/stats/top_ips) def top_ips(): rows query_top_ips(limit10) return jsonify(rows) app.route(/api/stats/trend) def trend(): rows query_trend() return jsonify(rows) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)host0.0.0.0允许局域网访问答辩时可以让老师用手机连你的电脑看页面这个小动作很加分。debugTrue平时开着方便改代码自动重载但答辩演示时建议关掉因为 debug 模式下如果页面报错会弹出交互式调试器反而显得不专业。3.4 ECharts 页面三张图讲清攻击全貌页面用 ECharts 画三张图布局上左上是告警实时滚动表左下是 TOP 10 攻击 IP 横向条形图右侧是攻击次数随时间变化的折线图。核心代码是页面加载时并发请求三个接口拿到数据后分别初始化图表。async function loadData() { const [alertsRes, ipsRes, trendRes] await Promise.all([ fetch(/api/alerts/recent).then(r r.json()), fetch(/api/stats/top_ips).then(r r.json()), fetch(/api/stats/trend).then(r r.json()) ]); renderAlertsTable(alertsRes); renderTopIpsBar(ipsRes); renderTrendLine(trendRes); } function renderTrendLine(data) { const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ title: { text: 攻击次数时间分布 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.map(d d.hour) }, yAxis: { type: value }, series: [{ name: 攻击次数, type: line, areaStyle: {}, data: data.map(d d.cnt) }] }); } window.onload loadData;Promise.all是必须的三个接口并行请求页面加载时间能缩短到原来的三分之一。折线图加areaStyle是为了视觉上更饱满——课设评分时带渐变面积图的页面就是比干巴巴的折线图看着高级虽然代码只多两行。如果你数据量小趋势图可能是平的建议按小时聚合而不是按分钟或者手动往日志里塞一些不同时间的攻击记录让曲线有起伏。4. 检测引擎的进阶参数阈值、时间窗口与误报抑制4.1 滑动窗口聚合从单条检测到行为检测单条日志规则只能抓「特征明显的攻击」比如路径里带union select。但真正的扫描器行为是分散的单个请求看起来很正常只有把时间窗口内的请求放在一起看才能发现异常。这就是基于 IP 的滑动窗口聚合代码里用一个字典维护每个 IP 的请求时间戳列表from collections import defaultdict, deque import time ip_hits defaultdict(lambda: deque(maxlen200)) def check_rate_limit(ip, current_time, window_seconds60, threshold30): q ip_hits[ip] q.append(current_time) # 移除窗口之外的时间戳 while q and current_time - q[0] window_seconds: q.popleft() return len(q) thresholddeque(maxlen200)是防止某个恶意 IP 刷爆内存最多保留 200 个时间戳超过的自动挤掉。popleft移除窗口外的时间戳后len(q)就是当前 IP 在窗口内的请求数。这个函数的返回值只判断「是否超过阈值」但检测引擎需要知道具体命中什么规则所以告警信息里要补一条「高频请求」规则def check_rules(parsed, rules): alerts [] # 单条路径规则 for rule_name, rule in rules.items(): if rule[type] path_contains: if any(k.lower() in parsed[path].lower() for k in rule[keywords]): alerts.append({ rule: rule_name, level: rule[level], path: parsed[path] }) # 频率规则 if check_rate_limit(parsed[ip], time.time()): alerts.append({ rule: high_frequency_request, level: medium, path: parsed[path] }) return alerts注意这里我用time.time()而不是日志里的时间戳。为什么因为日志时间可能是历史时间如果用日志时间去重放或分析旧文件IP 请求频率永远是 0。用当前时间模拟实时检测逻辑上更符合「入侵检测系统」的语义——检测发生在请求进入的瞬间。如果你要处理的是历史日志就把时间窗口计算改成基于日志时间这是两套逻辑别混。4.2 三个必须现场调过的参数第一个是threshold高频请求阈值。用 Nginx 默认日志做演示时正常爬虫每分钟请求量在 10~30 之间攻击扫描器通常在 50 以上。我建议初始设 30然后故意用脚本去刷日志看能不能触发触发不了就往低调。第二个是window_seconds时间窗口长度。窗口太短慢速扫描器会漏掉窗口太长正常用户会被误报。60 秒是个稳妥的起步值但如果你的日志是模拟几分钟内灌入的建议调到 10~20 秒这样演示时能看到告警快速出现不用等一分钟。第三个是min_alert_interval同一个 IP 同一规则的告警冷却时间。没有冷却时间扫描器触发一次高频规则后后续 200 个请求会生成 200 条告警告警表瞬间被刷屏。加冷却时间后同一个 IP 的同类告警 5 分钟只报一次页面看起来干净得多。4.3 误报抑制把「看起来像攻击」变成「确定是攻击」规则匹配最大的痛点是误报。日志里出现select不一定是 SQL 注入可能是用户搜索关键词../不一定是路径穿越可能是前端路由。我的做法是给规则加一个context_required字段{ sql_injection: { type: path_contains, keywords: [ union, or 11, sleep(, extractvalue], context_required: true, level: high } }context_required: true的含义是命中关键词后还要验证上下文。比如路径里出现sleep(通常是延时注入但普通的 URL 里也可能带sleep单词所以我会检查关键词前是否跟了数字或括号——sleep(5)是攻击sleepy是正常词。这个逻辑不复杂但能显著降低误报率答辩时老师问「你怎么减少误报」你把这几行代码指出来就是活生生的得分点。4.4 日志回放没有攻击数据怎么演示检测效果课设最容易卡住的地方是没有真实攻击日志。我的建议是用脚本生成模拟日志把攻击特征混在正常请求里顺便构造出时间上的波峰波谷。推荐做法是准备两个基础模板数组正常请求模板和攻击请求模板然后按比例随机拼接import random import time normal_paths [/, /index.php, /about, /product/123, /news?id5] attack_paths [ /index.php?id1 union select 1,2,3--, /admin.php?page../../etc/passwd, /search?qscriptalert(1)/script, /login.php, /login.php, /login.php ] def gen_log_line(ip, path, ts): method POST if login in path else GET status 404 if etc/passwd in path else 200 ua Mozilla/5.0 (compatible; sqlmap/1.5) if union in path else Mozilla/5.0 return f{ip} - - [{ts}] {method} {path} HTTP/1.1 {status} 512 - {ua} # 生成 2000 条正常 200 条攻击 with open(logs/access.log, w, encodingutf-8) as f: for i in range(2200): if i % 10 0: ip f192.168.1.{random.randint(2, 20)} path random.choice(attack_paths) else: ip f10.0.{random.randint(0, 5)}.{random.randint(1, 254)} path random.choice(normal_paths) ts time.strftime(%d/%b/%Y:%H:%M:%S 0800, time.localtime(1600000000 i * 3)) f.write(gen_log_line(ip, path, ts) \n)这个脚本按每 3 秒一条生成日志每 10 条插入一条攻击记录攻击 IP 集中在192.168.1.2~20。注意这里用i * 3控制时间间隔让日志时间看起来均匀分布。生成后先跑一遍检测脚本看告警数是不是符合预期再决定调规则还是调数据。日志回放是整个演示的地基地基稳了后面才不慌。5. 部署与答辩避坑本地跑通容易演示不翻车很难5.1 常见问题一编码错误导致解析中断现象程序跑到一半报UnicodeDecodeError或者页面里告警记录出现乱码。原因访问日志里的 User-Agent 是各种浏览器、爬虫、扫描器混着来有些 UA 带了非 UTF-8 编码的字节。解决读文件时加errorsignore同时数据库连接字符串里指定charsetutf8mb4。我用 SQLite 没有编码问题但如果有人用 MySQL建表语句里必须加DEFAULT CHARSETutf8mb4否则 emoji 或特殊符号插入就报错。5.2 常见问题二页面图表不显示接口返回空数组现象Flask 页面能打开但 ECharts 区域空白打开浏览器开发者工具发现接口返回[]。原因通常是两种一是数据库里确实没数据因为日志文件路径不对process_log_file没有执行二是前端 JS 代码里data.map(d d.hour)里的字段名和接口返回的键名不一致比如 Python 返回{hour: 12, cnt: 5}JS 里写成了d.time。解决先访问接口 URL 确认返回结构再检查 JS 里字段名大小写。这个坑我在第一次做的时候踩过后端改了个字段名忘了同步前端排查了半小时。5.3 常见问题三高频请求告警刷屏页面卡死现象点击演示按钮后告警表瞬间多出几千条记录浏览器滚动都卡顿。原因没有做告警冷却检测引擎对每一条日志都生成告警。解决在检测引擎里加一个字典key 是(ip, rule_name)value 是上次告警时间只有距离上次告警超过min_alert_interval才写入新告警。另一个更彻底的方案是前端只展示最近 100 条数据库里保留全部这样即使刷屏也不影响页面性能。5.4 常见问题四答辩时演示流程忘顺序现象打开页面先点「开始检测」然后才想起来日志还没导入页面一直空着。原因演示没有固定脚本。解决写一个init_db.py把「清空数据库 → 读日志 → 检测 → 入库」串成一个命令答辩前跑一遍。现场演示只做两件事启动 Flask刷新页面。如果老师问你检测过程再把init_db.py跑一遍给他看命令行输出这样每一步都是可控的。血泪经验是答辩现场别现场改代码改完忘了重启 Flask 导致页面不更新这种翻车比功能做不出来还尴尬。5.5 没有数据可视化时拿什么撑场面万一 ECharts 库加载失败或图表渲染异常页面别白屏。我建议在告警表格旁放一个「原始日志」标签页把日志表的数据用 Bootstrap Table 全量展示出来。这样即使图表挂了你还能按 IP 搜索日志、按状态码筛选讲「分析」功能一样不缺内容。原始日志表格的搜索框很加分加上按时间倒序排序演示时输入一个攻击 IP所有相关记录高亮展示评委马上能理解你的系统在做什么。6. 让系统像「会思考」一样加一个攻击者画像页课设做到前面五章内容已经稳在 90 分以上了。如果你想冲 95 分我有一个私藏技巧加一个「攻击者画像」页面。本质上就是对告警数据库做 GROUP BY 聚合把同一个 IP 的行为拼成一段可读的描述让老师感觉到你的系统不是死板的规则匹配而是有分析逻辑的。SELECT ip, COUNT(*) AS attack_cnt, GROUP_CONCAT(DISTINCT rule_name) AS rules, COUNT(DISTINCT path) AS target_cnt, MAX(timestamp) AS last_attack FROM alerts GROUP BY ip ORDER BY attack_cnt DESC LIMIT 20;拿到这个结果后用 Python 生成画像描述def build_profile(row): profile f攻击者 {row[ip]} 共发起 {row[attack_cnt]} 次攻击 profile f涉及 {row[target_cnt]} 个不同路径 profile f主要攻击类型为 {row[rules]} profile f最近一次攻击发生在 {row[last_attack]}。 return profile页面左侧放威胁等级仪表盘用 ECharts gauge 图展示攻击者的严重程度按攻击次数映射 0~100 分右侧放画像文本。注意GROUP_CONCAT(DISTINCT rule_name)在 SQLite 里是支持的但如果规则名本身包含逗号输出会粘连建议改成GROUP_CONCAT(DISTINCT rule_name, |)分隔符。我在做演示时发现攻击次数超过 50 的 IP 基本都是扫描器可以把等级阈值设为小于 10 次为低危10~50 为中危大于 50 为高危这个划分标准在答辩时要能说得清依据。关于验证方法我最后会跑一遍端到端测试把日志文件用tail -f实时追加攻击记录观察页面 5 秒内是否自动刷新出告警。这里用setInterval每 5 秒轮询一次/api/alerts/recent虽然比 WebSocket 笨但胜在实现简单、稳定可靠没有任何浏览器兼容问题。轮询代码里加一个判断如果告警数量比上次多就弹出一个浏览器通知条视觉效果上很像真实入侵检测系统在报警。我一直保留的习惯是答辩前一晚把数据库删了重新灌一遍日志确认整个流程从零开始能跑通而不是依赖之前调试留下的脏数据。这套流程帮你避开「昨天能跑今天跑不了」的玄学问题。另外给日志文件和代码文件都加上注释头标注作者、日期、运行方式这虽然不加技术分但能让评委觉得你工程素养好。希望帮到你照着这个方案做你的课程设计离 95 分就不远了。本文还有配套的精品资源点击获取