拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot3整合行为验证码,10分钟搞定登录防刷与暴力破解

SpringBoot3整合行为验证码,10分钟搞定登录防刷与暴力破解

做后端这行,谁没被“撞库”和“暴力破解”恶心过?我手上一个SpringBoot3项目,登录接口上线不到一周,安全日志里就全是某个IP段发起的密码尝试请求,数据库里莫名多了一堆无意义的“待激活”账号。老办法上了字符验证码,结果用户嫌难认,OCR脚本分分钟绕过,白搭。后来把方案换成了行为验证码,配合SpringBoot3把登录入口重新武装了一遍,效果立竿见影。这篇文章就把这套“10分钟落地”的接入过程、防刷组合策略和几个真正会卡住你的坑全部摊开讲,适合正在用SpringBoot3做Web项目、又不想被脚本刷爆登录接口的Java后端同学参考。

1. 为什么业务系统需要单独接一道“行为验证码”

1.1 从一道被OCR干翻的老式验证码说起

传统图形验证码的逻辑很简单:服务端生成一张扭曲的字符图片,用户输入图片里的内容,对了就放行。放在五年前还能拦住一批脚本,现在基本形同虚设。主流的OCR开源引擎配合深度学习模型,识别准确率已经高得吓人,尤其是那种没有干扰线、背景干净的纯字符图,识别速度比人眼还快。至于算术题、文字点选这类的老方案,也早就有对应的打码平台和打码工场在批量处理,成本低到可以忽略。

更要命的是体验问题。我见过不少用户卡在“看半天认不出验证码”这一步,输错三次直接关页面走了。移动端小屏幕上,扭曲字符的误触率更是高得离谱。也就是说,图形验证码既没挡住攻击者,又把正常用户挡在了门外,两头不讨好。

行为验证码解决的正是这个矛盾。它不再让用户“认图”,而是让用户“做动作”——拖动滑块拼图、按顺序点选文字,甚至某些无感模式下用户根本感知不到验证的存在。机器脚本模拟人类的轨迹和点击行为很难做到完全一致,这个差异就是行为验证码的立身之本。从用户侧看,滑动一下鼠标就能通过,比输四位扭曲字符体感舒服太多;从服务端看,拦截脚本刷接口的效果比静态图片码高一个量级。

1.2 行为验证码到底防的是什么

先说清楚一个概念:行为验证码不是万能的,它防的是“自动化脚本攻击”,不是“人工恶意操作”。具体到业务场景,主要有几类:

  • 撞库与暴力破解:攻击者拿泄露的账号密码批量尝试登录,或者对某个账号做高频密码猜测。行为验证码要求每次登录前先完成滑动/点选,脚本没法低成本地批量处理这一步,攻击成本立刻翻倍。
  • 短信与邮件轰炸:注册、找回密码这类会触发短信验证码的接口,经常被脚本用来批量触发,导致短信费用爆炸。在这些接口前加一道行为验证,能挡住绝大多数批量调用。
  • 薅羊毛与批量注册:活动抽奖、新用户立减金这类福利接口,脚本会注册大量小号来套取权益。行为验证能显著提高批量注册的“人力成本”,让自动化方案变得不划算。

在SpringBoot3项目的整体防刷架构里,行为验证码的位置非常靠前。它不是最终防线,而是第一道闸门,负责把自动化流量和真人流量区分开。闸门后面还有限流、账号锁定、风控规则等一系列措施。很多团队只接了一个验证码就以为万事大吉,其实后续的配套策略更重要,这个我在第6部分详细展开。

2. 主流选型:自研、第三方SaaS还是开源自部署

2.1 三类方案的横向对比

接入之前先聊选型,这一步很多人忽视,直接决定后期投入的精力。

自研行为验证码是最不建议的路线。原因很简单:行为验证的本质是“让机器难以伪造人类行为”,这需要海量样本数据来训练和优化识别模型,还需要对抗黑产持续升级的攻击手段。一个业务团队做出来的轨迹识别模型,面对专业打码平台基本是裸奔状态。更别提前端还需要适配不同端的轨迹采集、指纹采集,工作量极大,效果还未必达标。

