简介:这是一份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'(?P<ip>\S+) \S+ \S+ ' r'\[(?P<time>[^\]]+)\] ' r'"(?P<request>[^"]*)" ' r'(?P<status>\d{3}) (?P<size>\S+) ' r'"(?P<referer>[^"]*)" ' r'"(?P<ua>[^"]*)"' ) 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-Agent:
sqlmap、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_seconds=60、threshold=30起步,如果你的日志是教学用的模拟日志,攻击者 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', encoding='utf-8', errors='ignore') 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_line=line.strip()) alerts_count += 1 return alerts_count这里有个细节:errors='ignore'必须加,因为访问日志里偶尔会出现乱码字节,不加这个参数程序会中途崩掉。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(limit=100) return jsonify(rows) @app.route('/api/stats/top_ips') def top_ips(): rows = query_top_ips(limit=10) return jsonify(rows) @app.route('/api/stats/trend') def trend(): rows = query_trend() return jsonify(rows) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)host='0.0.0.0'允许局域网访问,答辩时可以让老师用手机连你的电脑看页面,这个小动作很加分。debug=True平时开着方便改代码自动重载,但答辩演示时建议关掉,因为 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(maxlen=200)) def check_rate_limit(ip, current_time, window_seconds=60, threshold=30): q = ip_hits[ip] q.append(current_time) # 移除窗口之外的时间戳 while q and current_time - q[0] > window_seconds: q.popleft() return len(q) >= thresholddeque(maxlen=200)是防止某个恶意 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 1=1", "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?id=5"] attack_paths = [ "/index.php?id=1' union select 1,2,3--", "/admin.php?page=../../etc/passwd", "/search?q=<script>alert(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", encoding="utf-8") as f: for i in range(2200): if i % 10 == 0: ip = f"192.168.1.{random.randint(2, 20)}" path = random.choice(attack_paths) else: ip = f"10.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 编码的字节。解决:读文件时加errors='ignore',同时数据库连接字符串里指定charset='utf8mb4'。我用 SQLite 没有编码问题,但如果有人用 MySQL,建表语句里必须加DEFAULT CHARSET=utf8mb4,否则 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 分就不远了。
本文还有配套的精品资源,点击获取