☰
基于监督学习的Web入侵检测系统:从毕设源码到实战部署
2026/9/28 8:47:41 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生与从业者的高分毕业设计源码,主题为基于监督学习的Web入侵检测系统,采用Python实现,评审分达97分,可直接用于毕业设计、期末课程设计或课程大作业。压缩包共60个文件,约2.25MB,以18个ipynb实验笔记、14个txt样本与词表、14个html页面样本、8个py脚本为主,另含pkl与pickle序列化模型文件、pyc编译文件及README说明,覆盖数据采集、字符处理、去重、特征提取与模型保存等完整流程。资源中既有爬虫与日志解析脚本,也有正常与恶意请求样本、关键词表及参数提取笔记,便于读者理解Web入侵检测从原始流量到监督学习建模的全链路。目前已有188人学习下载,适合需要完整项目方案、可运行代码与排错思路的读者参考复用。

1. 从一份毕设源码说起:监督学习怎么做 Web 入侵检测

很多同学做毕设时都会遇到这个场景:导师给了一个题目叫「基于监督学习的 Web 入侵检测系统」,听起来方向明确,但真正动手时才发现不知道从哪下手。数据集去哪找、特征怎么提、模型选哪个、检测效果怎么评估,每一步都是坑。这份 95 分以上的毕设源码,核心思路就是用 Python 把监督学习落地到 HTTP 请求流量上,通过提取 URL、请求方法、参数等特征,训练分类模型来区分正常请求和恶意攻击。它适合正在做安全方向毕设的本科生,也适合想入门机器学习安全应用的开发者。读完你能搞清楚整个系统的技术链路,知道每个模块为什么这么设计,也能自己动手复现一套可运行的检测流程。

2. 监督学习做入侵检测:为什么选它、数据从哪来

2.1 监督学习在 Web 入侵检测里的定位

Web 入侵检测本质上是一个二分类或多分类问题:给你一条 HTTP 请求,判断它是正常访问还是攻击行为。监督学习的优势在于,只要你有标注好的数据集,就能训练出一个可用的分类器。常见做法是用公开的 Web 攻击数据集,比如 CSIC 2010、ECML/PKDD 2007,这些数据集里每条请求都标了 normal 或 anomalous。

和基于规则匹配的 WAF 相比,监督学习的好处是能泛化到没见过的攻击变种。规则匹配遇到编码变形、参数污染就容易漏报,而模型学的是统计模式,对变形有一定的鲁棒性。当然代价是需要标注数据,而且模型的可解释性不如规则。

我一般会先把问题定义清楚:输入是一条 HTTP 请求的原始文本,输出是 0(正常)或 1(攻击)。特征工程的核心就是把原始文本转成数值向量,这一步直接决定模型上限。

2.2 数据集获取与预处理的最小流程

CSIC 2010 是最常用的入门数据集,包含 36000 多条正常请求和 25000 多条攻击请求,覆盖 SQL 注入、XSS、路径遍历等类型。下载后你会得到几个文本文件,每行是一条完整的 HTTP 请求。

预处理的第一步是解析原始请求。用 Python 的urllib.parse把 URL 拆成路径、查询参数、参数值。下面是一个最小可用的解析函数:

from urllib.parse import urlparse, parse_qs, unquote def parse_http_request(raw_line): """ 解析一行原始 HTTP 请求,返回结构化字段 raw_line 格式示例: "GET /search?q=test HTTP/1.1" """ parts = raw_line.strip().split(' ') if len(parts) < 2: return None method = parts[0] url = parts[1] parsed = urlparse(url) path = unquote(parsed.path) # 解码路径,还原 %2e 等编码 query = parse_qs(parsed.query) # 解析查询参数为字典 # 把参数值拼成一个字符串,方便后续做字符级特征 param_values = ' '.join([v for vals in query.values() for v in vals]) return { 'method': method, 'path': path, 'query': parsed.query, 'param_values': param_values, 'raw': raw_line.strip() }

这段代码的逻辑是:先按空格切分请求行,拿到方法和 URL;然后用urlparse拆出路径和查询串;unquote做一次 URL 解码,因为攻击者经常用百分号编码绕过检测;parse_qs把查询串转成字典,方便按参数名取值。参数说明:raw_line是数据集里的一行文本,method和path后续会做独热编码,param_values用来提取特殊字符比例等特征。