第三方SaaS是大部分企业的选择。极验、腾讯防水墙、阿里云验证码这些厂商都提供了完整的前后端SDK,接入速度快,识别模型有厂商持续维护,效果有保障。缺点是商业化产品按调用量计费,量大的时候成本不低;同时验证服务部署在厂商侧,数据和流量会经过第三方,对数据合规要求严格的团队需要多评估一层。

开源自部署是我个人比较偏好的中间路线。以AJ-Captcha为代表,它提供了完整的服务端和前端实现,可以部署在自己服务器上,数据不出内网,没有按量计费的问题,还可以按业务需求改源码。效果上,虽然识别模型不如商业SaaS那么“聪明”,但对绝大多数中小团队的防刷需求完全够用。

我做了一张选型对比表,你可以根据自己的实际约束来判断:

方案初次接入成本长期费用识别能力数据合规维护压力
自研极高极低弱完全自主重
第三方SaaS低按量计费强数据过第三方轻
开源自部署中零中完全自主中

2.2 为什么我选AJ-Captcha

我对AJ-Captcha的评估结论是:性价比和可控性的平衡点最好。它在Gitee/GitHub上开源,社区活跃,支持两种验证模式——滑动拼图(blockPuzzle)和文字点选(clickWord),并且内置了缓存接口抽象,默认走本地内存缓存,也可以接Redis。这对已经使用Redis做会话管理的SpringBoot3项目来说非常顺滑。

接口设计也很简洁,核心就两个:获取验证码的/captcha/get,以及校验结果的/captcha/check。前端有官方提供的vue组件,同时支持H5,移动端适配不费劲。更关键的一点是,AJ-Captcha的授权方式对商用友好,公司内部使用不用太担心授权风险。综合下来,如果你需要一套能自己掌控的验证码服务,它是最值得先花一小时试跑的方案。

3. SpringBoot3环境里最容易被忽略的三个前置问题

3.1 JDK17与javax/jakarta命名空间切换

SpringBoot3相比SpringBoot2,最大的隐性变化是底层从javax.*迁移到了jakarta.*命名空间,同时强制要求JDK17及以上。这两个变化对使用老版本第三方库的项目来说非常致命,因为很多在SpringBoot2时代正常工作的starter组件,在Boot3环境下会直接抛出ClassNotFoundException,或者因为包名不一致出现各种诡异的运行时报错。

行为验证码的接入同样面临这个问题。网上能搜到的大部分AJ-Captcha接入教程都是基于SpringBoot2写的,它们引入的captcha-spring-boot-starter依赖是为Boot2构建的,直接搬进Boot3项目大概率翻车。我当时的处理方式很直接:不用starter,直接把源码里核心的CaptchaService和两个Controller接口逻辑拿出来维护,这样命名空间完全由自己控制,反而更干净。

这里给个实操建议:不管是AJ-Captcha还是其他扩展组件,在SpringBoot3项目里引入前先确认它的pom.xml依赖和编译目标。如果发现它的依赖树里还是javax.servlet,基本可以断定没有适配Boot3,要么手动迁移,要么换方案。

3.2 依赖冲突:log4j2、logback与验证码框架的日志摩擦

SpringBoot3项目里日志框架的选择本身就有讲究。默认SpringBoot使用的是Logback,但有些团队为了性能会切换成Log4j2。行为验证码框架如果内部用SLF4J写日志,理论上和你选的日志实现是解耦的,问题出在版本冲突上。

我踩过的具体情况是:项目里为Log4j2配置了log4j2-spring.xml,但引入验证码依赖时,传递依赖里带了一个老版本的logback-classic,导致SLF4J绑定冲突,运行时报出“SLF4J: Class path contains multiple SLF4J bindings”之类的提示,某个日志配置直接失效。排查方法也简单,用mvn dependency:tree看传递依赖,把多余的logback依赖排除掉,或者统一用spring-boot-starter-log4j2替换默认日志starter。

至于另一种场景——如果你用的是Logback,则要处理logback-spring.xml里验证码请求的日志噪音问题。验证码接口QPS很高,默认DEBUG级别的日志会把大量验证请求刷进日志文件,一晚上能涨几个GB。我后面专门在logback-spring.xml里加了过滤器,把/captcha/get和/captcha/check这个路径的访问日志单独降级或直接剔除,详细配置见第7部分。

3.3 Redis序列化器对验证码缓存的影响

