上周朋友老张的公司上线了个新内部审批系统,刚跑两天就出事了——用户上传PDF时偶尔卡死,后台日志里反复出现 NullPointerException,但没人知道具体哪步出的问题。运维重启服务顶了一阵,结果第二天数据库连接池直接耗尽,整个审批流程瘫痪了两小时。
错误不是敌人,藏起来才是
很多开发者觉得“只要页面不崩、用户没报错,就算稳了”。可现实是:一次未捕获的空指针可能悄悄把日志写满磁盘;一个没校验的JSON解析异常,可能让攻击者构造恶意payload绕过权限检查;甚至一段没加超时的HTTP调用,就能把线程池拖成死水。
真正管用的安全错误处理,得做三件事
第一,分层拦截,不甩锅
前端做基础校验(比如邮箱格式、文件后缀),但绝不只信前端;API网关层统一熔断和限流;业务层捕获具体异常(如 InvalidTokenException),而不是全丢给 Exception 一把抓;最后兜底的日志框架要带上下文(用户ID、请求路径、traceID)。
第二,错误信息不裸奔
用户看到的永远是“操作失败,请稍后重试”,而不是“ORA-01403: no data found”或者“/etc/passwd not found”。后端日志里才保留完整堆栈,且敏感字段(密码、密钥、身份证号)必须脱敏:
logger.error("支付失败,用户:{}, 订单ID:{}, 原因:{}", userId, orderId, e.getMessage());第三,让错误自己说话
比如登录失败,别只记“用户名或密码错误”,而是记录“用户A在IP 192.168.3.57连续5次输错密码,最后一次尝试时间:2024-06-12 14:22:03”。这类结构化日志,配合ELK或Loki,能快速识别暴力破解行为。
一个小而实用的Java示例
下面这段代码,把数据库查询异常转为带业务语义的安全响应:
try {
User user = userDao.findById(userId);
if (user == null) {
throw new BusinessException("USER_NOT_FOUND", "用户不存在");
}
return user;
} catch (DataAccessException e) {
log.error("DB query failed for userId:{}", userId, e);
throw new SystemException("SYSTEM_ERROR", "服务暂时不可用");
}注意:两个自定义异常都继承了 RuntimeException,但分别对应不同HTTP状态码(404 和 503),前端也能据此做差异化提示,而不是全显示“网络错误”。
说到底,安全相关的错误处理,不是给代码加保险丝,而是给整个系统装上“故障雷达”——它不阻止问题发生,但确保问题一露头,就被看见、被定位、被隔离。