预处理完还要做标签对齐。CSIC 2010 的正常请求和攻击请求是分开存放的,你需要给每条请求打上 0 或 1 的标签,然后合并成一个 DataFrame。常见做法是用 pandas 读入,加一列label,最后 shuffle 一下再切分训练集和测试集。

注意:切分数据集时一定要用train_test_split的stratify参数,保证训练集和测试集的类别比例一致,否则评估结果会失真。

3. 特征工程:把 HTTP 请求变成模型能吃的数字

3.1 三类核心特征与提取代码

特征工程是这套系统里最需要经验的部分。我一般把特征分成三组:字符统计特征、路径结构特征、参数语义特征。

字符统计特征最直接:统计请求里特殊字符的出现次数和比例。SQL 注入里常见单引号、双引号、分号、注释符,XSS 里常见尖括号、script 关键字。下面这个函数提取一组基础统计量:

import re import numpy as np def extract_char_features(parsed_req): """ 从解析后的请求中提取字符级统计特征 返回一个固定长度的数值列表 """ raw = parsed_req['raw'] path = parsed_req['path'] params = parsed_req['param_values'] features = [] # 特殊字符计数 special_chars = ["'", '"', ';', '<', '>', '(', ')', '=', '%'] for ch in special_chars: features.append(raw.count(ch)) # 特殊字符占总长度的比例 total_len = len(raw) + 1e-6 features.append(sum(features) / total_len) # 路径深度 features.append(path.count('/')) # 参数值长度 features.append(len(params)) # 是否包含 SQL 关键字 sql_keywords = ['select', 'union', 'insert', 'update', 'delete', 'drop'] features.append(sum(1 for kw in sql_keywords if kw in raw.lower())) # 是否包含 XSS 关键字 xss_keywords = ['script', 'alert', 'onerror', 'onload', 'iframe'] features.append(sum(1 for kw in xss_keywords if kw in raw.lower())) # 数字和字母的比例 digits = sum(c.isdigit() for c in raw) letters = sum(c.isalpha() for c in raw) features.append(digits / total_len) features.append(letters / total_len) return features

逻辑说明:前 10 个特征统计特殊字符出现次数,第 11 个是特殊字符占比,第 12 个是路径层级,第 13 个是参数值总长度,第 14、15 个是 SQL 和 XSS 关键字命中数,最后两个是数字和字母占比。参数方面,parsed_req是上一节解析函数的输出,1e-6是防止除零。这些特征加起来一共 17 维,配合后面的模型足够跑出一个不错的基线。

3.2 特征归一化和维度控制

提取出来的特征量纲不统一,比如字符计数可能是 0 到几百,而比例特征是 0 到 1。直接喂给逻辑回归会导致大数值特征主导梯度。常见做法是用StandardScaler做标准化,或者用MinMaxScaler缩到 0 到 1。

from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split import pandas as pd # 假设 df 里有 raw_request 列和 label 列 parsed_list = [parse_http_request(r) for r in df['raw_request']] feature_matrix = np.array([extract_char_features(p) for p in parsed_list]) labels = df['label'].values # 标准化 scaler = StandardScaler() feature_matrix = scaler.fit_transform(feature_matrix) # 切分,stratify 保证类别比例 X_train, X_test, y_train, y_test = train_test_split( feature_matrix, labels, test_size=0.3, random_state=42, stratify=labels )

这里StandardScaler的fit_transform只在训练集上做,测试集要用训练集的均值方差来transform,否则会数据泄露。random_state=42是为了结果可复现,stratify=labels保证正负样本比例一致。

维度控制方面,17 维不算多,但如果后续加了 TF-IDF 或者 n-gram 特征,维度可能上千。这时候要么用SelectKBest做特征选择,要么用 PCA 降维。我一般先看特征重要性,把贡献低的直接砍掉,比 PCA 更直观。

提示:特征提取函数里用了raw.lower()做大小写归一,因为攻击者经常用SeLeCt这种混合大小写绕过规则,但模型对大小写不敏感反而更稳。

4. 模型训练与评估:从逻辑回归到集成学习

4.1 基线模型选择和训练脚本

特征准备好之后,模型选择其实没有想象中那么玄学。Web 入侵检测的特征维度不高,样本量几万条,逻辑回归和随机森林都能跑出不错的效果。我一般先用逻辑回归做基线,因为它训练快、可解释性强,系数能看出哪些特征重要。