行为验证码的数据必然要解决服务端存储问题。AJ-Captcha支持通过CaptchaCacheService接口自定义缓存实现。如果你用Redis存储验证码信息,序列化器的选择会直接影响校验逻辑。

默认的RedisTemplate<Object, Object>使用的是JDK序列化,Redis里存的key是\xAC\xED\x00\x05t\x00开头的一串乱码,问题不大,但key中包含的captcha:前缀会被序列化掉,导致排查Redis数据时看着很别扭。更推荐的做法是单独配置一个StringRedisTemplate实例来存取验证码信息,key和value都用String类型,后续调试、设置TTL、查看过期情况都一目了然。

实际设置TTL时要注意,验证码的过期时间不能太短,我见过有人设成30秒,用户刚拖动完滑块,后端校验时就提示“验证码已过期”。比较合适的区间是60秒到120秒,既能防止验证码被长时间复用,又不至于误伤正常用户。这个值在AJ-Captcha里可以通过配置属性调整,我用的配置是120秒。

4. 后端接入核心链路:初始化、二次校验、结果透传

4.1 初始化:后端把验证码参数发出去

整个行为验证码的流程,从用户的视角看是“打开页面,拖动滑块,进入登录”,但从代码链路看,实际上是后端先发包,前端再交互,最后后端再校验收尾。

第一步是后端提供一个获取验证码的接口。AJ-Captcha的/captcha/get接口,入参通常是captchaType(指定blockPuzzle还是clickWord)、clientUid(用户标识)、ts(时间戳)。后端处理时,会先生成一张背景图和一块拼图,记录拼图的正确位置坐标,生成一个唯一标识token,然后把图片和token返回给前端。

我在项目里没有直接用AJ-Captcha自带的Controller,而是自己封装了一层,原因有两个:一是自带的Controller返回结构比较固定,我想统一包一层业务响应体;二是需要在返回前把token做一次签名处理,防止被篡改。这里贴一段核心封装逻辑:

@RestController @RequestMapping("/captcha") public class CaptchaController { @Resource private CaptchaService captchaService; @GetMapping("/get") public Result<CaptchaVO> getCaptcha(@RequestParam String captchaType) { CaptchaVO captchaVO = new CaptchaVO(); captchaVO.setCaptchaType(captchaType); // 这两个参数用于后续校验时需要回传,客户端会原样带回 captchaVO.setClientUid(IdUtil.fastSimpleUUID()); captchaVO.setTs(System.currentTimeMillis()); // 底层会根据 captchaType 生成对应的验证码数据 CaptchaVO result = captchaService.get(captchaVO); // 业务层在此对 result.getToken() 做额外处理,例如绑定客户端IP return Result.ok(result); } }

这里有个容易被忽略的细节:clientUid和ts不是随便带的,它们会在后续校验接口里被回传,服务端可以用ts校验请求时效,用clientUid做用户级数据隔离。如果你在网关层按IP做了限流,还可以在获取验证码时就把IP和token绑定到Redis里,后续校验时比对IP是否一致,能进一步降低验证码被盗用的风险。

4.2 二次校验:滑动结束后的那个请求

用户把滑块拖到目标位置后,前端会把本次操作产生的行为轨迹数据(包括拖动坐标序列、耗时、偏移量)和token一起发回后端的/captcha/check接口,这就是第二次校验。AJ-Captcha在这里做的核心事情有两件:一是检查token是否存在且未过期,二是判断用户提交的拼图位置是否在允许的误差范围内。

/captcha/check接口的入参是captchaType、pointJson、token。pointJson里包含用户最终停留的坐标点,服务端会和生成验证码时记录的原始坐标做对比,横纵坐标的绝对偏差同时小于某个阈值才算通过。校验通过后,服务端会返回一个新的validate凭证,这个凭证才是后续业务接口要真正使用的“通行证”。

我自己封装校验接口时,在AJ-Captcha之上还加了一个步骤:校验通过后,把validate凭证写入Redis,设置一个较短的有效期(比如90秒),同时绑定token保证一次一用。业务接口拿这个validate过来时,我先查Redis是否存在,存在才放行并立即删除该凭证。这个“一次性票据”的设计在后面第6部分讲防刷策略时还会提到,它能解决验证码结果被重放的问题。

4.3 业务接口中如何拿到“验证通过”的凭证

做到这一步,很多人的疑问是:/captcha/check校验通过之后,登录接口还要不要再校验一次?答案是必须。因为验证码和登录是两步操作,攻击者可以手动完成一次验证码,拿到合法的校验结果,然后把这个结果抓到脚本里重放,绕过验证码直接打登录接口。

所以正确的时序是:请求先到验证码校验,通过后拿到一次性validate凭证;请求再带着用户名、密码、validate一起打到登录接口;登录接口第一件事就是检查validate的合法性。这里有两个实现方案,我对比过之后推荐方案B:

