尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Zero-LLM与Sub-200us:MCP工具调用的确定性安全联锁机制

Zero-LLM与Sub-200us:MCP工具调用的确定性安全联锁机制 最近在做 LLM Agent 工具调用的时候我卡在了一个非常朴素的问题上模型已经拿到了足够的工具也确实在正确地调用但那个“错误地调用”的风险仍然没有消失。比如删除操作、转账操作、对外发送消息的操作模型在生成参数时可能思考得不够周全甚至被一段注入文本诱导。传统的防护手段要么是再让一个 LLM 做复核要么是加人工审批但这两者的延迟都太高没法嵌入到高频的工具调用路径里。如果你也在搭 MCP Server或者正在把一个 Agent 从 Demo 推向生产环境那么“如何让工具执行前的安全检查足够快、足够确定”这个问题你一定绕不开。这篇文章要解析的是标题为Show HN: Atomadic – Zero-LLM Sub-200us MCP Action Interlock的开源项目。它的核心判断非常明确**不要用另一个 LLM 去判断当前这个 LLM 的操作是否安全而是用一个确定性的、零 LLM 参与的、耗时控制在 200 微秒以内的 Action Interlock 机制在工具真正执行前做一次原子级拦截。**这不是放弃安全审查而是把安全审查从“概率性的事后评判”变成“确定性的执行前联锁”。读完后你会理解 MCP 工具调用场景里的安全边界应该画在哪里会知道如何把策略规则引擎接入 MCP Server 的执行链路也会拿到一套可运行的拦截器示例代码以及在生产环境里应该怎么设计默认拒绝策略、审计日志和回滚机制。1. 这篇文章真正要解决的问题先看一个真实场景。你正在做一个企业内部的数据助手接入了 MCP Server模型可以查询员工信息、调取项目文档甚至执行数据更新。表面上看MCP 只是把工具使用方式标准化了但一旦 Agent 可以调用工具问题就从“模型说什么”变成了“模型做了什么”。这个过程中最容易被低估的风险点有三个。第一是过度代理Excessive Agency。模型本来只该查文档但如果我们给它注册了一个“批量删除”的工具模型在用户的模糊指令下可能会真的执行删除。很多靶场里所谓的 exploiting LLM APIs with excessive agency 攻击针对的就是这种权限边界失控。第二是提示注入。恶意文本可能出现在网页、邮件、PDF 甚至数据库字段里。当模型阅读这些内容后如果它拥有工具的调用权就可能被诱导去调用某些敏感操作。这类攻击不是模型能力不够而是模型在工具调用场景下天然存在“越权执行”的可能。第三是延迟矛盾。如果让每个工具调用都经过一次人类审批或者经过另一个 LLM 做二次判断安全性确实提高了但延迟会从毫秒级上升到秒级这对于需要连续调用多个工具的任务来说是不可接受的。项目标题里有两个关键词值得重点关注Zero-LLM和Sub-200us。它的方案不是彻底放弃判断而是把判断从 LLM 的语义空间里拿出来放到一个确定的策略引擎里。这个引擎只做规则匹配、上下文检查和状态判断不产生 token不做概率推理所以可以在极短的时间内给出 allow 或 deny 的结果。这篇文章适合正在开发 MCP Server 的开发者、正在设计 Agent 工具接入规范的平台工程师以及所有担心“模型有了工具之后乱来”的 LLM 应用开发者。如果你只是把 MCP 当作一个 API 封装协议那么你看到的是“便利”如果你开始关心工具链路的安全边界那么你需要的是“联锁”。2. 基础概念MCP、Action Interlock、Zero-LLM2.1 MCP 是什么MCPModel Context Protocol是一种让 LLM 应用与外部工具、数据源进行标准化交互的协议。你可以把它理解成“工具调用的 USB 接口”以前每个 Agent 框架都要自己实现一套工具调用格式接入不同的服务需要写不同的适配器有了 MCP 之后MCP Server 负责暴露工具列表和接收调用请求MCP Client 负责在应用与服务器之间转发消息。在你开始接触 MCP 时至少会接触到这些角色角色作用关键操作MCP Server提供工具和服务能力注册工具、接收调用参数、返回执行结果MCP Client连接 LLM 与 Server维护会话上下文、调用远程工具、处理错误LLM Agent根据用户需求决定调用哪个工具生成结构化工具调用参数2.2 MCP 工具调用链路中的安全缺口MCP 在解决“如何调用工具”的问题时并没有解决“哪些工具可以被调用”“什么条件下可以调用”的问题。工具注册表只是告诉模型“你现在有哪些工具可用”而工具的授权、风险等级、调用频次、参数约束都需要使用者自己实现。缺一个前置动作。传统应用中API 网关可以在请求进入时做鉴权但在 MCP 链路里模型是直接发起的工具调用如果我们在模型和真正的执行函数之间不做一个拦截层那么调用请求就会直接打到业务代码上。2.3 什么是 Action InterlockInterlock 这个词来自工业控制领域指的是“联锁”只有当某个前置条件成立时后续的机械或电气动作才会被允许执行。比如高压设备维护时必须先断开断路器才能打开维护门这个“断路器状态”就是维护门的联锁条件。联锁的关键特征是确定性和不可绕过性它不靠操作员临场判断而是靠硬逻辑保证。MCP Action Interlock 就是把这个思路搬到了模型工具调用链路上。它不是问“这次操作是否合理”而是检查“这次操作是否符合当前策略规则”。如果不符合直接拒绝执行。它发生在模型生成工具调用参数之后、业务函数真正被调用之前。2.4 Zero-LLM 设计为什么一定要 Zero-LLM两条路线其实都有人尝试过。路线 A 是“LLM 审核 LLM”。让一个更强的模型监督当前模型的工具调用是否安全。缺点很明显延迟高、成本高、而且审核模型本身也可能被注入攻击。如果主模型看到了一段恶意文本并把恶意意图转化成了工具调用参数审核模型看到的是同一个参数它未必能发现异常。路线 B 是确定性策略检查。我们定义一个策略规则集规则基于工具名、参数、调用者身份、调用频率等结构化数据来做判断。这个过程是确定性的同样的输入永远得到同样的结果不会有概率波动也不会被 prompt injection 影响。Atomadic 选择的是路线 B。零 LLM 不只是为了性能更是为了让安全判断具备可解释性、可测试性和可审计性。你可以给每条规则写单元测试可以逐条审查规则内容也可以确定某一次拦截是由于哪一条规则生效。2.5 Sub-200us 是怎么来的200 微秒差不多是普通函数在本地做多次字符串匹配和条件判断的时间量级。如果这个拦截器只读内存中的策略缓存不访问远程服务不做磁盘 IO不做复杂的 JSON 序列化那么单次决策完全可以控制在 200 微秒以内。需要强调一下这里的 200 微秒是判断本身的耗时不包含工具执行的耗时也不包含 LLM 生成参数的耗时。3. 环境准备与前置条件下面进入实操部分。我们会从一个最简 MCP Server 开始在它的工具调用路径前接上 Action Interlock。注意下面的代码演示的是通用思路具体的 SDK 版本和依赖版本请以你的实际项目为准。建议环境Node.js 18 或更高版本支持原生fetch与process.hrtime.bigint()。TypeScript 环境方便定义策略类型和拦截器接口。安装了modelcontextprotocol/sdk的 MCP 项目。一个可以运行npm run dev或node命令的终端。如果你是从零开始创建示例项目可以做一次最简初始化mkdir atomadic-demo cd atomadic-demo npm init -y npm install modelcontextprotocol/sdk如果你的 MCP SDK 版本沿用了比较新的包名modelcontextprotocol/sdk那么核心的Server、CallToolRequestSchema等工具可以从包中导出。版本细节不一致不影响本文的代码示意图重要的是把拦截器放在哪个位置。4. 核心流程拆解4.1 调用链路全貌加了 Action Interlock 之后一次工具调用的完整链路应该是这样的LLM 根据用户需求和上下文生成一个工具调用请求比如send_message。MCP Client 将请求转发给 MCP Server。MCP Server 收到CallToolRequest此时我们不直接执行工具函数。拦截器读取调用上下文工具名、参数、调用者、请求 ID、当前时间。策略引擎对上下文执行规则匹配。如果规则全部通过放行到真正的工具执行函数。如果某条 deny 规则命中拦截器返回一个安全的错误提示不执行工具。无论放行还是拦截都把决策结果写入审计日志。这个链路最重要的变化在第三步**业务工具函数只能被拦截器调用不能被外部直接调用。**如果这个约束没有做到那么即使写了策略也没有用因为攻击者或注入场景可能绕过策略引擎直接调用函数。4.2 策略规则的分类在设计策略规则时建议至少覆盖这几类规则类型作用示例绝对拒绝规则直接禁止某类工具任何delete_*工具都不允许执行条件允许规则满足特定参数条件才能执行只允许在envstaging时执行写入类工具频控规则限制工具的调用频率一个请求周期内send_message最多调用 5 次上下文限定规则依赖实时状态判断服务处于“维护模式”时禁止写操作4.3 原子性的含义标题里的“Action Interlock”还有一层“原子性”的含义。对于一次工具调用判断和执行之间不能插入其他不可控操作。如果判断时规则满足但等真正执行时上下文已经变了那就不是原子联锁。所以在实现上我们倾向于把策略判断的执行路径做成同步逻辑避免在判断和调用之间引入异步 await 造成的状态漂移。这并不代表工具本身不可以异步执行而是说判断必须发生在发起工具调用之前而且判断结果要立即作为后续调用的前置条件。如果担心判断时读取的上下文不够新可以采用“定时刷新状态”的方式比如每 50 毫秒从外部配置中心同步一次策略版本而不是在每次判断时都去查询外部服务。5. 完整示例与代码实现接下来我们用一个最小实现把上面的思路落地。为了让代码更容易理解我分成三个文件策略规则引擎、拦截器、MCP Server 接入示例。5.1 策略规则引擎这个文件负责定义规则的数据结构并且提供一个纯函数来判断某个工具调用是否被允许。// 文件路径src/policy/types.ts export type RuleAction allow | deny; export interface RuleWhen { toolName?: string | RegExp; paramCheck?: (args: Recordstring, unknown) boolean; enabled?: () boolean; } export interface PolicyRule { id: string; action: RuleAction; when: RuleWhen; reason?: string; } export interface PolicyConfig { version: number; defaultAction: RuleAction; rules: PolicyRule[]; } export interface ActionContext { toolName: string; args: Recordstring, unknown; actor?: string; requestId?: string; timestamp: number; }// 文件路径src/policy/engine.ts import { ActionContext, PolicyConfig } from ./types; export interface InterlockDecision { allowed: boolean; reason: string; matchedRuleId?: string; policyVersion: number; } export class PolicyEngine { private compiledRules: ArrayPolicyRule { toolPattern?: RegExp } []; constructor(private config: PolicyConfig) { this.compileRules(); } private compileRules() { this.compiledRules this.config.rules.map((rule) { let toolPattern: RegExp | undefined; if (typeof rule.when.toolName string) { toolPattern new RegExp(^ rule.when.toolName.replace(/\*/g, .*) $); } else if (rule.when.toolName instanceof RegExp) { toolPattern rule.when.toolName; } return { ...rule, toolPattern }; }); } decide(ctx: ActionContext): InterlockDecision { for (const rule of this.compiledRules) { if (rule.toolPattern !rule.toolPattern.test(ctx.toolName)) { continue; } if (rule.when.paramCheck !rule.when.paramCheck(ctx.args)) { continue; } if (rule.when.enabled !rule.when.enabled()) { continue; } if (rule.action deny) { return { allowed: false, reason: rule.reason || rule ${rule.id} matched, matchedRuleId: rule.id, policyVersion: this.config.version, }; } if (rule.action allow) { return { allowed: true, reason: rule ${rule.id} allowed, matchedRuleId: rule.id, policyVersion: this.config.version, }; } } if (this.config.defaultAction deny) { return { allowed: false, reason: default-deny, policyVersion: this.config.version }; } return { allowed: true, reason: default-allow, policyVersion: this.config.version }; } }这里有一个值得注意的设计细节defaultAction默认为deny时策略引擎会拒绝所有未被显式 allow 的工具调用。在 LLM Agent 场景里工具权限应该遵循最小可用原则默认拒绝比默认允许要安全得多。5.2 拦截器拦截器是策略引擎和 MCP 工具执行逻辑之间的薄薄一层接口。它负责构造ActionContext调用引擎再根据决策结果决定是否继续执行。// 文件路径src/interlock/interceptor.ts import { ActionContext } from ../policy/types; import { PolicyEngine, InterlockDecision } from ../policy/engine; type ToolHandler () Promiseunknown; export class MCPActionInterceptor { private engine: PolicyEngine; constructor(engine: PolicyEngine) { this.engine engine; } buildContext(toolName: string, args: Recordstring, unknown, meta?: PartialActionContext): ActionContext { return { toolName, args, actor: meta?.actor, requestId: meta?.requestId, timestamp: meta?.timestamp ?? Date.now(), }; } decide(ctx: ActionContext): InterlockDecision { return this.engine.decide(ctx); } async guard(ctx: ActionContext, handler: ToolHandler) { const decision this.engine.decide(ctx); if (!decision.allowed) { return { error: true, message: interlock blocked: ${decision.reason}, matchedRuleId: decision.matchedRuleId, }; } try { const result await handler(); return { error: false, result }; } catch (err) { return { error: true, message: tool execution failed: ${(err as Error).message} }; } } }在这个实现里业务工具函数被包在handler中拦截器只有在decide返回 allow 时才会调用它。返回值统一封装成{ error, message, result }这样的结构方便 MCP Server 层直接转换为协议响应。5.3 接入 MCP Server接下来是接入 MCP Server 的部分。我们定义一个最简的 Server注册两个工具get_user和send_message然后通过CallToolRequestSchema把请求交给拦截器。// 文件路径src/server/index.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { CallToolRequestSchema, ListToolsRequestSchema } from modelcontextprotocol/sdk/types.js; import { PolicyEngine } from ../policy/engine; import { MCPActionInterceptor } from ../interlock/interceptor; import { PolicyConfig } from ../policy/types; const policyConfig: PolicyConfig { version: 1, defaultAction: deny, rules: [ { id: allow-get-user, action: allow, when: { toolName: get_user }, reason: read operation, }, { id: block-send-on-prod, action: deny, when: { toolName: send_message, paramCheck: (args) (args as { channel?: string }).channel production, }, reason: send_message to production is blocked, }, ], }; const engine new PolicyEngine(policyConfig); const interceptor new MCPActionInterceptor(engine); const server new Server( { name: atomadic-demo, version: 1.0.0 }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: get_user, description: get user by id, inputSchema: { type: object, properties: { id: { type: string } } } }, { name: send_message, description: send message to channel, inputSchema: { type: object, properties: { channel: { type: string }, content: { type: string } } } }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; const ctx interceptor.buildContext(name, args ?? {}, { actor: unknown-actor, requestId: req_${Date.now()}, }); const toolHandler: () Promiseunknown async () { if (name get_user) { const { id } args as { id: string }; return { id, name: user-${id}, role: member }; } if (name send_message) { const { channel, content } args as { channel: string; content: string }; return { sent: true, channel, contentLength: content.length }; } throw new Error(unknown tool: ${name}); }; return interceptor.guard(ctx, toolHandler); }); server.listen(process.stdin as never);这里有几个注意点第一ListToolsRequestSchema返回的工具列表其实是模型能看到的“全量能力”。但工具能被列出不代表工具能被任意调用。模型看到的工具列表是模型侧的信息而拦截器是执行侧的安全边界。两者必须分开看待。第二工具执行函数不能把整个CallToolRequestSchema的 handler 做空转然后把真正的逻辑散落到外面。每一步都必须经过interceptor.guard否则联锁就没有意义。第三这里我们把策略配置直接写在代码里只是为了演示。生产环境建议把策略放到配置中心或独立文件并支持动态刷新。5.4 一个比较完整的策略示例上面的规则其实已经算可运行。如果你想体验更多场景可以把策略扩展成文件形式例如使用 JSON 描述策略{ version: 2, defaultAction: deny, rules: [ { id: allow-read, action: allow, when: { toolName: get_* } }, { id: deny-delete, action: deny, when: { toolName: delete_* } }, { id: allow-send-staging, action: allow, when: { toolName: send_message, paramCheck: env_is_staging } } ] }需要说明的是如果在 JSON 里做paramCheck你需要把函数名映射到代码里已注册的校验器不能在 JSON 中直接写函数体。比如可以在代码里维护一个validators对象const validators { env_is_staging: (args: Recordstring, unknown) (args as { env?: string }).env staging, };然后在加载 JSON 规则时把paramCheck字符串替换成对应的函数引用。这样策略文件可以保持结构化又不会丢失函数能力。6. 运行结果与效果验证6.1 启动 MCP Server在不同的 MCP Client 里连接这个 Server 的方式不太一样但作为最小验证你可以使用 MCP Inspector 或直接通过一个测试脚本调用server.listen的输入输出流。为了方便演示下面用node运行编译后的文件npm run build node build/server/index.js如果项目没有配置构建命令也可以先安装tsxnpm install -D tsx npx tsx src/server/index.ts6.2 预期输出和行为当你通过 MCP Client 发起一次get_user调用时策略引擎会命中allow-get-user规则拦截器放行并返回用户信息。当你发起一次send_message并且参数里channelproduction时拦截器会命中block-send-on-prod规则阻止执行并返回类似下面的结果{ error: true, message: interlock blocked: send_message to production is blocked, matchedRuleId: block-send-on-prod }需要特别说明的是拦截器返回的错误信息要不要完整暴露给模型取决于产品设计。如果让 LLM 看到完整的拒绝原因它可能会尝试换个参数继续调用也可能把这些信息用于下一步对话。但如果你不希望对模型暴露太多策略细节返回一个通用的interlock blocked就好审计日志里再记录详细原因。6.3 延迟测量Sub-200us 是标题里最硬核的指标。你可以在本地加入一个计时器验证策略判断本身的耗时// 文件路径src/bench/bench.ts import { PolicyEngine } from ../policy/engine; import { MCPActionInterceptor } from ../interlock/interceptor; import { PolicyConfig } from ../policy/types; const config: PolicyConfig { version: 1, defaultAction: deny, rules: [ { id: allow-get, action: allow, when: { toolName: get_user } }, { id: deny-delete, action: deny, when: { toolName: delete_user } }, ], }; const engine new PolicyEngine(config); const interceptor new MCPActionInterceptor(engine); function bench(toolName: string, args: Recordstring, unknown, times 10000) { const ctx interceptor.buildContext(toolName, args, { actor: bench }); const start process.hrtime.bigint(); let lastDecision; for (let i 0; i times; i) { lastDecision interceptor.decide(ctx); } const end process.hrtime.bigint(); const totalUs Number((end - start) / 1000n); const avgUs totalUs / times; return { allowed: lastDecision.allowed, avgUs, totalUs, times }; } console.log(bench(get_user, { id: a })); console.log(bench(delete_user, { id: danger }));在本地机器上这种纯内存的正则匹配和条件判断单次决策通常在几微秒到几十微秒之间。200 微秒并不是一个紧张的预算但前提是不要在decide里做网络请求、磁盘读写、超长日志输出或复杂的深拷贝。如果测量结果异常偏高优先排查这几个点是不是在拦截器里打了大对象日志是不是每次判断都重新编译了正则是不是策略配置加载走了同步 IO这些细节才是影响延迟的真实因素。7. 常见问题与排查思路问题现象可能原因排查方式解决方案工具调用总是被默认拒绝拦截defaultAction设置为deny并且没有匹配到 allow 规则在调用前打印规则匹配过程检查规则顺序确认规则是否覆盖了工具名或补充对应的 allow 规则使用了RegExp规则但匹配不到工具名是动态的比如加了版本号或随机后缀用单元测试直接测试正则匹配上下文为工具名定义明确的命名规范调整正则模式paramCheck校验不到参数参数值类型和预期不一致比如数字传成了字符串对参数值做一次 JSON schema 校验在拦截器入口统一做参数类型规整延迟超过预期每次请求都读取外部配置或写数据库日志使用process.hrtime.bigint()分阶段计时找出耗时点策略配置做本地缓存审计日志改为异步批量写入拦截器被绕过业务代码在别处直接导出了工具执行函数没有经过拦截器代码审查检查工具函数的引用路径把工具执行函数私有化只暴露经过guard的调用入口模型反复尝试被拒绝的操作LLM 看到了完整的拒绝原因尝试换参数绕过查看 MCP Server 返回给模型的错误信息对模型返回统一模糊错误详细原因只写入审计日志排查顺序建议是先看最外层的决策结果再逐步向内看规则匹配过程最后看执行链路是否有旁路。一个比较实用的办法是给每条规则加一个traceId在策略决策前后打印带有traceId的最小日志这样你可以把一次具体的工具调用和一条策略决策精确对应起来。8. 最佳实践与工程建议8.1 默认拒绝显式放行如果你还在犹豫默认策略是 allow 还是 deny我建议选择默认拒绝。LLM Agent 的工具调用通常会注册很多工具但真正高频使用的可能只有少数几个。默认拒绝会把新增工具的安全成本变成“必须主动声明”而不是“忘记限制了”。这个取舍在事故发生时非常关键。8.2 把策略配置当成代码策略规则应该纳入版本管理和业务代码一起走评审、测试、发布流程。每次调整工具权限都要让团队里其他人能看到变更内容和变更原因。不要直接在线上服务器手工修改策略文件。推荐的做法是将策略文件放到独立的目录例如config/policy.json并且为每次变更更新version。发布新版本时先在小范围流量上验证确认没有误拦截再全量生效。8.3 审计日志与最小数据原则拦截器放行和拦截都值得记录但是记录内容要注意最小化。不要记录完整的工具入参尤其是包含手机号、身份证号、密钥、Token 之类的敏感信息。建议记录请求 ID调用者标识工具名不记录完整参数决策结果命中的规则 ID时间戳如果业务上确实需要存参用于事后排查应该把参数加密存储并且设置访问权限和保留期限。8.4 拦截器本身不能修改策略这里要特别强调一个安全边界拦截器只负责执行策略不能拥有修改策略的权限。如果某个工具已经可以被 LLM 调用而拦截器又暴露了一个“更新规则”的工具那 LLM 就可能通过这个后门把自己放行。更稳妥的设计是策略更新通道只开放给受信任的管理员接口不进入 MCP 的工具列表。8.5 关注“策略漂移”问题随着业务发展工具列表会变多工具的入参也会改版。如果策略规则长时间不更新可能出现两类问题一是新工具没有规则覆盖被默认拒绝误伤二是旧规则引用了已经删除的工具形成无效规则。建议定期做一次规则清理和分析最好在 CI 里加入一个规则检查项检测那些永远无法匹配的规则。8.6 避免过度拦截Action Interlock 的主要目的是防风险但不能把业务正常路径也堵死。策略上线前要拿真实业务样本跑一遍“影子模式”。影子模式下拦截器只记录决策结果不真正阻断工具执行。观察几天看 allow 和 deny 的比例是否合理确认没有大规模误拦截后再切换成强制模式。9. 总结与后续学习方向这篇深度解析围绕Atomadic: Zero-LLM Sub-200us MCP Action Interlock这个项目标题展开核心其实是一句话**在 LLM 应用里工具执行前的安全判断应该交给确定性联锁而不是交付给另一个概率模型。**MCP 把工具调用标准化了但安全边界需要你自己画。Action Interlock 就是这条边界上的那道闸门。我们讨论了 MCP 工具链路里过度代理和提示注入的风险解释了为什么 Zero-LLM 的判断方式更可控、更适合高频调用拆解了一次工具调用从发起到执行应该经过的策略判断链路并通过一个最小的 TypeScript 拦截器示例演示了如何把规则引擎接入 MCP Server。延迟方面只要策略判断保持在纯内存、无 IO 的路径里单次决策控制在 200 微秒以内是现实的但具体数据需要依赖你的策略复杂度和机器性能不建议直接搬运任何基准数字。下一步如果你要动手实践建议按这样的顺序来先搭建一个最简 MCP Server再接入一个只带三条规则的拦截器跑通“默认拒绝”和“白名单放行”两种场景。然后逐步加入参数校验、频控和审计日志。等到策略稳定后再考虑把策略配置外置化并补上影子模式验证。如果你更关心 Agent 整体安全架构可以继续研究 MCP 协议的鉴权扩展、工具调用参数 Schema 约束、以及 LLM 提示注入防护。这些方向与 Action Interlock 相互补充组合起来才能形成更完整的纵深防御。建议把文章中这个最小拦截器保存下来作为后续安全改造的基础模板。
返回列表