from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score # 逻辑回归基线 lr = LogisticRegression(max_iter=1000, class_weight='balanced', random_state=42) lr.fit(X_train, y_train) lr_pred = lr.predict(X_test) lr_prob = lr.predict_proba(X_test)[:, 1] print("逻辑回归评估:") print(classification_report(y_test, lr_pred, target_names=['正常', '攻击'])) print("AUC:", roc_auc_score(y_test, lr_prob)) # 随机森林对比 rf = RandomForestClassifier(n_estimators=100, max_depth=15, random_state=42, n_jobs=-1) rf.fit(X_train, y_train) rf_pred = rf.predict(X_test) print("随机森林评估:") print(classification_report(y_test, rf_pred, target_names=['正常', '攻击']))

参数说明:max_iter=1000是因为逻辑回归默认迭代次数可能不够收敛;class_weight='balanced'在类别不均衡时自动调权重,CSIC 数据集正常请求偏多,这个参数很关键;随机森林的n_estimators=100是树的数量,max_depth=15控制过拟合,n_jobs=-1用满所有 CPU 核。

评估指标不能只看准确率。如果正常请求占 70%,全猜正常也有 70% 准确率,但攻击一个都抓不到。所以重点看攻击类别的召回率和 F1,以及 AUC。我一般要求攻击召回率在 0.95 以上,误报率控制在 5% 以内。

4.2 交叉验证和超参数调整

单次切分的评估有随机性,更稳的做法是做 5 折交叉验证。用cross_val_score可以快速看模型在不同数据划分下的表现:

from sklearn.model_selection import cross_val_score, GridSearchCV # 5 折交叉验证,看 F1 的均值和方差 cv_scores = cross_val_score(rf, X_train, y_train, cv=5, scoring='f1') print(f"F1 均值: {cv_scores.mean():.4f}, 标准差: {cv_scores.std():.4f}") # 网格搜索调参 param_grid = { 'n_estimators': [50, 100, 200], 'max_depth': [10, 15, 20, None], 'min_samples_split': [2, 5, 10] } grid = GridSearchCV( RandomForestClassifier(random_state=42, n_jobs=-1), param_grid, cv=3, scoring='f1', n_jobs=-1, verbose=1 ) grid.fit(X_train, y_train) print("最佳参数:", grid.best_params_) print("最佳 F1:", grid.best_score_)

cross_val_score的scoring='f1'直接优化 F1,比准确率更贴合安全场景。GridSearchCV的cv=3是内部交叉验证折数,n_jobs=-1并行搜索。注意网格搜索很吃时间,参数组合多的时候先粗调再细调。

如果逻辑回归和随机森林效果都不理想,可以试试 XGBoost 或 LightGBM,这两个在表格数据上通常更强。但毕设场景下,随机森林的 F1 到 0.97 以上已经够用了,没必要上太复杂的模型。

注意:训练完模型一定要用joblib.dump保存,不然每次演示都要重新训练。保存时把 scaler 也一起存,推理时要先做同样的标准化。

5. 避坑与排查:那些让我熬夜的翻车现场

5.1 数据泄露导致评估虚高

现象:训练集准确率 99%,测试集也有 98%,但实际部署后误报一堆。原因:特征提取时用了全局统计量,比如用整个数据集的均值做填充,或者标准化时在切分前就fit了全部数据。解决:所有拟合操作只在训练集上做,测试集用训练集的参数transform。用Pipeline把 scaler 和模型串起来能避免这个问题。

5.2 URL 解码顺序搞反

现象:模型对%27这种编码的 SQL 注入完全检测不到。原因:先做了关键字匹配再解码,导致%27没被还原成单引号。解决:解析请求时第一步就unquote,把编码还原后再提取特征。但要注意解码两次可能引入新问题,一般解一次就够。

5.3 类别不均衡导致攻击召回率低

现象:模型把大部分请求都判为正常,攻击召回率只有 0.6。原因:正常样本远多于攻击样本,模型偏向多数类。解决:训练时加class_weight='balanced',或者用 SMOTE 做过采样。我一般先试class_weight,不行再上采样。

5.4 特征维度爆炸拖慢训练

现象:加了 TF-IDF 后特征从 17 维涨到 5000 维,训练时间从几秒变成几分钟,效果还没提升。原因:字符级 n-gram 产生大量稀疏特征,很多是噪声。解决:用SelectKBest选前 200 个特征,或者限制 n-gram 范围在 2 到 3,min_df=5过滤低频词。