  • 方案A:登录接口内部调用captchaService.check再做一次验证码校验。坏处是校验逻辑重复执行,验证码服务耦合在登录逻辑里,职责混乱。
  • 方案B:验证码校验独立成前置接口,通过后发放一次性validate票据,登录接口只验票据。这样职责清晰,验证码服务和登录业务完全解耦,也方便后续和网关限流做组合。

我在项目里用的方案B,登录接口的入口处做了一个注解拦截,凡是标注了@CaptchaRequired的接口,都会先校验请求头里的validate字段:

@Component public class CaptchaValidateInterceptor implements HandlerInterceptor { @Resource private StringRedisTemplate stringRedisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 前后端约定:验证码校验通过后,validate凭证放在 header 里 String validate = request.getHeader("X-Captcha-Validate"); if (StrUtil.isBlank(validate)) { throw new BizException("缺少验证码凭证"); } // 从 Redis 查凭证,存在则立即删除,保证一次性 Boolean removed = stringRedisTemplate.delete("captcha:validate:" + validate); if (!Boolean.TRUE.equals(removed)) { throw new BizException("验证码凭证无效或已过期"); } return true; } }

这套拦截器的好处是:登录、注册、找回密码、发送短信验证码这几个高风险接口,全部一键加上验证码校验,不用每个接口都写重复逻辑。

5. 前端触发与校验流程:滑动拼图、点选文字、二次时序

5.1 前端两种模式的实际体验差异

AJ-Captcha的前端组件支持两种模式,对应后端的captchaType参数。实际使用中,这两种模式的体验和适用场景差别不小。

blockPuzzle就是大家最常见的滑动拼图:一张背景图被裁剪出一块拼图形状,用户按住滑块拖到正确缺口位置即可。它最大的优势是用户认知成本为零——主流App和网页都在用这个形式,几乎不用引导。缺点是背景图能否有效区分“真人拖动”和“脚本拖动”,完全取决于滑块与缺口的贴合度以及轨迹特征采样质量。

clickWord则是给出几个汉字,用户按顺序点击句子中对应的字。这种模式在PC端上体验还行,但在移动端小屏上,文字偏小、点击精准度不高,误触率明显上升。它更适合作风控升级后的二次验证,或者对安全性要求更高的场景,比如找回密码、修改手机号这类敏感操作。

我的建议是不要只配一种模式。登录页面用blockPuzzle,保持低摩擦;触发敏感操作(比如更换绑定手机号、提现)时切换到clickWord,增加一步验证成本。AJ-Captcha的前端组件支持动态切换captchaType,前后端约定好即可。

5.2 前端引入与参数回传

前端接入在官方文档里写得比较清楚,我在这里补充几个容易忽略的实操点。使用官方Vue组件时,通常是这样初始化的:

import { Captcha } from 'aj-captcha' const captcha = new Captcha({ element: '#captcha-box', captchaType: 'blockPuzzle', mode: 'pop', // float: 浮动, pop: 弹出, fixed: 固定 // 获取验证码接口由后端二次封装后的路径 captchaId: '', verifyBarColor: '#3370ff', apiCodeRequest: (data) => { // 向后端 /captcha/get 发起请求 return request.post('/captcha/get', data) }, apiCodeCheck: (data) => { // 向后端 /captcha/check 发起请求 return request.post('/captcha/check', data) }, success: (res) => { // 校验成功后拿到 validate 凭证 const { validate } = res // 把 validate 存入 sessionStorage,后续登录请求时带上 }, fail: (err) => { // 校验失败,组件会自动刷新验证码 console.error('验证失败', err) } })

有几个前端细节值得注意。一是mode选择,弹窗模式(pop)不会打断用户的页面操作,体验优于固定模式,但要注意移动端弹窗的样式适配;二是验证码组件内部的get/check请求走的是自定义接口,意味着组件本身不关心后端实现,只要保证两个接口的出入参格式和AJ-Captcha约定一致就行;三是success回调里拿到的validate一定要临时存起来,而不是每次现取,因为第二次校验返回的validate只在很短时间窗口内有效。

5.3 校验时序:三个请求,谁先谁后

把前后端串起来看,完整的请求链路是三个请求依次发生:

  1. 前端加载验证码组件时,向后端发GET /captcha/get,拿到背景图、滑块图、token。
  2. 用户完成拖动/点选后,前端向后端发POST /captcha/check,携带pointJson和token,校验通过后拿到validate。
  3. 用户点击登录,前端把用户名、密码、validate一起发到业务登录接口,登录接口先校验validate,再走账号密码认证。

这三步合在一起,才是行为验证码完整的一套闭环。很多团队只做了前两步,第三步漏掉了,结果验证码形同虚设。攻击者只需要人工过一步,拿到validate后写进脚本里循环重放,验证码这道闸就直接失效了。这也是我在4.3里反复强调一次性票据的原因——validate必须绑定一次性使用,用完即焚,不留重放窗口。

6. 登录接口的防刷防暴力破解组合策略:验证码只是第一道闸

6.1 先想清楚:验证码失效之后,你的接口还靠什么

行为验证码把九成以上的脚本流量挡在了门外,但剩下的一成依然需要处理。比如攻击者用真人众包的方式过验证码,或者用真实浏览器内核的框架模拟行为,这些高级攻击手段不是验证码本身能解决的。换句话说,验证码是防刷的开始,而不是结束。

登录接口的防护必须是一个分层的组合策略。我的经验是按照“来源可信度”从高到低排列防护强度:正常可信用户走最轻的校验路径;可疑来源逐步加码,从IP限流到账号锁定再到强制二次验证;确认恶意来源直接拉黑并告警。每一层策略之间要能够独立开关,方便灰度调整。

6.2 Redis+Lua实现登录接口限流

限流是除验证码之外最重要的一道防线。SpringBoot3项目里最直接的做法就是基于Redis做应用层限流,用Lua脚本保证计数的原子性。我用的方案是结合固定窗口和滑动窗口两种模式:固定窗口适合对单个IP的单日总请求数做限制,滑动窗口适合对某个账号在一分钟内的高频尝试做精细控制。

这里是一个基于SpringBoot3 + Redis的滑动窗口限流Lua脚本:

-- key: 限流key,例如 rate:ip:1.2.3.4 -- argv[1]: 窗口大小(毫秒) -- argv[2]: 窗口内最大请求数 local key = KEYS[1] local window = tonumber(ARGV[1]) local limit = tonumber(ARGV[2]) local current = redis.call('TIME')[1] * 1000 + redis.call('TIME')[2] / 1000 -- 用 ZSET 存储请求时间戳,移除窗口外的记录 redis.call('ZREMRANGEBYSCORE', key, 0, current - window) local count = redis.call('ZCARD', key) if count < limit then redis.call('ZADD', key, current, current) -- 为滑动的 key 设置过期时间,避免冷数据堆积 redis.call('PEXPIRE', key, window) return 1 else return 0 end

在Java侧封装一个@RateLimit注解,通过AOP统一处理。在实际项目中我把限流分成了两个维度:IP维度和账号维度。IP维度用于拦截同一个出口IP的高频调用;账号维度用于拦截对某个特定账号的持续猜测。两个维度独立计数,后者需要账号字段在请求中可解析,通常在登录Service的入参里获取。

6.3 账号维度锁定与异常告警

限流解决的是“短时间内大批量打”的问题,账号锁定解决的是“低频长时间试探”的问题。暴力破解经常是慢速的,每分钟试三五个密码,IP限流很难命中,这时候账号维度的策略就要跟上。

我在项目里的实现是:每个账号维护一个Redis计数器,记录连续登录失败的次数,失败一次加一,同时设置过期时间。当失败次数达到5次时,锁定该账号15分钟,期间输入正确密码也拒绝登录。密码正确时清除失败计数。这个策略很朴素,但效果非常稳定。

另外还有一个小技巧值得分享:不要对所有账号一视同仁地触发验证码。正常用户的浏览器和设备指纹是稳定的,如果同一个用户短时间内多次触发验证码,说明可能有人在自动化模拟。我在验证码触发策略上做了分级:

风险等级触发条件校验强度
低首次登录、固定设备默认不弹验证码
中切换IP、异常时间登录滑动拼图
高连续失败超过3次、触发频率高文字点选 + 账号临时锁定

实际落地后,正常用户的登录路径基本不会碰到验证码,体验几乎没有影响;而脚本和暴力破解在第一个高风险账号登录尝试时就会被卡住,攻击成本显著上升。

7. 实战踩坑记录:从logback-spring.xml到校验失败率飙升

7.1 坑一:反向代理下面,限流拿到了同一个IP

上线第一天就发现一个诡异现象:所有限流规则全都没生效,仔细一查,所有请求拿到的IP都是Nginx的内网地址。原因很简单,我用的SpringBoot3服务在Nginx后面,直接读request.getRemoteAddr()拿到的当然是代理服务器IP,而不是真实客户端IP。

解决方案是加一个过滤器,从X-Forwarded-For或X-Real-IP头中提取真实IP,并把它放进ThreadLocal或者请求上下文里,供后续的限流AOP和日志模块复用。要提醒的是,X-Forwarded-For头本身可以被客户端伪造,所以必须保证只有可信反代层才能设置这个头,服务端取的时候要取最右侧追加的IP,而不是最左侧。

7.2 坑二:验证码过期时间设置太短

有段时间运营反馈用户登录时“滑动完提示验证失败”,排查发现验证码缓存TTL设成了30秒。用户打开登录页、停留几秒、再滑一下,时间就超过了30秒,后端校验时缓存已过期,直接判失败。

这个坑其实很好避免,但容易被忽视。验证码的过期时间要考虑的是“用户从看到验证码到完成操作”的最长合理时间,而不是你希望验证码多久刷新一次。我后来统一把captcha.get返回的验证码TTL调整为120秒,再把校验通过后的validate票据独立设置90秒有效期。前者给用户留足操作时间,后者严格控制重放窗口,两者各司其职。

7.3 坑三:滑块轨迹校验误伤正常用户

接入初期发现一个反常数据:滑块校验失败率达到了15%,而且集中出现在移动端。查看后端校验日志,发现大量失败原因是“轨迹坐标偏移超限”。进一步排查,移动端部分低端手机上触摸采样率不足,手指滑动再快,上报的轨迹点也比较稀疏,偏移量一放大就超过了阈值。

AJ-Captcha的轨迹校验逻辑是通过前端上报的坐标序列和耗时计算相似度的,对轨迹点数量有一定要求。解决思路有两个方向:一是调低相似度阈值,给移动端留出更多容错空间;二是关闭某些过于严格的干扰选项,比如滑块干扰轨迹。我最终采用的是动态阈值策略:PC端保留严格阈值,移动端按User-Agent识别后自动放宽。调整之后,移动端校验失败率降到了3%左右,基本和PC端持平。

7.4 坑四:日志爆炸,logback-spring.xml里的一行配置

验证码接口接入后,日志量以肉眼可见的速度暴涨。原因不复杂:/captcha/get和/captcha/check是高QPS接口,每个人打开登录页都会触发至少两次请求,即使不打DEBUG日志,INFO级别的出入参记录在高峰期也足以把磁盘撑爆。

排查后我在logback-spring.xml里针对这两个路径单独降级了日志级别。需要注意,Logback的默认配置是按全局级别来的,想要单独控制某个路径,可以用Logger标签配合filter实现。配置方式大致如下:

<springProfile name="prod"> <logger name="com.captcha" level="WARN" additivity="false"> <appender-ref ref="FILE_ERROR"/> </logger> </springProfile>

更进一步的做法,是给验证码接口的访问日志单独建一个文件,和业务日志分离,后续排查验证码问题的时候直接看这个文件,不会被其他日志干扰。这套小改造做完之后,日志磁盘增长问题彻底解决,排查验证码相关问题也更快了。

最后再分享一个经验:行为验证码接入本身不难,难的是把它嵌进整体的防刷体系里。验证码、限流、账号锁定、会话票据,每一环单独拿出来都不复杂,但组合在一起才能形成真正的防御纵深。如果你正在SpringBoot3项目里做登录安全改造,建议按这个顺序逐步落地,边上线边观察误杀率,不要一次性把闸门全部开到最严。

返回列表