
简介一套基于机器学习的 Web 攻击检测系统完整源码面向毕业设计、期末大作业与课程设计等场景适合有一定 Python 基础、希望快速搭建安全检测项目的学生参考复用。系统涵盖数据预处理、模型训练与攻击识别等关键环节配套详细文档说明与代码注释整体结构清晰便于二次开发与答辩展示。压缩包共 69 个文件约 26.55MB核心内容以 Python 脚本、模型权重pkl/pb/h5/word2vec、环境配置 yml、说明文档 md/txt 以及少量样例数据与界面图片为主同时包含 pyc 缓存、csv 数据与 pcap 抓包样本帮助理解项目运行结果与测试流程训练好的模型文件也已打包在内拿到后可按已有流程直接使用或对照学习。资源目前已有 136 人学习关注作为导师认可的高分项目具备较高的完整度和实用性能帮助快速完成功能完善、界面美观的 Web 攻击检测系统是毕业设计或综合作业的优质起点。1. 机器学习 Web 攻击检测这套源码解决什么问题适合谁用把机器学习用在 Web 攻击检测上有一个反直觉的结论规则引擎拦不住未知变种的攻击但一个训练得当的模型可以。这套基于机器学习的 Web 攻击检测系统源码把完整链路拆成了能直接跑通的代码——从 HTTP 日志清洗、标签标注、特征提取到模型训练、阈值调优、Flask 实时检测接口每一步都有对应脚本和一份能跟着走的说明文档。它不是给你一个黑匣子模型就完事而是让你能看到数据怎么进、特征怎么算、阈值为什么这么设。适合三类人做毕业设计或期末大作业的学生想在内部快速搭一个轻量检测原型的从业者以及想搞懂特征工程和模型调参落地细节的初学者。2. 从日志到数据集字段设计、标签处理和样本平衡怎么做2.1 先定检测目标再定日志字段拿到这类项目第一件事不是跑代码而是搞清楚它要检测什么。常见的检测目标是 SQL 注入、XSS、命令注入三类多分类场景下还会带上路径穿越、文件包含等。检测目标的定义方式决定你用哪些字段做特征如果只看 URL检测不到 POST body 里的注入载荷如果只关注请求头又容易漏掉参数里的编码混淆。我一般会把请求日志按下面的字段结构落成 CSV这套字段设计也是源码里默认的输入格式。字段类型说明methodstringGET / POST / PUT / DELETEurlstring去掉域名后的路径和查询参数bodystringPOST 请求体可能为空headersstring关键请求头拼接控制长度防噪statusint响应状态码ipstring来源 IP用于分组切分timestampdatetime请求时间用于时间切片验证labelstringnormal / sqli / xss / cmdi 等注意 status 和 ip 不是特征的全部它们更多用在数据切分上。timestamp 尤其重要很多人忽略它但 Web 日志本质是时间序列训练集和测试集如果随机混着切后面会出大问题第 5 章专门讲这个坑。2.2 读取数据和标签映射源码里训练脚本的第一步是把 CSV 读进来然后做标签映射。这里有一个容易翻车的点label 列里可能存在漏标、多标或 NaN映射后必须过滤掉无法识别的行否则模型会学到一堆噪声。import pandas as pd from sklearn.utils import class_weight import numpy as np df pd.read_csv(web_attack_logs.csv, encodingutf-8, dtype{ip: string}) print(原始样本量:, df.shape) print(df[label].value_counts()) label_map {normal: 0, sqli: 1, xss: 2, cmdi: 3, other_attack: 4} df[y] df[label].map(label_map) df df[df[y].notna()] df[y] df[y].astype(int) print(过滤后样本量:, df.shape)读文件时给 ip 指定为 string 类型是防止像 255.255.255.255 这种纯数字 IP 被 pandas 自动识别成 float 后丢失精度。过滤掉 label 无法识别的行后再用 value_counts 确认每个类别的样本量这个数字直接决定后面用哪种样本平衡策略。如果只做二分类把 sqli、xss、cmdi、other_attack 全部映射成 1normal 映射成 0 即可。2.3 样本不平衡先算权重别急着删数据Web 攻击日志的典型分布是正常请求占九成以上攻击样本往往只有几百到几千条。如果直接用原始分布训练模型会倾向把所有样本都判成正常因为这样准确率也能到 90% 以上。常见的做法有两个一是物理下采样把负样本压到正样本的三到五倍二是计算类别权重让少数类样本的 loss 惩罚更大。我不会一上来就删数据而是先算 class_weight保留原始分布让模型自己适应。class_weights class_weight.compute_class_weight( balanced, classesnp.array([0, 1, 2, 3, 4]), ydf[y] ) class_weight_dict {i: class_weights[i] for i in range(5)} print(类别权重:, class_weight_dict)compute_class_weight 的 balanced 模式会按 n_samples / (n_classes * np.bincount(y)) 自动计算每个类的权重。如果你的模型支持 class_weight 参数直接把它传进去如果用的是 XGBoost就传给 scale_pos_weight 或 sample_weight。物理下采样留给训练速度撑不住的时候再用而且下采样会丢掉真实流量分布信息部署后容易产生分布偏移。3. 特征工程TF-IDF 统计特征把 HTTP 请求变成模型能懂的向量3.1 为什么选 TF-IDF 而不是 CountVectorizer文本向量化最基础的是词袋模型但 Web 请求文本里 GET、HTTP、Host 这类词几乎每个样本都有词袋模型会给它们很高的计数权重而对真正有区分度的 payload 关键词关注不够。TF-IDF 的核心思路是词频高但普遍出现的词权重被拉低词频低但在少数样本里出现的词权重被拉高。这正好匹配攻击检测场景——SQL 注入的 union、select、sleepXSS 的 script、onerror这些词在正常请求里极少出现IDF 值会很高。源码里默认参数我梳理了一下也是我平时在同类项目里的习惯配置参数取值作用ngram_range(1, 3)覆盖单词、二元、三元组合捕捉“union select”这类连续语义max_features5000限制特征维度防止稀疏矩阵过大sublinear_tfTrue用 1log(tf) 平滑抑制高频词strip_accentsunicode统一编码防止乱码干扰analyzerword按词切分Web 攻击载荷通常以空格和符号分隔ngram_range 是这里最值得调的参数。只取 (1, 1) 会丢掉“union select”这种组合语义取到 (1, 4) 会带来大量稀疏特征且训练变慢(1, 3) 是精度和速度的平衡点。3.2 统计特征长度、特殊字符、熵光靠 TF-IDF 还不够。很多攻击载荷通过编码绕过文本匹配比如把 select 写成 sele%63t、把空格替换成注释符。这类变种在词频上可能和正常请求很像但它们的字符分布特征暴露了问题URL 异常长、特殊字符密度高、字符熵偏大。统计特征就是用来抓这些信号的。import re import math def statistical_features(text): if not isinstance(text, str) or len(text) 0: return [0, 0, 0, 0, 0] length len(text) param_count text.count() 1 if ? in text else 0 special_count len(re.findall(r[\();,], text)) upper_ratio sum(c.isupper() for c in text) / length freq {} for c in text: freq[c] freq.get(c, 0) 1 entropy -sum(cnt / length * math.log2(cnt / length) for cnt in freq.values()) return [length, param_count, special_count, round(upper_ratio, 4), round(entropy, 4)]special_count 的正则里有单引号、双引号、尖括号、圆括号、分号和等号。SQL 注入最常见的特征是单引号打破字符串边界XSS 最常见的特征是尖括号包裹标签这两个符号单独拿出来做计数比混在文本里让模型自己找要高效得多。熵的计算用香农熵公式对每个字符统计频率后求和。经过 base64 或十六进制编码的 payload字符分布接近均匀分布熵值显著高于正常 URL这个特征对绕过型攻击很有效。3.3 合并特征矩阵TF-IDF 输出的是稀疏矩阵统计特征输出的是稠密数组。合并时用 scipy.sparse.hstack 把两者横向拼接注意统计特征也要转成稀疏矩阵否则拼接后整个矩阵会变成稠密的内存直接爆掉。from scipy.sparse import hstack corpus df[url].fillna() df[body].fillna() X_stat np.array([statistical_features(text) for text in corpus]) X_tfidf tfidf.fit_transform(corpus) X hstack([X_tfidf, X_stat.astype(float)]).tocsr() print(特征矩阵维度:, X.shape)corpus 构造时把 URL 和 body 拼在一起中间用空格分隔这样 TF-IDF 的 ngram 不会跨字段组合出无意义的词组。hstack 要求两个矩阵行数一致这一行如果报错先检查是不是 corpus 长度和 df 行数不匹配。合并后 X.shape 大概是 (样本数, 50005)5000 来自 max_features5 来自统计特征维度。稀疏度通常在 99% 以上实际参与计算的只有少数非零位。4. 模型训练与调参三套算法对比阈值别默认设 0.54.1 算法选型逻辑回归、随机森林、XGBoost 怎么选同一个特征矩阵不同模型的精度、训练速度和可解释性差异很大。三套算法我都跑过给一个直观的对比模型训练速度可解释性精度上限风险点LogisticRegression最快高中特征非线性关系学不到RandomForest快中中高容易过拟合噪声特征XGBoost慢低高参数多调参成本高我的建议先用逻辑回归跑通全流程确认数据管道没问题再上 XGBoost 追求精度。逻辑回归的优势是训练快几分钟能出结果而且系数可以用作特征重要性参考——某个特征权重绝对值越大说明它和攻击类别的相关性越强。XGBoost 的精度通常能比随机森林高两到三个点但对特征尺度不敏感、对异常值鲁棒代价是网格搜索耗时长。如果你的机器没有 GPUXGBoost 全量数据训练可能要十几分钟先用 30% 样本做参数搜索定好参数再全量训练。4.2 网格搜索与交叉验证网格搜索的常见误区是把所有参数都交给 GridSearchCV导致组合爆炸。我的习惯是先固定学习率只搜 n_estimators 和 max_depth 这两个对精度影响最大的参数确定后再微调 min_child_weight。from xgboost import XGBClassifier from sklearn.model_selection import GridSearchCV, StratifiedKFold param_grid { n_estimators: [100, 200, 300], max_depth: [6, 10, 14], min_child_weight: [1, 3] } model XGBClassifier(eval_metriclogloss, use_label_encoderFalse) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) search GridSearchCV(model, param_grid, cvcv, scoringf1_macro, n_jobs-1) search.fit(X_train, y_train) print(最优参数:, search.best_params_) print(最优F1:, search.best_score_)eval_metric 参数记得设成 loglossXGBoost 新版默认的 eval_metric 会报警告不影响训练但干扰日志。use_label_encoderFalse 是配合新版 XGBoost 的接口变化防止旧参数报错。scoring 选 f1_macro 而不是 accuracy原因在第 5 章样本不平衡那条里讲。4.3 阈值优化0.5 不是默认答案训练完模型后predict 默认把预测概率大于 0.5 判为正类。这个 0.5 是在正负样本均衡的假设下默认的攻击检测场景里正常请求占绝大多数0.5 的阈值会导致误报率难以接受。正确做法是在验证集上画出 precision-recall 曲线找 F1 最大的点作为最优阈值。from sklearn.metrics import precision_recall_curve y_proba best_model.predict_proba(X_val)[:, 1] precision, recall, thresholds precision_recall_curve(y_val, y_proba) f1_scores 2 * precision * recall / (precision recall 1e-6) best_idx f1_scores.argmax() best_threshold thresholds[best_idx] print(最优检测阈值:, round(best_threshold, 4)) print(该阈值下 P/R:, round(precision[best_idx], 4), round(recall[best_idx], 4))f1_scores 里最大值对应的索引就是最优阈值的位置。加 1e-6 是为了防止 precision 和 recall 都为 0 时除零。经过这步调优阈值通常会落在 0.65~0.85 之间而不是 0.5。训练脚本会把最优阈值写成一个单独变量输出让部署端直接引用避免部署时再拍脑袋决定阈值。4.4 保存模型和向量化器模型训练完需要保存两样东西分类器模型和 TF-IDF 向量化器。很多人只保存模型部署时重新初始化向量化器再 fit 一遍然后报出维度不匹配的错误。训练好的 TF-IDF 里有整个词汇表部署时必须直接 transform不允许再 fit。import joblib joblib.dump(best_model, model/xgb_model.pkl) joblib.dump(tfidf, model/tfidf_vectorizer.pkl) print(模型和向量化器已保存到 model/ 目录)用 joblib 而不是 pickle是因为 joblib 对 numpy 数组的内部引用处理更好加载速度更快且不容易出现 pickle 对象图递归序列化导致的内存暴涨或 import 依赖错乱。保存完成后可以立即做一次重加载验证确认模型文件能被正常读回来这一步能提前发现版本不兼容问题。5. 避坑与排查训练和部署中最常见的五个翻车现场5.1 训练时 F1 很高部署后几乎全判成正常现象交叉验证的 F1 达到 0.95但把模型接到真实流量上一测几乎检测不到攻击误报倒是没几个。原因数据切分方式不对。直接调用 train_test_split(random_state42) 时同一个 IP 的连续请求、同一段时间内的相似请求被拆到训练集和测试集两侧模型实际学到的是识别 IP 和路径模板而不是识别攻击特征。切分泄漏造成的指标虚高。解决按时间切片。用 80% 时间段的日志做训练20% 做验证保证验证集全部是模型没见过的未来数据。如果数据集没有时间戳用 GroupShuffleSplit 按 IP 分组切分让同一 IP 的所有样本只出现在一侧。5.2 大量正常样本把模型压成“全部判正常”现象训练完成后测试集准确率 96%但混淆矩阵一看攻击类别召回率只有个位数。原因类别严重不平衡。攻击样本可能只占 3%真实准确率 96% 全是正常类贡献的。模型学到的策略是全预测成正常损失最小。解决训练时传入 4.3 节算的 class_weight_dict或者对正常样本做下采样到攻击样本的 3~5 倍数量。评估指标切换成 f1_macro 和召回率不要再看 accuracy。这个坑在排序上排在前面因为没解决它会直接影响后面所有调参判断。5.3 预测时报特征维度不匹配模板错误提示没任何提示现象部署端调接口传入一条 URL模型 predict 直接抛 “Dimension mismatch” 或 “feature_names mismatch”。原因预测脚本里 TfidfVectorizer 是重新初始化和 fit 的。训练时 fit 出的词汇表包含训练集独有的攻击词预测时的向量化器基于新数据重新学词表两个词表长度不一样特征矩阵维度对不上。解决向量化器在训练端 fit 完成后和模型一起保存部署端只调 transform。增加一行自检逻辑处理前比较 X 的 shape[1] 和模型 expect 的维度不一致时直接打印提示。5.4 sklearn / XGBoost 版本差异导致模型加载失败现象训练环境 A 跑通换到部署环境 Bjoblib.load 报 “ValueError: buffer size” 或者 “ModuleNotFoundError: xgboost” 的怪错。原因sklearn 和大版本升级时pickle/joblib 序列化格式不保证向后兼容。老版本 dump 的模型对象新版本环境里反序列化时类结构变化导致加载失败。解决requirements.txt 固定依赖版本关键版本号写死比如 xgboost1.7.6、scikit-learn1.3.2。模型保存时额外输出一行 sklearn.version和 xgboost.version到日志部署环境装成完全一致的版本。血泪经验不要在部署环境图省事装最新版稳定复现比追求新版重要。5.5 URL 里的中文和特殊编码导致特征抖动现象同一个攻击 payload在浏览器里是 %E7%AD%89%E4%BA%8E在数据集里可能是乱码导致模型对同一攻击的前后判断不一致。原因CSV 读取时 encoding 没统一。有的日志是 UTF-8有的是 GBK混在一起导致同一字符在不同行里被解析成不同编码特征抽取结果完全不一样。解决读取 CSV 时统一指定 encodingutf-8对 corpus 做一次 sanitize把所有非 ASCII 字符统一转义——要么全部 percent-encoding 保留要么全部解码成 unicode不能两种混用。统计特征里的熵对编码方式很敏感编码不统一会让熵值特征完全失真。6. 部署验证与进阶Flask 实时检测接口和 ROC 曲线验证6.1 Flask 接口与阈值配置训练完的模型最终要变成可调用的接口。源码里给了一套 Flask 检测接口核心逻辑是接收 JSON 请求取出 url 和 body走统计特征抽取 TF-IDF transform拼矩阵后预测概率和配置里的阈值比较后输出判定结果。from flask import Flask, request, jsonify import joblib import numpy as np from scipy.sparse import hstack app Flask(__name__) model joblib.load(model/xgb_model.pkl) vectorizer joblib.load(model/tfidf_vectorizer.pkl) THRESHOLD 0.72 # 从 4.3 节验证集上计算得到 app.route(/detect, methods[POST]) def detect(): data request.get_json() url data.get(url, ) body data.get(body, ) text f{url} {body} stat np.array([statistical_features(text)]) vec vectorizer.transform([text]) X hstack([vec, stat]).tocsr() proba model.predict_proba(X)[0, 1] label attack if proba THRESHOLD else normal return jsonify({url: url, score: round(float(proba), 4), label: label}) app.run(host0.0.0.0, port5000)预测时 vectorizer 只做 transform不重新 fit。THRESHOLD 是我根据验证集 PR 曲线算出来的一个值不同数据集上要重新计算接口代码里用配置文件管理不要写死。com 建议加一个日志字段记录每条请求的 score 分布用于后续持续监控模型效果。6.2 ROC 曲线验证模型排序能力模型上线前用测试集画 ROC 曲线确认模型对不同类别的排序能力达标。from sklearn.metrics import roc_auc_score, roc_curve import matplotlib.pyplot as plt y_score model.predict_proba(X_test)[:, 1] print(测试集 AUC:, round(roc_auc_score(y_test, y_score), 4))AUC 0.95 以上说明模型排序能力合格。但要注意 AUC 反映的是整体排序质量不代表高阈值区间一定好——实际部署时看的是在高阈值段如 0.7 以上的精确率这个看 PR 曲线别只看一点。把模型和服务跑通后你还能往这个框架里加东西一是集成更多的攻击类别二是引入在线学习用运行时产生的误报反馈定期微调模型。从那以后我每次拿到攻击检测类项目第一件事不再是翻模型而是先看样本分布和切分方式这两点决定了后面所有调参动作有没有意义。希望帮到你——源码包和配套文档我都整理好了按第 2 到第 4 章的流程跑一遍训练、调参、部署这条链路你就能完整走通。本文还有配套的精品资源点击获取