news 2026/9/14 4:04:50

基于TypeScript的网络安全检测系统服务端实战:从类型安全到实时告警

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于TypeScript的网络安全检测系统服务端实战:从类型安全到实时告警

简介:这是一份完整的信息安全课程设计源码,采用类型脚本语言构建网络安全检测系统的服务端部分。面向计算机、人工智能、通信工程等专业学生,适合作为课程作业、毕业设计或项目初期演示。压缩包内共有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赋值为字符串'tcp',TS 会直接报错,而不是等到检查规则跑挂了才知道。对课程设计来说,这种“编译期挡错误”的特性,天然比无类型脚本更适合承载一个多模块协作的系统。

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 Record<string, 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 配合typescript@5.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很重要,它开启了strictNullChecksnoImplicitAny等一系列严格检查。对于刚写 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 包数量是否超过阈值。这里需要一个滑动窗口计数器,可以用一个Map<string, 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: Map<string, 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: Map<string, Set<number>>; } 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 Set<number>(); 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 }); } }); // 最近告警查询,支持 ?limit=50 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); } } }); }

这里的关键点:WebSocketServerserver参数传的是之前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 的原因:告警写入是低频操作,同步写不会阻塞事件循环,代码更直观。如果未来要支撑高并发检测,再改异步sqlite3node: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: Omit<AlertRow, '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,同时推送给所有前端。模块之间只依赖AlertBussubscribe方法,互不感知具体实现,未来要加邮件通知、飞书机器人,只需要再增加一个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 和端口扫描规则中,状态容器使用MapSet。Node.js 是单线程事件循环,理论上不会出现真正的多线程竞争;但要注意异步操作中如果不小心await,状态更新可能产生交错。更重要的是,多个请求同时到达时,这些状态操作都是同步的,所以Map操作不会被中断,这一点是安全的。

但不要因此掉以轻心。如果将来使用 worker threads 或者将状态存储在可变全局中,就会出现竞争。课程设计中一个常见的错误是在DetectionRule闭包里直接let count = 0,这样多实例或热重载时状态会混乱。推荐的模式是“状态容器显式注入”,代码里已经体现了。额外注意:portScanRuleSet没有清理机制,运行时间长了可能内存泄漏。可以增加一个定时器,周期性清理旧数据;但由于规则对象没有生命周期钩子,最好把清理放在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-devtsx监听),然后另一个终端跑脚本。注意 axios 需要先安装。这组模拟方法比单纯 curl 更有说服力,可以在演示时展示实时 WebSocket 收到告警的截图。

6.4 性能边界:当前实现的极限在哪

尽管课程设计不需要高并发,但你应该知道自己系统的瓶颈。当前实现中,最大的开销是RuleRegistry.evaluate对每条报文遍历所有规则,如果规则数量增长到 1000 条,每次检测也还是同步的。更关键的是,parsePacketJSON.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?limit=5"

返回的 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 = 50MAX_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_COUNT=5后马上触发告警,比死改成代码再重启更有说服力。

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 服务器和一个假检测函数,而这里已经具备了能演示、可答辩、有工程思路的完整闭环。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 4:04:32

pwn入门必读:栈溢出原理与内存布局实战详解

先给各位对二进制安全感兴趣的朋友交个底&#xff1a;这篇文章我要讲的是pwn方向绕不开的第一座山——”栈“和”内存“。不管你是刚看完几篇pwn入门文章、正准备在buuctf上刷第一道题&#xff0c;还是已经能跑通ret2text但一遇到ret2libc就发懵&#xff0c;这篇文章都适合你。…

作者头像 李华
网站建设 2026/9/14 4:02:55

论文转PPT神器实测:大模型+PDF解析三分钟生成学术汇报初稿

GitHub热榜上每天都有新项目&#xff0c;但能让我一眼看到就想立刻动手试的&#xff0c;最近只有这个论文转PPT工具。三分钟&#xff0c;从一篇PDF论文到一份能拿去汇报的pptx文件&#xff0c;标题、大纲、正文要点、图表占位全给你铺好。我一开始以为是标题党&#xff0c;直到…

作者头像 李华
网站建设 2026/9/14 4:02:35

基于Hyperledger Fabric的区块链供应链管理系统设计与实现

简介&#xff1a;基于区块链的供应链管理系统毕业设计项目&#xff0c;源码与文档齐备&#xff0c;评审得分九十八分&#xff0c;难度适中&#xff0c;适合高校学生用于毕业设计、课程设计或期末大作业等场景&#xff0c;也适合作为区块链应用开发的入门参考。压缩包共六十一个…

作者头像 李华
网站建设 2026/9/14 4:02:33

合成大西瓜微信小游戏源码解析:工程结构、合成逻辑与广告变现

简介&#xff1a;微信小程序“合成大西瓜”的云开发版源码&#xff0c;包含完整小游戏工程与流量主接入逻辑。游戏将经典合成玩法与俄罗斯方块式下落、消消乐元素结合&#xff0c;用户只需拖动同样的水果让其碰撞合成&#xff0c;逐步挑战最终的大西瓜&#xff0c;操作简单、反…

作者头像 李华