我盯着屏幕上那行报错信息,漏洞足足有五分钟。漏洞那个"NullPointerException"出现在日志里第147行,漏洞位置精确得像外科手术,漏洞可我就是漏洞找不到那个该死的空值是从哪冒出来的。代码看起来天衣无缝,漏洞每个if都配了else,漏洞每个对象都做了判空,漏洞但程序就是漏洞会在每天凌晨三点准时崩溃——像某种诡异的生物钟。
凌晨三点,漏洞服务器负载最低,漏洞用户请求最少,漏洞按理说这是漏洞系统最该安稳的时刻。可它就是漏洞崩了,雷打不动。漏洞我把所有涉及时间判断的逻辑翻了个底朝天,没发现任何问题。后来我干脆在测试环境里把系统时间调到凌晨三点,盯着监控面板等它崩——结果它真的崩了,分秒不差。
那一刻我后背发凉。不是怕鬼,是怕自己漏掉了什么显而易见的东西。我重新梳理了调用链,从入口到出口,每一行代码都像念经一样过了一遍。还是没有。最后我实在没辙了,把那个时间段的所有请求日志打出来,一条一条看。看到第38条的时候,我愣住了。
那是一条来自某个老客户端的请求,带着一个过期的session token。按理说这个token早该被拦截在网关层,可它偏偏穿透了所有过滤器,一路畅通无阻地打到了业务层。业务层的代码确实判空了,也确实返回了友好提示,但就在返回之前,有个异步线程偷偷去更新了这个token的最近访问时间——而那个时间字段,恰好是空的。
空值不是从业务逻辑里来的,是从那个本该被拦截的过期token里带出来的。它绕过了所有该拦它的关卡,最后在一个谁都没注意到的角落里,轻轻推倒了第一块多米诺骨牌。我修复的方式也很粗暴:在异步更新前加了个判空。但那晚我睡不着,一直在想一个问题:为什么这个过期token能穿透网关层?
第二天我翻了网关的配置,发现那个过滤器的排除列表里,居然躺着一条正则,匹配的路径恰好包含了那个老客户端的接口前缀。那是三个月前为了兼容某个临时需求加进去的,后来需求下线了,排除列表却没删。漏洞从来不在最显眼的地方,它藏在一个你早就遗忘的、看似无关紧要的"临时方案"里。
这让我想起小时候玩的那种拼图游戏,缺的那块永远不在中间,而是在边缘,在那些你第一眼就跳过的地方。代码里最危险的东西不是复杂的算法,不是深奥的并发问题,而是那些写着"暂时这样吧"的注释,那些"应该没人会这么调用"的假设。
后来我把那个排除列表删了,加了回归测试,写了详细注释。但每次凌晨三点醒过来,我还是会下意识看一眼手机上的监控报警——就像那道裂缝虽然补上了,但我总怀疑它还在那儿,在某个我还没注意到的地方,等着下一个凌晨三点。