
简介这是一份完整的信息安全课程设计源码采用类型脚本语言构建网络安全检测系统的服务端部分。面向计算机、人工智能、通信工程等专业学生适合作为课程作业、毕业设计或项目初期演示。压缩包内共有63个文件核心为类型脚本源码辅以配置文件、脚本、样式、标记等类型涵盖服务端路由、业务逻辑、数据过滤、工具函数及环境配置等常见模块整体仅241KB结构紧凑且非常便于阅读。目前已有587人学习浏览源码经过运行验证可直接使用或二次开发。该工程按分层思想组织清晰展示安全检测功能的实现思路借助该源码读者可快速掌握基于类型脚本搭建安全检测后端的完整流程满足课程设计或毕业设计对完整性和实用性的要求。1. 一门课程设计为什么值得按生产标准重做“信息安全课程设计-基于TypeScript实现的网络安全检测系统服务端源码.zip”这个标题信息量其实比看上去大得多。它既不是单纯的前端练习也不是用 Python 写个端口扫描器交差——关键词落在“TypeScript”和“服务端”上说明作者希望用一门静态类型语言来构建网络检测系统的后端这在实际的甲方验收、毕业答辩甚至后续找工作时都比“能跑就行”的脚本更有说服力。很多同学拿到类似题目第一反应是选 Node.js JavaScript 快速写完结果类型一乱后期加功能就崩也有不少人用 Java 写完后发现和“TypeScript”这个标题根本对不上答辩时被问得哑口无言。这篇文章要做的就是抛开假设中的那份 zip从零讲清楚一套可落地的方案为什么网络安全检测系统的服务端适合用 TypeScript 实现、检测模块和服务端代码如何分层、系统监控与告警的数据流怎么设计以及最终如何验证你的“检测系统”不是纸老虎。适合正在做课程设计的学生也适合刚接触 Node.js 服务端开发、想理解 TS 在真实后端项目中边界在哪里的工程师。下面的内容全部围绕这个标题展开所有代码你都可以直接抄下来改改跑。2. 为什么网络安全检测系统的服务端要选 TypeScript从类型安全到运行时行为2.1 网络检测系统对“类型”的天然依赖网络安全检测系统要处理的对象本质上是一堆结构化程度很低的网络流量IP 地址、端口、协议号、TCP 标志位、HTTP 头、DNS 查询记录……如果后端用纯 JavaScript 写这些数据会被到处传递函数签名只能靠注释维护一旦某个字段改了名运行到一半才会报undefined。而 TypeScript 的核心价值是在编译期把这些网络协议字段变成显式的接口和联合类型。比如一个检测模块收到的原始报文在 TS 里可以定义成// src/types/packet.ts export interface PacketRecord { timestamp: number; // 捕获时间戳ms srcIp: string; dstIp: string; srcPort: number; dstPort: number; protocol: TCP | UDP | ICMP; length: number; tcpFlags?: { syn: boolean; ack: boolean; fin: boolean; rst: boolean; }; payload?: Uint8Array; // 原始负载可选字段 }接口定义完之后所有检测函数都基于这个结构做输入输出编译期就能拦截三分之一左右的低级错误——比如把dstPort拼错成dtsPort或者把protocol赋值为字符串tcpTS 会直接报错而不是等到检查规则跑挂了才知道。对课程设计来说这种“编译期挡错误”的特性天然比无类型脚本更适合承载一个多模块协作的系统。2.2 运行时安全和“可信数据边界”的关系但类型安全并不能解决所有问题。网络检测系统的服务端需要接收来自流量采集器(如 Zeek、Suricata 或自己写的抓包程序)的 JSON 数据这些数据来自不可信的网络环境。理论上TS 的interface在运行时不存在它只是编译期的约束如果你直接JSON.parse一个外部报文再拿去调用检测函数TypeScript 不会在运行时替你校验字段。因此真正的安全检测系统服务端通常会在“边界”处做一层运行时校验TS 在这里发挥的作用是让校验器返回的类型可以自动推断// src/validation/packetValidator.ts import { PacketRecord } from ../types/packet; export function parsePacket(raw: unknown): PacketRecord { if (typeof raw ! object || raw null) { throw new Error(Invalid packet: not an object); } const p raw as Recordstring, unknown; if (typeof p.srcIp ! string || typeof p.dstIp ! string) { throw new Error(Invalid packet: missing IP); } if (!Number.isInteger(p.srcPort) || !Number.isInteger(p.dstPort)) { throw new Error(Invalid packet: bad port); } // 校验通过后这里就能安全地断言为 PacketRecord return p as unknown as PacketRecord; }这个函数在入口把所有外部数据“洗一遍”之后的内部函数才能放心地以PacketRecord作为参数类型。这种做法在工程上叫“管道过滤模型”TS 让这个模型的实现成本低了很多——你不需要在每个检测规则里重复判断字段是否存在。对于课程设计的答辩这也是一个可以展开讲的亮点不是写了 TS 就安全而是用 TS 在边界上建立可信契约。2.3 TypeScript 服务端的主流运行时与框架选型说了优点也要说选型。TS 本身只是语言运行在 Node.js 环境中但 Node.js 的生态里处理网络抓包这种偏底层的事情并不擅长一般会组合两个层采集层用原生net/dgram模块或pcap库业务层则用框架组织 HTTP API 和 WebSocket 推送。常见的选择有两种Express TypeScript入门门槛最低中间件丰富适合课程设计快速出活。缺点是类型支持需要手动补齐异步错误处理要自行封装。NestJS一个重度使用装饰器和依赖注入的框架对 TS 的支持是“一等公民”适合要展示工程化能力的同学。缺点是概念多、学习曲线陡。如果是做课程设计我一般会建议用 Express TS,理由是目标不是做一个微服务架构而是把检测逻辑讲清楚。下面所有示例都基于 Express 和 Node.js 的TypeScript编译配置版本你可以用当前最新的 Node 18/20 LTS 配合typescript5.x。3. 服务端骨架与检测引擎的最小可运行实现3.1 初始化项目tsconfig 与服务入口先建立工程结构。假设你要把系统命名为ts-netdetect在空目录里执行mkdir ts-netdetect cd ts-netdetect npm init -y npm install express ws npm install -D typescript types/node types/express types/ws npx tsc --init --rootDir src --outDir dist --strict true --target ES2020 --module commonjs--strict true很重要它开启了strictNullChecks、noImplicitAny等一系列严格检查。对于刚写 TS 的人来说这个开关会让你在写代码时多报错但后期排错会省大量时间。接下来创建src/server.ts// src/server.ts import express from express; import { createServer } from http; import { attachDetectionRoutes } from ./routes/detection; const app express(); app.use(express.json({ limit: 1mb })); // 路由挂在 /api 前缀下 app.use(/api, attachDetectionRoutes()); const server createServer(app); const PORT process.env.PORT || 3000; server.listen(PORT, () { console.log([netdetect] server listening on ${PORT}); });这里使用createServer而不是app.listen是为了后面如果要做 WebSocket 实时告警推送可以直接把同一个 HTTP 服务器实例交给ws库避免端口分离导致的跨域问题。express.json限制了请求体大小为 1MB因为网络检测结果通常不会太大这个限制能防御简单的大体量请求攻击。3.2 检测引擎核心模块规则注册与匹配检测引擎是整个系统最核心的部分。为了课程设计的可维护性不要把所有规则写在一个巨型函数里而是设计一个简单的规则接口// src/engine/detectionRule.ts import { PacketRecord } from ../types/packet; export interface DetectionResult { ruleId: string; severity: low | medium | high | critical; description: string; matchedAt: number; } export interface DetectionRule { id: string; name: string; severity: DetectionResult[severity]; // 返回 true 表示匹配到攻击行为 match(packet: PacketRecord): boolean; }然后实现一个规则仓库把所有规则收集到一起// src/engine/ruleRegistry.ts import { DetectionRule } from ./detectionRule; import { portScanRule } from ./rules/portScanRule; import { synFloodRule } from ./rules/synFloodRule; export class RuleRegistry { private rules: DetectionRule[] []; constructor() { // 注册内置规则后续可以改成从数据库/配置文件加载 this.rules.push(portScanRule, synFloodRule); } public addRule(rule: DetectionRule): void { this.rules.push(rule); } public evaluate(packet: PacketRecord): DetectionResult[] { const results: DetectionResult[] []; for (const rule of this.rules) { try { if (rule.match(packet)) { results.push({ ruleId: rule.id, severity: rule.severity, description: rule.name, matchedAt: Date.now(), }); } } catch (err) { // 单条规则出错不影响整体检测 console.error([engine] rule ${rule.id} error:, err); } } return results; } }注意evaluate中使用了 try/catch 包裹单条规则这是生产系统的常见做法——规则里可能有偶发的边界异常不能让一条规则导致整个检测流程终止。这个设计在答辩时可以补充说明。3.3 规则示例一SYN Flood 检测SYN 洪泛攻击是最常见的课程设计题目。简单实现方式是检测短时间窗口内单个源 IP 发出的 SYN 包数量是否超过阈值。这里需要一个滑动窗口计数器可以用一个Mapstring, number[]存储时间戳数组// src/engine/rules/synFloodRule.ts import { DetectionRule } from ../detectionRule; import { PacketRecord } from ../../types/packet; const WINDOW_MS 10_000; // 10 秒窗口 const MAX_SYN_COUNT 50; // 阈值窗口内最多 50 个 SYN export const synFloodRule: DetectionRule { id: RULE_SYN_FLOOD, name: SYN flood detected: high rate of SYN packets from a single source, severity: high, match(packet: PacketRecord): boolean { if (packet.protocol ! TCP || !packet.tcpFlags?.syn) { // 只处理 TCP SYN 包ack 为 true 时是连接握手的第二个包不统计 if (packet.tcpFlags?.syn !packet.tcpFlags.ack) { // 这里应该用外部状态但为了简单见下文说明 } return false; } // 注意规则实例是单例无法保存状态。实际实现应通过外部缓存 // 以下提供一个静态计数器示例但课程设计推荐使用 Redis 或内存 Map 注入 return false; }, };上面的写法暴露了一个问题规则对象是单例无法维护跨次调用的状态。因此更好的做法是将规则设计为工厂函数每次通过RuleRegistry构造时传入缓存容器。下面重写一下// src/engine/rules/synFloodRule.ts import { DetectionRule } from ../detectionRule; import { PacketRecord } from ../../types/packet; interface SynFloodState { timestamps: Mapstring, number[]; } export function createSynFloodRule(state: SynFloodState): DetectionRule { const WINDOW_MS 10_000; const MAX_SYN_COUNT 50; return { id: RULE_SYN_FLOOD, name: SYN flood detected, severity: high, match(packet: PacketRecord): boolean { if (packet.protocol ! TCP || !packet.tcpFlags?.syn || packet.tcpFlags.ack) { return false; } const key ${packet.srcIp}:${packet.srcPort}; const now Date.now(); // 取出该源地址在窗口内的时间戳清除掉过期的 const recent (state.timestamps.get(key) || []).filter(t now - t WINDOW_MS); recent.push(now); state.timestamps.set(key, recent); return recent.length MAX_SYN_COUNT; }, }; }createSynFloodRule接收一个外部状态容器这使得多个规则实例可以共享同一个状态也方便测试时重置。timestamps数组使用filter清理过期时间戳避免内存无限增长。这个规则的时间复杂度是 O(n)n 是窗口内请求数对课程设计来说完全够用。3.4 规则示例二端口扫描检测端口扫描检测通常基于连接目的端口的多样性。如果一个源 IP 在短时间内访问了大量不同的目的端口且都是 TCP SYN 包没有完成握手那大概率是扫描行为。同样需要状态// src/engine/rules/portScanRule.ts import { DetectionRule } from ../detectionRule; import { PacketRecord } from ../../types/packet; interface PortScanState { ports: Mapstring, Setnumber; } export function createPortScanRule(state: PortScanState): DetectionRule { const WINDOW_MS 5_000; const MAX_DIFF_PORTS 20; return { id: RULE_PORT_SCAN, name: Port scan detected: multiple different destination ports from a single source, severity: medium, match(packet: PacketRecord): boolean { if (packet.protocol ! TCP || !packet.tcpFlags?.syn || packet.tcpFlags.ack) { return false; } // 用 srcIp 做 key端口集合记录不同目标端口 let portSet state.ports.get(packet.srcIp); if (!portSet) { portSet new Setnumber(); state.ports.set(packet.srcIp, portSet); } portSet.add(packet.dstPort); return portSet.size MAX_DIFF_PORTS; }, }; }这个实现有一个副作用端口集合不会过期如果攻击者放慢扫描速度比如每秒 1 个端口最终还是会触发。要改进的话可以给每个端口加时间戳但暂时够用。课程设计答辩时可以说“当前实现了简易状态若要对抗慢扫描需要引入滑动窗口”。到这里检测引擎已经能接收报文并输出告警结果。接下来需要把这些结果“送出去”——通过 REST API 供前端查询通过 WebSocket 实时推送。4. 服务端接口设计与实时告警推送不只是存数据库4.1 内存事件总线检测引擎和 API 的解耦检测引擎产生告警后最好通过一个事件机制传输而不是直接调用 HTTP 接口。这样可以方便地扩展后续的存储、通知模块。在 TS 里可以用EventEmitter或自定义一个简单总线:// src/events/alertBus.ts import { EventEmitter } from events; import { DetectionResult } from ../engine/detectionRule; export interface AlertEvent { packetId: string; result: DetectionResult; } export class AlertBus { private emitter new EventEmitter(); public publish(event: AlertEvent): void { this.emitter.emit(alert, event); } public subscribe(handler: (event: AlertEvent) void): void { this.emitter.on(alert, handler); } }这个AlertBus对象可以在server.ts中创建单例然后注入到规则引擎的处理器中。当evaluate返回结果后遍历结果并publish。订阅方可以是一个 WebSocket 广播器也可以是一个保存到内存数组的存储器。4.2 REST API 设计查询检测结果与系统状态服务端需要暴露两种接口一个是检测数据的查询接口一个是系统自身的健康检查。用 Express 实现// src/routes/detection.ts import { Router } from express; import { RuleRegistry } from ../engine/ruleRegistry; import { parsePacket } from ../validation/packetValidator; import { alertHistory, addAlert } from ../store/alertStore; export function attachDetectionRoutes(): Router { const router Router(); const registry new RuleRegistry(); // 单条报文检测接口 router.post(/detect, (req, res) { try { const packet parsePacket(req.body); const results registry.evaluate(packet); // 将结果存入历史记录 for (const r of results) { addAlert({ packetId: ${packet.srcIp}:${packet.dstPort}:${Date.now()}, result: r }); } res.json({ results }); } catch (err) { res.status(400).json({ error: (err as Error).message }); } }); // 最近告警查询支持 ?limit50 router.get(/alerts, (req, res) { const limit Math.min(Number(req.query.limit) || 20, 200); res.json(alertHistory.slice(-limit)); }); return router; }parsePacket如果抛错会返回 400 和错误信息这是服务端接口的基本契约。alertHistory用一个简单的数组存储告警为了课程设计不引入数据库但可以在答辩时说“当前使用内存存储正式环境可替换为 Redis 或 PostgreSQL”。4.3 WebSocket 实时推送 alert 事件网络安全检测系统如果只有轮询接口实时性是不够的。更合理的做法是检测到异常时主动推送给前端。使用ws库实现代码很短// src/ws/alertSocket.ts import { WebSocketServer, WebSocket } from ws; import { Server } from http; export function attachAlertSocket(server: Server, bus: AlertBus): void { const wss new WebSocketServer({ server, path: /ws/alerts }); wss.on(connection, (socket: WebSocket) { console.log([ws] client connected); socket.send(JSON.stringify({ type: welcome, message: connected to alert feed })); }); bus.subscribe((event) { // 广播给所有连接的客户端 const message JSON.stringify({ type: alert, data: event }); for (const client of wss.clients) { if (client.readyState WebSocket.OPEN) { client.send(message); } } }); }这里的关键点WebSocketServer的server参数传的是之前createServer创建的 HTTP 实例并且设置了path这样/ws/alerts是一个专用 WebSocket 端点不会和 REST API 冲突。广播时遍历所有客户端readyState判断可以过滤掉断开或正在关闭的连接。4.4 中间件和错误处理服务端的基本素养为了展示工程化能力再加上一个请求日志中间件和统一错误处理中间件。注意 Express 4 的错误处理中间件必须放在所有路由之后// src/middleware/errorHandler.ts import { Request, Response, NextFunction, ErrorRequestHandler } from express; export const logRequests: ErrorRequestHandler (req, res, next) { console.log([${new Date().toISOString()}] ${req.method} ${req.url}); next(); }; export const errorHandler: ErrorRequestHandler ( err: Error, _req: Request, res: Response, _next: NextFunction ) { console.error([error], err); res.status(500).json({ error: internal server error, detail: process.env.NODE_ENV development ? err.message : undefined }); };logRequests的类型写成了ErrorRequestHandler有点取巧更准确的是RequestHandler但为了简洁。errorHandler在开发环境下输出错误详情生产环境隐藏内部细节这是安全意识的体现。至此服务端的骨架已经完成启动后可以 POST 一个报文得到检测结果同时通过 WebSocket 推送。接下来需要填充一个关键模块——存储与历史查询否则所有告警都是“一次性”的重启就丢。5. 检测结果的存储与历史分析从内存数组到可持久化5.1 用 SQLite 做轻量持久化为什么不直接上 MySQL课程设计要求“系统”意味着要有历史数据否则没法展示趋势图。引入一个 SQLite 就能满足需求避免搞复杂数据库的运维。Node.js 里可以用better-sqlite3这个同步库API 简单TS 类型支持也很好npm install better-sqlite3 npm install -D types/better-sqlite3这里选择同步 API 的原因告警写入是低频操作同步写不会阻塞事件循环代码更直观。如果未来要支撑高并发检测再改异步sqlite3或node:sqlite内置模块。5.2 设计告警表结构与数据访问层创建src/store/database.ts// src/store/database.ts import Database from better-sqlite3; const db new Database(netdetect.db); db.exec( CREATE TABLE IF NOT EXISTS alerts ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_id TEXT NOT NULL, severity TEXT NOT NULL, description TEXT NOT NULL, packet_id TEXT NOT NULL, matched_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_alerts_matched_at ON alerts (matched_at); ); export interface AlertRow { id: number; rule_id: string; severity: string; description: string; packet_id: string; matched_at: number; } export function insertAlert(row: OmitAlertRow, id): void { db.prepare( INSERT INTO alerts (rule_id, severity, description, packet_id, matched_at) VALUES (rule_id, severity, description, packet_id, matched_at) ).run(row); } export function queryAlerts(limit: number, severity?: string): AlertRow[] { const base SELECT * FROM alerts; if (severity) { return db.prepare(${base} WHERE severity ? ORDER BY matched_at DESC LIMIT ?) .all(severity, limit) as AlertRow[]; } return db.prepare(${base} ORDER BY matched_at DESC LIMIT ?).all(limit) as AlertRow[]; }创建matched_at索引非常重要因为后续查询历史告警基本都是按时间倒序。SQLite 的预处理语句severity这样的命名参数比字符串拼接安全得多可以避免 SQL 注入——这也是“信息安全”主题下必须展示的细节。5.3 调整 AlertBus 的订阅逻辑入库 广播之前使用alertHistory内存数组现在替换成 SQLite。在server.ts的入口初始化总线时同时注册两个订阅者// src/server.ts import { AlertBus } from ./events/alertBus; import { insertAlert } from ./store/database; import { attachAlertSocket } from ./ws/alertSocket; const bus new AlertBus(); // 订阅: 写入数据库 bus.subscribe((event) { const { result } event; insertAlert({ rule_id: result.ruleId, severity: result.severity, description: result.description, packet_id: event.packetId, matched_at: result.matchedAt, }); }); // 订阅: WebSocket 广播 attachAlertSocket(server, bus);这样每一次检测产生告警都会自动完成两件事写入 SQLite同时推送给所有前端。模块之间只依赖AlertBus的subscribe方法互不感知具体实现未来要加邮件通知、飞书机器人只需要再增加一个subscribe。5.4 统计数据 API按小时聚合告警数为了给前端画趋势图服务端应该提供聚合查询接口。用 SQLite 的strftime按小时分组:// src/routes/statistics.ts import { Router } from express; import Database from better-sqlite3; const db new Database(netdetect.db); export function attachStatisticsRoutes(): Router { const router Router(); router.get(/stats/hourly, (_req, res) { const rows db.prepare( SELECT strftime(%Y-%m-%d %H:00, matched_at / 1000, unixepoch) AS hour, COUNT(*) AS count FROM alerts GROUP BY hour ORDER BY hour DESC LIMIT 24 ).all(); res.json(rows); }); return router; }匹配时间戳的 SQL 写法matched_at / 1000把毫秒转成秒unixepoch告诉 SQLite 输入是 Unix 时间戳。分组后取最近 24 小时前端就能画一个柱状图。这个接口的粒度是小时如果希望更细可以把%H:00改成%H:%M但要注意 SQLite 的格式化字符串。到这里一个基本完整的服务端已经包含输入校验、检测引擎、规则仓库、实时推送、持久化、统计查询。但真正要拿去验收还需要解决三个“看起来简单、实际很容易翻车”的问题并发安全、配置管理、如何测试和演示。6. 并发安全下的状态管理别再犯闭包和全局变量的错6.1 规则状态并发访问的正确姿势在 SYN Flood 和端口扫描规则中状态容器使用Map和Set。Node.js 是单线程事件循环理论上不会出现真正的多线程竞争但要注意异步操作中如果不小心await状态更新可能产生交错。更重要的是多个请求同时到达时这些状态操作都是同步的所以Map操作不会被中断这一点是安全的。但不要因此掉以轻心。如果将来使用 worker threads 或者将状态存储在可变全局中就会出现竞争。课程设计中一个常见的错误是在DetectionRule闭包里直接let count 0这样多实例或热重载时状态会混乱。推荐的模式是“状态容器显式注入”代码里已经体现了。额外注意portScanRule的Set没有清理机制运行时间长了可能内存泄漏。可以增加一个定时器周期性清理旧数据但由于规则对象没有生命周期钩子最好把清理放在AlertBus的订阅中或者启动一个setInterval。这里给出一个简单清理方案// src/engine/clearance.ts export function startStateCleanup(state: PortScanState, intervalMs: number): NodeJS.Timeout { return setInterval(() { state.ports.clear(); console.log([state] cleared port scan state); }, intervalMs); }对课程设计来说把清理周期设为 5 分钟每天清一次逻辑上能接受。如果你在论文中提到“周期性清理旧状态以控制内存占用”这就成了一个加分点。6.2 闭包陷阱规则工厂与参数捕获之前提到的createSynFloodRule接收state参数但要小心闭包捕获的是同一个对象引用。否则如果每次请求都创建新规则状态就丢失了。正确的做法是在应用启动时创建一次规则和状态之后不断复用// src/engine/engineFactory.ts import { RuleRegistry } from ./ruleRegistry; import { createSynFloodRule } from ./rules/synFloodRule; import { createPortScanRule } from ./rules/portScanRule; export function buildEngine(): RuleRegistry { const registry new RuleRegistry(); const synState: SynFloodState { timestamps: new Map() }; const scanState: PortScanState { ports: new Map() }; registry.addRule(createSynFloodRule(synState)); registry.addRule(createPortScanRule(scanState)); // 每 5 分钟清理端口扫描状态 setInterval(() scanState.ports.clear(), 5 * 60 * 1000).unref(); return registry; }unref()在 Node.js 中意味着该定时器不会阻止进程退出。测试时如果忘记清理定时器进程会一直挂住unref()可以避免这个问题——这也是 Node.js 服务端开发中一个少有人提但很实用的细节。6.3 测试与演示用模组批量报文模拟器在课程设计答辩时不可能真的发动攻击所以需要写一个测试脚本生成符合PacketRecord格式的模拟数据批量发送到/api/detect。可以用 Node.js 脚本实现:// scripts/simulate.ts import axios from axios; const API http://localhost:3000/api/detect; function makeSynPacket(srcIp: string, dstPort: number) { return { srcIp, dstIp: 192.168.1.10, srcPort: 12345, dstPort, protocol: TCP, length: 60, tcpFlags: { syn: true, ack: false, fin: false, rst: false }, }; } async function run() { for (let i 0; i 60; i) { const packet makeSynPacket(10.0.0.100, 1000 i); await axios.post(API, packet); console.log(sent port ${packet.dstPort}); } console.log(finished simulation); } run().catch((err) { console.error(err); process.exit(1); });模拟 60 个不同的端口如果端口扫描规则阈值是 20很快就能产生告警。运行前先启动npm run dev用ts-node-dev或tsx监听然后另一个终端跑脚本。注意 axios 需要先安装。这组模拟方法比单纯 curl 更有说服力可以在演示时展示实时 WebSocket 收到告警的截图。6.4 性能边界当前实现的极限在哪尽管课程设计不需要高并发但你应该知道自己系统的瓶颈。当前实现中最大的开销是RuleRegistry.evaluate对每条报文遍历所有规则如果规则数量增长到 1000 条每次检测也还是同步的。更关键的是parsePacket中JSON.parse的复杂度是 O(n)报文 payload 越大越慢。因此如果每秒检测 1000 条报文应该用消息队列如 RabbitMQ异步处理或者用 worker threads 分担 CPU 密集的解析。如果想要更复杂的规则可以引入正则匹配或状态机但 TS 类型安全并不能替代算法设计。在答辩时可以诚实地说“当前版本是单线程、同步检测适用于中小规模流量如需扩展可以采用多实例部署 Redis 共享状态”。这样的表述比吹嘘“高并发”更可靠。7. 服务端的验证与验收从接口测试到规则调参技巧7.1 接口自测用 curl 和 wscat 快速验证启动服务后先用 curl 测试一个正常报文和一个异常报文。第一组正常报文curl -X POST http://localhost:3000/api/detect \ -H Content-Type: application/json \ -d {srcIp:10.0.0.1,dstIp:10.0.0.2,srcPort:5000,dstPort:80,protocol:TCP,length:60,tcpFlags:{syn:true,ack:false,fin:false,rst:false}}预期返回{results:[]}因为单个 SYN 不会触发阈值。接着发送一组扫描报文可以写个 shell 循环或直接跑上面的simulate.ts。结束后查询告警列表curl http://localhost:3000/api/alerts?limit5返回的 JSON 中应能看到RULE_PORT_SCAN的记录。对于 WebSocket可以用wscat工具连接npx wscat -c ws://localhost:3000/ws/alerts然后在另一个终端发送模拟数据观察终端里是否收到type: alert的消息。这里提一个坑如果 WebSocket 连接一直没收到消息先确认 HTTP 服务是否绑定在0.0.0.0而不是127.0.0.1其次浏览器打开的前端页面和wscat能连上但服务端广播可能因为异常而没有触发看服务端控制台输出是最直接的排错手段。7.2 规则参数怎么调阈值不是拍脑袋定的MAX_SYN_COUNT 50、MAX_DIFF_PORTS 20这两个值看起来随意实际上是有依据的。正常网络环境中一个客户端在 10 秒内发出 50 个 SYN 的可能性很低除非是大量并行连接比如网页加载时的并发请求也可能达到几十个。为了避免误报课程设计里可以把阈值调高到MAX_SYN_COUNT 100。对于端口扫描一个客户端在 5 秒内访问超过 20 个不同目的端口这个行为特征就比较显著了。调参时建议做成配置文件// src/config/rules.ts export const ruleParams { synFlood: { windowMs: 10_000, maxSynCount: Number(process.env.SYN_MAX_COUNT) || 50, }, portScan: { windowMs: 5_000, maxDiffPorts: Number(process.env.PORT_SCAN_MAX) || 20, }, };这样在答辩时可以直接用环境变量现场演示参数变化比如设置SYN_MAX_COUNT5后马上触发告警比死改成代码再重启更有说服力。7.3 三个容易忽略的安全细节课程设计既然叫“信息安全”服务端代码里必须体现安全实现否则会被要求整改。三个重点限制请求体大小express.json({ limit: 1mb })已经做了但要记得处理PayloadTooLargeError可以在错误处理中间件里单独判断err.type entity.too.large返回 413。校验源 IP 和目的 IP 格式parsePacket目前只检查typeof但abc也能通过。用net.isIP校验更严谨import { isIP } from net; if (!isIP(p.srcIp as string)) { throw new Error(Invalid srcIp); }防止 WebSocket 被随意连接课程设计可以不考虑鉴权但要意识到这是弱点。至少可以设置一个简单的?token查询参数连接时校验。虽然不算强安全但能体现“有安全边界意识”。7.4 验证系统“确实在检测”构造一个小型数据集与其空口说“我的系统能检测攻击”不如准备一个大小为几十 KB 的 JSON 数据集包含正常流量和攻击流量混合然后跑一遍批量检测并统计召回率。由于当前检测规则只有两条数据集可以聚焦在这两个场景场景构造方法预期告警正常 HTTP 流量每个源 IP 访问少数几个端口无 SYN 风暴不告警SYN 洪泛同一源 IP 在 1 秒内发 200 个 SYN 包RULE_SYN_FLOOD端口扫描同一源 IP 在 5 秒内访问 30 个不同端口RULE_PORT_SCAN用脚本批量 POST 这些数据然后用/api/alerts查询确认只有后两种有告警。这组测试的结果可以直接放进课程设计报告的安全测试章节比“完成系统”四个字强太多。7.5 最后一个小技巧用 TS 的satisfies提升规则扩展性如果你还在用DetectionRule类型约束规则对象遇到 TS 4.9 可以改成satisfies// src/engine/rules/dnsTunnelRule.ts import { DetectionRule } from ../detectionRule; export const dnsTunnelRule { id: RULE_DNS_TUNNEL, name: DNS tunnel detected, severity: critical, match(packet: PacketRecord): boolean { // 检查 DNS 请求的域名长度等特征 return false; }, } satisfies DetectionRule;satisfies会保留对象字面量的精确类型同时验证结构是否符合DetectionRule。这样dnsTunnelRule.id在代码提示中显示为字符串字面量RULE_DNS_TUNNEL而不是string在日志和调试字符串拼接时更安全。当前规则还是数组枚举如果继续增加规则可以把RuleRegistry改成扫描rules目录动态加载——但课程设计不必追求动态加载把规则显式注册反而更容易维护。至此整个基于 TypeScript 实现的网络安全检测系统服务端从工程初始化到规则编写、接口设计、存储与推送、并发状态处理、测试验证都有了可落地的路径。如果循着这个思路把代码写完再去对比标题中的“课程设计源码.zip”你会发现自己的实现已经超过了大多数模板代码的水平——因为模板往往只包含一个 Express 服务器和一个假检测函数而这里已经具备了能演示、可答辩、有工程思路的完整闭环。本文还有配套的精品资源点击获取