5.5 推理时忘记加载 scaler

现象:训练时 F1 0.97,部署后预测结果全乱。原因:推理代码只加载了模型,没加载 scaler,特征没标准化就喂进去了。解决:用joblib把 scaler 和模型打包成一个字典一起保存,推理时先scaler.transform再model.predict。

6. 把检测系统跑起来:从脚本到可演示的完整流程

6.1 用 Flask 包一个最小推理接口

毕设答辩时老师通常想看到实际效果,光有训练脚本不够。用 Flask 包一个 HTTP 接口,输入一条请求文本,返回是否攻击,演示起来很直观。

from flask import Flask, request, jsonify import joblib import numpy as np app = Flask(__name__) # 加载训练好的 scaler 和模型 artifact = joblib.load('ids_model.pkl') scaler = artifact['scaler'] model = artifact['model'] @app.route('/detect', methods=['POST']) def detect(): data = request.get_json() raw = data.get('request', '') parsed = parse_http_request(raw) if parsed is None: return jsonify({'error': '无法解析请求'}), 400 features = np.array([extract_char_features(parsed)]) features = scaler.transform(features) pred = model.predict(features)[0] prob = model.predict_proba(features)[0][1] return jsonify({ 'is_attack': bool(pred), 'confidence': round(float(prob), 4) }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)

逻辑说明:接口接收 JSON 格式的请求文本,复用训练时的解析和特征提取函数,保证线上线下一致。joblib.load加载的字典里同时有 scaler 和 model,避免遗漏。返回结果包含是否攻击和置信度,方便演示时展示。参数方面,port=5000是 Flask 默认端口,debug=False在生产演示时关掉调试模式。

启动后用 curl 测一下:

curl -X POST http://127.0.0.1:5000/detect \ -H "Content-Type: application/json" \ -d '{"request": "GET /search?q=1%27%20OR%20%271%27=%271 HTTP/1.1"}'

如果返回is_attack: true,说明整条链路通了。

6.2 验证模型是否真的学到了攻击模式

训练完不能只看指标,还要做行为验证。我一般会构造几条典型攻击和正常请求,看模型输出是否符合预期。下面这个测试脚本覆盖 SQL 注入、XSS、路径遍历和正常访问:

test_cases = [ ("GET /search?q=1' OR '1'='1 HTTP/1.1", True), ("GET /page?name=<script>alert(1)</script> HTTP/1.1", True), ("GET /../../etc/passwd HTTP/1.1", True), ("GET /index.html HTTP/1.1", False), ("POST /login user=admin&pass=123456 HTTP/1.1", False), ] for raw, expected in test_cases: parsed = parse_http_request(raw) feat = scaler.transform(np.array([extract_char_features(parsed)])) pred = model.predict(feat)[0] status = "通过" if pred == expected else "失败" print(f"[{status}] 预期={expected} 实际={pred} | {raw[:50]}")

这个脚本的价值在于,如果某条攻击没检测出来,你能立刻定位是特征没覆盖还是模型没学好。比如路径遍历那条如果失败,说明路径深度特征权重不够,可以针对性加强。

6.3 一个容易被忽略的技巧:特征重要性反推攻击类型

随机森林自带feature_importances_,逻辑回归有coef_。把特征名和重要性对应起来,能看出模型到底在关注什么。我一般会打印前 10 个重要特征,如果发现某个特征权重异常高,就去检查是不是数据泄露或者特征设计有问题。

import pandas as pd feature_names = [ "单引号数", "双引号数", "分号数", "左尖括号", "右尖括号", "左括号", "右括号", "等号", "百分号", "特殊字符占比", "路径深度", "参数长度", "SQL关键字", "XSS关键字", "数字占比", "字母占比" ] importances = rf.feature_importances_ feat_imp = pd.Series(importances, index=feature_names).sort_values(ascending=False) print(feat_imp.head(10))

如果「单引号数」和「SQL关键字」排在前列,说明模型确实学到了 SQL 注入的模式,这个结果是可信的。如果某个无关特征排第一,比如「字母占比」,那就要警惕过拟合。

这套流程跑下来,从数据解析到模型部署大概两三天能搞定,剩下的时间可以打磨特征和调参。我自己的习惯是先把基线跑通,再逐步加特征,每加一组就记录 F1 变化,这样能清楚知道哪个改动真正有用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询