去年冬天,我负责的一个电商优惠券接口上线。凌晨两点,运营突然打电话:后台出现大量异常领券记录,有人用脚本在十分钟内领走了价值几十万的优惠券。我们紧急下线接口,排查后发现,代码逻辑本身没错,但三个安全细节全被忽略了。今天把这段经历写出来,希望你别再踩同样的坑。
第一个细节:参数校验只做了前端,后端完全信任客户端
当时的领券接口是这样写的:
@PostMapping("/coupon/receive") public Result receive(@RequestBody CouponRequest request) { Long userId = request.getUserId(); Long couponId = request.getCouponId(); couponService.receive(userId, couponId); return Result.success(); }userId直接来自请求体,后端没有校验这个 ID 是否属于当前登录用户。攻击者只要抓包,把userId改成任意值,就能替别人领券。更致命的是,couponId也没有校验是否存在、是否在活动期内。攻击者遍历couponId,把所有能领的券全领了一遍。
正确做法:用户身份必须从服务端会话或 Token 中解析,绝不能信任客户端传入的userId。所有入参都要做合法性校验,包括非空、格式、范围、业务状态。用@Valid加自定义注解,或者在 Service 层显式校验。记住一句话:前端校验是体验,后端校验是安全。
第二个细节:接口没有限流,一个脚本就能打垮服务
攻击者领券时,用的是多线程脚本,QPS 瞬间冲到几千。我们的接口没有任何限流措施,数据库连接池直接被打满,正常用户也无法访问。更糟的是,领券逻辑里有“先查库存再扣减”的操作,高并发下出现了超卖。
正确做法:所有写接口必须限流。可以用 Guava RateLimiter 做单机限流,用 Redis + Lua 做分布式限流。针对用户维度,限制每个用户每秒最多请求几次;针对接口维度,限制总 QPS。另外,领券这类操作要加分布式锁,或者用 Redis 的原子操作扣减库存,避免并发问题。限流不是可选项,是线上接口的标配。
第三个细节:日志打印了敏感信息,攻击者直接拿来复用
排查时我们发现,攻击者不仅领券,还尝试调用退款接口。他们怎么拿到退款所需的参数?翻日志发现的:我们的接口在 INFO 级别打印了完整的请求体和响应体,包括用户的 Token、手机号、订单号。攻击者通过某种方式拿到了日志文件,直接复用这些信息。
正确做法:日志中禁止打印密码、Token、身份证、银行卡、手机号等敏感字段。如果必须记录,一定要脱敏。比如手机号只留前三位后四位,Token 只留前六位。另外,日志文件权限要控制,不要放在 Web 可访问的目录下。很多团队用 ELK 收集日志,也要确保传输和存储加密。
事后复盘,我们还补了这些
第一,所有接口增加签名机制,防止参数被篡改。第二,关键业务操作增加风控规则,比如同一 IP 短时间大量请求直接封禁。第三,上线前做安全测试,用 Burp Suite 或 Postman 模拟越权、重放、遍历攻击。第四,建立告警机制,接口 QPS 突增、错误率上升时第一时间通知。
接口安全不是“等保”才需要关心的事,它就在每一行代码里。那三个细节——不信任客户端、必须限流、日志脱敏——听起来简单,但多少人写接口时随手就忘了。上线当天被攻击,损失的不只是钱,还有用户信任。下次写接口时,花五分钟检查这三项,也许就能避免一场灾难。