资讯详情

基于监督学习的Web入侵检测系统:从特征工程到Flask部署的完整实战

📅 2026/10/7 11:32:34 | 华诺云谱 👁 阅读
基于监督学习的Web入侵检测系统:从特征工程到Flask部署的完整实战
简介这份资源是面向计算机相关专业学生与从业者的毕业设计级项目源码主题为基于监督学习的Web入侵检测系统采用Python实现评审分达97分适合用作毕业设计、期末课程设计或课程大作业的参考方案。压缩包共60个文件约2.25MB以18个ipynb实验笔记、14个txt样本与词表、14个html页面样本、8个py脚本为主另含pkl与pickle模型文件、md说明文档及少量pyc缓存覆盖数据采集、字符处理、去重、参数提取与模型保存等环节。项目围绕Web请求载荷与正常流量样本展开包含爬虫脚本、关键词与安全词表、正常参数提取及多份实验笔记便于理解监督学习在入侵检测中的特征工程与训练流程。目前已有188人学习下载可帮助读者快速获取完整目录结构、样本数据与可运行代码对照复现实验并梳理排错思路。1. 从一份毕设压缩包说起监督学习做 Web 入侵检测到底在做什么很多人第一次拿到「基于监督学习的 Web 入侵检测系统」这类毕设题目时脑子里冒出来的第一个问题是这东西跟装个 WAF 有什么区别答案在于定位不同。WAF 是规则驱动的工程产品靠正则和特征库拦截已知攻击而监督学习做入侵检测核心是把 HTTP 请求变成特征向量用带标签的数据训练一个分类器让它自己学会区分正常请求和恶意请求。它解决的是规则写不完、变种绕过的痛点适合安全方向的学生做毕设也适合后端工程师给自家接口加一层轻量异常识别。这个方向的技术栈并不复杂Python 做数据处理和建模Flask 或 Django 做 Web 展示层scikit-learn 或 XGBoost 做分类器数据集用公开的 HTTP 攻击样本。真正拉开分数差距的不是模型多深而是特征工程是否合理、评估是否严谨、系统是否能跑通一条完整的「请求进来 → 特征提取 → 模型判定 → 结果展示」链路。下面按落地顺序拆开讲从数据到模型到系统每一步都给可复现的做法。2. 数据与特征把一条 HTTP 请求变成模型能吃的向量2.1 公开数据集怎么选标签怎么对齐做监督学习第一道坎不是模型是数据。Web 入侵检测常用的公开数据来源有几类CSIC 2010 HTTP 数据集西班牙研究机构发布包含正常请求和多种攻击请求、ECML/PKDD 2007 数据集、以及从真实日志中脱敏后自建的样本。毕设场景下我一般建议以 CSIC 2010 为主因为它请求量适中约 6 万条标签清晰正常与攻击分开存放省去大量标注工作。CSIC 2010 的目录结构通常是正常请求一个文件、攻击请求一个文件每行一条原始 HTTP 请求。加载时要注意请求是跨多行的请求行、头部、空行、body不能按行直接读。常见做法是按空行切分请求块再逐块解析。标签对齐很简单正常文件标 0攻击文件标 1。但要注意类别不平衡问题——攻击样本往往少于正常样本训练时要设置class_weightbalanced或做重采样。import re def parse_http_requests(filepath, label): 按空行切分 HTTP 请求块返回 (raw_text, label) 列表 with open(filepath, r, encodingutf-8, errorsignore) as f: content f.read() # 连续两个换行视为请求分隔符 blocks re.split(r\n\s*\n, content) samples [] for block in blocks: block block.strip() if len(block) 10: # 过滤空块和碎片 continue samples.append((block, label)) return samples normal parse_http_requests(normalTrafficTraining.txt, 0) attack parse_http_requests(anomalousTrafficTest.txt, 1) print(f正常样本 {len(normal)} 条攻击样本 {len(attack)} 条)这段代码的关键点是re.split(r\n\s*\n, content)用空行作为请求边界。参数errorsignore是为了防止编码问题导致读取中断。过滤长度小于 10 的块是经验值能去掉解析产生的碎片。实际跑的时候如果发现样本数远少于预期先检查文件换行符是\n还是\r\nWindows 下拿到的数据经常是后者正则要相应调整。2.2 特征工程URL、参数、头部各提取什么原始文本不能直接喂给分类器必须转成数值向量。Web 入侵检测的特征通常从三个维度提取URL 层面、请求头层面、body 参数层面。URL 层面看长度、特殊字符数量、、、、;、--、路径深度、是否含编码字符%xx请求头看 User-Agent 长度、Cookie 长度、Content-Type 类型body 看参数个数、参数值长度、是否含 SQL 关键字。import numpy as np from urllib.parse import urlparse, parse_qs SQL_KEYWORDS [select, union, insert, update, delete, drop, --, or 11] XSS_KEYWORDS [script, javascript:, onerror, onload, alert(] def extract_features(raw_request): 从单条原始请求提取数值特征 lines raw_request.split(\n) request_line lines[0] if lines else parts request_line.split( ) url parts[1] if len(parts) 1 else body raw_request.split(\n\n, 1)[1] if \n\n in raw_request else full_text (url body).lower() parsed urlparse(url) query parse_qs(parsed.query) feats { url_len: len(url), path_depth: parsed.path.count(/), num_params: len(query), body_len: len(body), special_char_count: sum(full_text.count(c) for c in [, , , , ;]), encoded_char_count: full_text.count(%), sql_kw_count: sum(1 for kw in SQL_KEYWORDS if kw in full_text), xss_kw_count: sum(1 for kw in XSS_KEYWORDS if kw in full_text), has_script_tag: int(script in full_text), digit_ratio: sum(c.isdigit() for c in url) / (len(url) 1), } return list(feats.values())每个特征都有明确含义special_char_count高往往意味着注入尝试encoded_char_count高可能是绕过检测的编码攻击sql_kw_count和xss_kw_count是直接的关键词命中统计。digit_ratio用 URL 中数字占比衡量因为很多扫描器生成的路径含大量随机数字。这些特征加起来十维左右对毕设来说足够跑出一个像样的基线。特征不是越多越好我见过有人堆到上百维结果过拟合严重交叉验证分数反而下降。2.3 训练集与测试集的划分陷阱划分数据时最容易翻车的地方是随机划分导致同一类请求的变体同时出现在训练集和测试集里。比如同一个攻击 payload 稍作修改一条在训练集一条在测试集模型等于抄答案准确率虚高到 99%。正确做法是按请求来源或时间划分确保测试集里的攻击类型在训练集中没有完全相同的副本。实操中可以用GroupShuffleSplit把同一来源 IP 或同一攻击类型的请求分到同一组。from sklearn.model_selection import GroupShuffleSplit # groups 可以按 URL 路径前缀或来源标识构造 groups [urlparse(req.split(\n)[0].split( )[1]).path.split(/)[1] for req, _ in normal attack] X np.array([extract_features(req) for req, _ in normal attack]) y np.array([label for _, label in normal attack]) gss GroupShuffleSplit(n_splits1, test_size0.3, random_state42) train_idx, test_idx next(gss.split(X, y, groupsgroups)) X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx]GroupShuffleSplit保证同一组的样本不会同时出现在训练和测试中。test_size0.3是常用比例数据量小时可以调到 0.2。random_state固定后结果可复现写论文时这点很重要答辩老师让你现场跑一遍结果得对得上。3. 模型选型与训练从逻辑回归到 XGBoost 的取舍3.1 为什么先跑逻辑回归再上集成模型毕设里常见的一个误区是一上来就上深度学习结果数据量不够模型不收敛调参调到怀疑人生。我的建议是先用逻辑回归跑一个基线它的好处是训练快、可解释性强、系数能直接看出哪个特征重要。基线跑通后再上随机森林或 XGBoost 做提升这样论文里也有对比实验可写。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.metrics import classification_report pipe_lr Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(class_weightbalanced, max_iter1000, random_state42)) ]) pipe_lr.fit(X_train, y_train) y_pred_lr pipe_lr.predict(X_test) print(classification_report(y_test, y_pred_lr, digits4))StandardScaler对逻辑回归是必须的因为特征量纲差异大URL 长度可能上百关键词计数只有个位数不归一化会导致梯度下降震荡。class_weightbalanced自动按类别频率反比加权缓解攻击样本少的问题。max_iter1000是防止默认迭代次数不够导致不收敛的警告。3.2 XGBoost 的参数怎么设才不玄学XGBoost 在表格类特征上表现稳定是毕设里性价比很高的选择。关键参数就几个n_estimators控制树的数量max_depth控制单棵树深度learning_rate控制每棵树的贡献。经验值是max_depth6、learning_rate0.1、n_estimators200起步然后用早停法找最优轮数。import xgboost as xgb from sklearn.metrics import accuracy_score, f1_score model_xgb xgb.XGBClassifier( n_estimators300, max_depth6, learning_rate0.1, subsample0.8, # 每棵树随机采样 80% 样本防过拟合 colsample_bytree0.8, # 每棵树随机采样 80% 特征 scale_pos_weight3, # 攻击样本少时调大约等于负正样本比 eval_metriclogloss, random_state42 ) model_xgb.fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse) y_pred_xgb model_xgb.predict(X_test) print(Accuracy:, accuracy_score(y_test, y_pred_xgb)) print(F1:, f1_score(y_test, y_pred_xgb))subsample和colsample_bytree是防过拟合的核心参数取值 0.8 是常用起点数据量小可以降到 0.6。scale_pos_weight设成负正样本比例的近似值比如正常 10000 条攻击 3000 条就设 3 左右。eval_set传入测试集后可以用model_xgb.evals_result()看每轮损失判断有没有过拟合。如果训练损失一直降但验证损失开始升就是过拟合信号该减max_depth或加正则项了。3.3 评估指标不能只看准确率入侵检测场景下准确率是最容易骗人的指标。如果正常请求占 90%模型全判正常也有 90% 准确率但攻击一个没抓到。必须看召回率Recall和 F1。召回率衡量攻击样本被抓住的比例F1 是精确率和召回率的调和平均。毕设答辩时老师大概率会问「你这个模型漏报率多少」提前把混淆矩阵和分类报告准备好。指标含义毕设建议目标Accuracy整体判对比例参考即可不作为核心Precision判为攻击的里真攻击比例0.90 以上Recall真攻击里被抓住比例0.85 以上F1精确率与召回率调和0.88 以上AUC排序能力不受阈值影响0.95 以上这些目标值是基于 CSIC 2010 数据集的经验范围实际跑出来有波动正常。如果 Recall 明显偏低先检查特征里有没有漏掉关键攻击模式再考虑调分类阈值。model_xgb.predict_proba(X_test)[:, 1]拿到概率后把阈值从 0.5 降到 0.3 能提高召回代价是精确率下降具体取舍得看业务场景。4. 系统落地Flask 把模型包成一个能演示的 Web 服务4.1 最小可运行的服务端结构毕设最终要交的是一个能跑的系统不是一堆 notebook。用 Flask 搭一个最小服务核心就三个路由首页展示、单条检测接口、批量检测接口。模型用joblib序列化后加载避免每次请求重新训练。from flask import Flask, request, jsonify, render_template import joblib import numpy as np from feature import extract_features # 前面定义的特征函数 app Flask(__name__) model joblib.load(xgb_model.pkl) # 训练好后保存的模型 app.route(/) def index(): return render_template(index.html) app.route(/detect, methods[POST]) def detect(): raw request.form.get(request_text, ) if not raw.strip(): return jsonify({error: 请求内容为空}), 400 feats np.array([extract_features(raw)]) prob model.predict_proba(feats)[0][1] label 恶意 if prob 0.5 else 正常 return jsonify({ label: label, confidence: round(float(prob), 4), features: extract_features(raw) }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)joblib.load加载的模型文件要在训练脚本里用joblib.dump(model_xgb, xgb_model.pkl)保存。predict_proba返回两个概率取索引 1 是恶意类的概率。debugFalse在生产演示时更安全避免调试页面暴露内部信息。host0.0.0.0让局域网内其他机器也能访问答辩时用手机演示方便。4.2 前端展示与结果解释前端不需要花哨一个文本框输入原始请求一个按钮提交下面显示判定结果和置信度。加分项是把提取到的特征也展示出来让老师看到模型不是黑匣子。用原生 HTML 加一点 JavaScript 的 fetch 就够了不必上 Vue 或 React毕设重点在算法不在前端框架。!-- templates/index.html 核心片段 -- form iddetectForm textarea namerequest_text rows8 cols80 placeholder粘贴原始 HTTP 请求.../textarea button typesubmit检测/button /form div idresult/div script document.getElementById(detectForm).onsubmit async (e) { e.preventDefault(); const formData new FormData(e.target); const resp await fetch(/detect, { method: POST, body: formData }); const data await resp.json(); document.getElementById(result).innerHTML p判定b${data.label}/b置信度${data.confidence}/p pre${JSON.stringify(data.features, null, 2)}/pre; }; /scriptFormData自动收集表单字段fetch发 POST 请求。返回的 JSON 里带特征数组前端用pre展示便于阅读。这段代码没有依赖任何前端库复制就能用。如果老师要求界面美观加个 CSS 框架的 CDN 链接即可但别在这上面花太多时间。4.3 批量检测与日志留存单条检测演示够了但系统感不足。加一个批量上传接口接收一个文本文件逐条检测后返回统计结果同时把每次检测写入日志文件。日志用 Python 标准库的logging模块按天切分方便后续分析。import logging from logging.handlers import TimedRotatingFileHandler handler TimedRotatingFileHandler(detect.log, whenD, backupCount7) handler.setFormatter(logging.Formatter(%(asctime)s %(message)s)) app.logger.addHandler(handler) app.logger.setLevel(logging.INFO) app.route(/batch, methods[POST]) def batch_detect(): file request.files.get(file) if not file: return jsonify({error: 未上传文件}), 400 content file.read().decode(utf-8, errorsignore) blocks [b.strip() for b in content.split(\n\n) if len(b.strip()) 10] results [] for block in blocks: feats np.array([extract_features(block)]) prob float(model.predict_proba(feats)[0][1]) results.append({label: 恶意 if prob 0.5 else 正常, prob: round(prob, 4)}) app.logger.info(fprob{prob:.4f} len{len(block)}) malicious sum(1 for r in results if r[label] 恶意) return jsonify({total: len(results), malicious: malicious, detail: results})TimedRotatingFileHandler按天切日志backupCount7保留一周。批量接口把结果汇总后返回总数和恶意数量前端可以画个饼图。日志里记录概率和请求长度出问题时能回溯是哪条请求判错了。这套东西加起来不到 200 行代码但系统完整度提升明显。5. 避坑与排查那些让毕设卡住三天的真实问题5.1 现象模型训练报错「Input contains NaN」原因特征提取时某些请求没有 body 或 URL 为空导致除零或urlparse返回空值特征向量里出现nan。解决在extract_features里对每个特征做兜底除法分母加 1空字符串返回 0。训练前用np.isnan(X).any()检查一遍有 nan 就定位到具体样本。5.2 现象测试集准确率 99%换一批数据掉到 60%原因训练集和测试集划分时同一攻击变体泄漏模型记住了特定 payload 而不是学到泛化模式。解决改用GroupShuffleSplit按来源分组或者手动按攻击类型划分确保测试集包含训练集没见过的攻击类型。这个坑我踩过论文里写 99% 答辩被追问一句就露馅。5.3 现象Flask 服务启动后请求超时原因模型文件太大加载慢或者debugTrue时重载机制导致每次请求都重新加载。解决模型在应用启动时加载一次放在全局变量里debug设为False如果模型确实大考虑用joblib的mmap_mode或换轻量模型。另外 Flask 默认单线程并发高时用gunicorn -w 4起多进程。5.4 现象XGBoost 训练时警告「scale_pos_weight」无效原因scale_pos_weight只在二分类且标签为 0/1 时生效如果标签是字符串或做了 one-hot 编码就不起作用。解决确认y_train是 0/1 整数数组用np.unique(y_train)检查。另外这个参数和class_weight不要同时用选一个即可。5.5 现象前端提交后返回 400 错误原因request.form.get(request_text)取不到值通常是表单字段名不匹配或 Content-Type 不对。解决打开浏览器开发者工具的 Network 面板看请求体里字段名是否和代码一致用fetch时如果发 JSON 要用request.get_json()而不是request.form。这个错误信息量少但排查靠看请求体最快。6. 把分数从 85 推到 95三个让答辩老师眼前一亮的技巧第一个技巧是做特征重要性分析并可视化。XGBoost 训练完直接调model_xgb.feature_importances_拿到每个特征的贡献度用 matplotlib 画横向条形图。答辩时指着图说「URL 长度和特殊字符数是判别力最强的两个特征」比干巴巴念准确率有说服力得多。代码就三行import matplotlib.pyplot as plt feat_names [url_len, path_depth, num_params, body_len, special_char, encoded_char, sql_kw, xss_kw, has_script, digit_ratio] importances model_xgb.feature_importances_ idx np.argsort(importances) plt.barh(np.array(feat_names)[idx], importances[idx]) plt.xlabel(Importance) plt.tight_layout() plt.savefig(feature_importance.png, dpi150)np.argsort排序后画横向条形图dpi150保证论文里插图清晰。这张图往论文里一放特征工程部分的论述就有了支撑。第二个技巧是加一个阈值调优的实验对比。默认 0.5 阈值不一定最优跑一组 0.1 到 0.9 的阈值画 Precision-Recall 曲线找到 F1 最大的点。论文里写「经阈值调优后 F1 从 0.88 提升到 0.91」这就是实打实的优化点。用sklearn.metrics.precision_recall_curve一行拿到曲线数据。第三个技巧是留一个「在线学习」的接口。系统跑起来后允许用户对判定结果反馈「判错了」把这条样本加入待标注队列定期重新训练。这个功能不需要真的实现自动重训但接口留出来论文里写「系统支持人工反馈闭环」架构完整度就上去了。实现上就是一个/feedback路由把请求和修正标签存到 SQLite简单可靠。我自己的习惯是每次改完特征或参数先把classification_report和混淆矩阵打印出来存到日志再跑一遍完整流程确认没有回归。毕设代码不怕简单怕的是跑不通和结果对不上。把上面这些串起来从数据加载到系统演示一条链路走通95 分以上是水到渠成的事。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