主题
项目总览与架构全景
一套可私有化独立部署的行为式验证码系统:客户端取题面 → 服务端校验并归因 → 命中写名单 → 后台可查可配,全部组件以 Docker 容器 + WAR/JAR 形态交付。
1. 产品定位与典型调用方
系统对外只暴露一组协议入口,全部收敛在 captcha-server 的 /api/** 路径族下(captcha-server/src/main/webapp/WEB-INF/captcha-server-servlet.xml:21-33 只对 /api/** 挂拦截器,其余路径直接走静态资源映射)。实际映射如下,注意取图与校验是 v3、二次校验与配置是 v2,且 check 是 GET:
| 协议 | 实际路径 | 方法 | 入口 |
|---|---|---|---|
| 取图 | /api/v3/get(小程序 /api/v2/mp/get) | GET | GetControllerV2:95(:55) |
| 校验 | /api/v3/check(小程序 /api/v3/mp/check) | GET(小程序 POST) | CheckControllerV2:80(:50) |
| 二次校验 | /api/v2/verify | POST | VerifyControllerV2:55 |
| 配置下发 | /api/v2/getconf(小程序 /api/v2/mp/getconf) | GET | GetConfController:129(:92) |
| 手机号绑定 | /api/v2/mobile.html | GET | MobileControllerV2:36 |
| 数据采集 | /api/v2/collect(小程序 /api/v2/mp/collect) | GET / POST | SdkCollectController:66(:51) |
| 调用方 | 典型场景 | 接入方式 | 依赖协议 |
|---|---|---|---|
| 客户前端页面(H5/PC Web) | 登录、注册、发帖、领券前的人机校验 | 引入 JS SDK,按版本从 /static/{version}/ 加载 | get、getConf、check |
| 客户业务后端 | 对前端 check 结果做二次确认,防止前端伪造结果 | HTTP 调用 / Java SDK NECaptchaVerifier | verify |
| 运营商短信网关 | 短信验证码上行回调(语音/短信答题) | 运营商上行推送 | UplinkSMSController |
| 客户运维/运营人员 | 产品配置、名单维护、日志检索、统计导出 | 浏览器访问管理后台 | 后台 HTML/JSON 接口 |
| 图片/静态资源访问方 | 客户端拉取验证码图片与 SDK 静态资源 | 直连 captcha-image | GET /image/* |
四个协议的分工是本系统的核心语义边界,不可混用:
get下发题面(captchaId+ 图片/语音资源),由GetControllerV2+GetCaptchaService承担;getConf下发 SDK 资源与灰度配置,按InterfaceVersion分支返回不同结构(GetConfController);check接收加密轨迹/答案(data字段必须经NisCryptoWrapper解密),返回一次性凭证validate;verify是服务端到服务端的二次校验,凭证validate由本系统签发、客户后端回传。
关键设计取舍:
get/check面向不可控的老客户端,因此协议版本以「新增*ControllerV2平行类」方式演进,而非在旧类上打分支——这解释了仓库中大量*ControllerV2命名的由来。
2. 技术栈总表
版本一律以根 pom.xml 的 <properties> 为准(pom.xml:22-173),下表只列实际被模块引用的组件;仅在 dependencyManagement 中声明版本、无模块引用的组件单列在末尾。
| 分类 | 组件 | 版本(根 pom 行号) | 实际使用模块 |
|---|---|---|---|
| 语言/构建 | Java / Maven 多模块 | 1.8(pom.xml:25) | 全部 |
| Web 框架 | Spring Framework | 5.3.39(pom.xml:29) | server / admin / image / base |
| 安全 | Spring Security | 5.8.14(pom.xml:30) | 仅 admin |
| 持久层 | MyBatis / mybatis-spring | 3.5.16 / 2.1.2(pom.xml:42-43) | base(唯一 DAO 出口) |
| 连接池 | Druid | 1.2.23(pom.xml:44) | base |
| 数据库驱动 | MySQL Connector / PanWeiDB JDBC | 8.0.33 / 1.0.0(pom.xml:45-46) | base |
| 缓存 | Jedis / spring-data-redis | 3.9.0 / 2.7.18(pom.xml:50,32) | base、server(高频统计) |
| 检索 | Elasticsearch Java Client | 7.17.28(pom.xml:54) | base(行为日志按日索引) |
| 算法 | Spark MLlib + Scala | 3.5.2 / 2.12.8(pom.xml:59-61) | 仅 server(同时编译 src/main/scala) |
| 图像处理 | JavaCV / OpenCV / img4j / jhlabs | 1.5.10 / 4.9.0-1.5.10 / 0.5.13(pom.xml:94-98) | base(题面生成)、test(破解用例) |
| 调度 | Quartz(版本声明)+ Spring @Scheduled(实际使用) | 2.3.2(pom.xml:146) | schedule、server、admin |
| 短信/邮件 | javax.mail | 1.6.2(pom.xml:109-110) | base(告警邮件) |
| 对象存储 | AWS S3 SDK | 1.12.767(pom.xml:113) | base(S3UploadService) |
| 监控 | Prometheus simpleclient | 0.16.0(pom.xml:116) | server(指标暴露)、admin(拉取判告警) |
| 模板引擎 | FreeMarker | 2.3.33(pom.xml:143) | admin(后台页面)、server(错误页) |
| 日志 | Log4j2 + slf4j + disruptor(异步) | 2.25.3 / 1.7.36 / 3.4.4(pom.xml:35-39) | 全部 |
| JSON | Jackson / fastjson | 2.17.2 / 1.2.83(pom.xml:70-71) | 全部 |
| 工具 | Guava / commons-lang3 / Kryo | 33.2.1-jre / 3.20.0 / 2.24.0(pom.xml:119-139) | 按模块 |
| 网易内部 | nisp-commons / ip-idc-local-api / netease-captcha / qy-qos | 0.5 / 1.2.0 / 0.182C0.10 / 1.1.0-SNAPSHOT(pom.xml:164-170) | 按模块 |
| 测试 | JUnit / TestNG / Selenium / BrowserMob | 4.13.2 / 7.5.1 / 4.9.1 / 2.1.5(pom.xml:82-91) | test |
仅在根 pom 声明版本、无任何模块引用的组件:spring-kafka 2.9.13(pom.xml:309-319)、dubbo 2.8.4-nisp(pom.xml:476-479)、redisson 3.27.2(pom.xml:606-608)、zookeeper 3.8.4 / curator 5.7.0(pom.xml:486-518)。这些是历史遗留的依赖坐标,不参与运行时,删除前需确认无反射/配置引用。
3. 模块职责表
| 模块 | 打包 | 容器端口 | 部署形态 | 职责 | 可伸缩性 |
|---|---|---|---|---|---|
captcha-common | jar | — | 编译期依赖,不单独部署 | 契约层:错误码/命中码/名单枚举、CaptchaInfo 模型、NIS 加解密、设备指纹 SDK | — |
captcha-base | jar | — | 编译期依赖,不单独部署 | 基础设施层:MyBatis DAO、Redis/ES 客户端、SysConfLoader 动态配置、License、题面素材生成 | — |
captcha-server | war | 8085 | Tomcat 容器,多副本 | 核心业务:协议入口 + 风控规则责任链 + 名单拦截 + 限额 + 模型在线预测 + 指标暴露 | 多副本,需消除 @Scheduled 重复 |
captcha-admin | war | 8082(上下文 /mgt/decision-captcha/v2/admin) | Tomcat 容器 | 运营配置:产品/名单/频率/审核流/日志/统计/版本,Spring Security 认证 | 建议单副本或按 is.backup 收敛 |
captcha-image | war | 8080 | Tomcat 容器 + 持久卷 | 图片存储与分发:上传鉴权、图片处理、多节点同步、JS SDK 静态资源 | 多副本但需 RWX 卷 |
captcha-schedule | jar | 无端口 | java -cp 常驻进程(Main) | 离线任务:题面预生成与补量、图片/ES/LSH 清理、版本灰度切换、请求统计 | 必须单副本 |
captcha-demo | war | 8083(上下文 /mgt/decision-captcha/v2/demo) | 演示环境,可选交付 | 前端演示页 + Java 接入 SDK(NECaptcha / NECaptchaVerifier) | 无约束 |
captcha-test | jar(shade 出可执行包,auto.Main) | — | 本地/测试环境执行 | Selenium + TestNG 验证码自动化破解用例,用于风控阈值验证 | 无约束 |
两处易踩的端口事实:captcha-image/pom.xml:165 的 tomcat7 插件写的是 8084(仅本地 mvn tomcat7:run 用),容器内实际暴露 8080;captcha-server/pom.xml:356-379 同时配了 tomcat7(8085)与 jetty(8080),以 tomcat7 的 8085 为交付口径。
4. 架构全景图
┌──────────── 客户前端 / 业务后端 ────────────┐
│ JS SDK · NECaptchaVerifier(Java) │
└───────┬──────────────────────┬─────────────┘
/api/v2/{get,getConf,check} /api/v2/verify
│ │
┌───────────────────────────────▼───────────────────────▼──────────────────┐
│ captcha-server :8085 (WAR, 多副本) │
│ ├ api/ GetControllerV2 · CheckControllerV2 · VerifyControllerV2 │
│ │ GetConfController · UplinkSMSController · SdkCollect │
│ ├ 准入链 LicenceCheckInterceptor → PrometheusMetricsInterceptor │
│ ├ 名单链 InterceptorServiceFactory → 指纹 / IP / 用户 三维短路 │
│ ├ 限额 RedisRateLimiter(独立 Redis) │
│ ├ 规则链 CaptchaContextCheckServiceImpl.checkInner │
│ │ Data → Header → PassTime → Trace → Model → Frequency │
│ └ 类型族 puzzle / click / icon / drag / avoid / space / word* / sms│
└───────┬───────────────────────────────────────────────┬─────────────────┘
│ 读配置/名单、写行为日志 │ 读预生成题面
┌───────▼─────────────────────────────┐ ┌─────────────▼─────────────────┐
│ captcha-base (JAR,被依赖) │ │ Redis 缓存 + HTTP 拉图 │
│ ├ dao/*Mapper (MyBatis + Druid) │ └─────────────┬─────────────────┘
│ ├ redis/*Service │ │
│ ├ sys/conf/SysConfLoader(30s 轮询) │ │
│ ├ license/ (nc_license) │ │
│ ├ licence/ (nc_licence) │ │
│ ├ imagemeta/* 题面生成算法 │ │
│ └ upload/S3UploadService │ │
└───┬──────────┬──────────┬───────────┘ │
│ │ │ │
┌───▼───┐ ┌────▼────┐ ┌───▼────┐ ┌─────────▼──────────┐
│ MySQL │ │ Redis │ │ ES 7.17│ │ captcha-image :8080│
│ nc_* │ │ 业务缓存 │ │captcha │ │ 上传/下载/多节点同步 │
│ sys_* │ │ + 限流 │ │_info_ │ │ 持久卷 custom-image │
└───────┘ └─────────┘ │yyyy-MM │ └─────────▲──────────┘
▲ └────────┘ │ 上传素材
│ │
┌───────┴──────────────────────────────┐ │
│ captcha-admin :8082 │ ┌───────┴──────────────┐
│ Spring Security + FreeMarker 页面 │ │ captcha-schedule │
│ 产品/名单/频率/审核/日志/统计/版本 │ │ (JAR, 无端口, 单副本) │
└──────────────────────────────────────┘ │ 预生成 + 补量 + 清理 │
└──────────────────────┘
旁挂:Prometheus(admin 5s 拉取判告警 + 邮件) · SMTP · S3 兼容对象存储图中三条跨模块硬约束:captcha-server 与 captcha-admin 互不编译依赖(通过共享库 + DB 通信,配置生效是跨进程语义);captcha-base 是唯一数据访问出口;题面与图片只由 captcha-base:imagemeta/* 生成,captcha-server 只消费。
5. 核心链路一:取图(get)
- 客户端带
captchaId(首次可为空)、fp、user、version请求GET /api/v3/get(小程序走GET /api/v2/mp/get)。 LicenceCheckInterceptor.preHandle调licenceService.queryHaveLicence(),无有效 License 直接返回LICENCE_ERROR并中断(captcha-server/src/main/java/com/netease/is/nc/server/common/interceptor/LicenceCheckInterceptor.java:27-32)。PrometheusMetricsInterceptor记录请求维度指标(captcha-server/src/main/webapp/WEB-INF/captcha-server-servlet.xml:28-32注册)。GetControllerV2解析参数 →GetCaptchaService按CaptchaType+TypeEnhance做能力校验:Validate.isTrue(licenseService.canUseCaptcha(captchaType, ...), "license不支持验证码类型:%s类型,增强类型:%s", ...)—— 这一步是取图的强校验点(captcha-server/src/main/java/com/netease/is/nc/server/service/GetCaptchaService.java:103)。- 从缓存/Redis 队列中取一条已预生成的题面(拼图缺口坐标、点选点位、词序答案等),组装
CaptchaImg。 - 返回
captchaId+ 图片地址;客户端再向captcha-image(或 S3/CDN)实际拉取图片二进制。 - 若缓存空,走
*CaptchaFallback(如PuzzleCaptchaFallback)降级,仍拿不到则按错误码返回——取图链路上绝不做同步图片生成,这是性能红线。
6. 核心链路二:校验(check / verify)
| 阶段 | 执行者 | 关键动作 | 命中/失败后果 |
|---|---|---|---|
| 1. 准入 | LicenceCheckInterceptor | License 有效性 | 直接拒绝,不进入业务 |
| 2. 名单短路 | InterceptorServiceFactory.methodInterceptor | 设备指纹 → IP → 用户 三维短路(IpListInterceptorServiceImpl.methodInterceptor:49) | 返回 413(指纹)/ 418(IP、用户),并写 hitBlacklist |
| 3. 解密 | NisCryptoWrapper | 用会话密钥解密 data 密文字段 | 解密失败 → SUCCESS_RATE_DATA_ERROR=0.15 语义的失败分支 |
| 4. 规则链 | CaptchaContextCheckServiceImpl.checkInner | Data → Header → PassTime → Trace → Model → Frequency 顺序判定 | 命中则覆写 hitType;高频命中额外 handleFreqHitBlacklist 回写名单 |
| 5. 高频统计 | FrequencyMonitorRedisService.hitByCount / hitBySimpleSet | 桶分片滑动窗口 + Lua 原子计数;StatResult{sum,start,end} | sum > threshold 即命中,SUCCESS_RATE_FREQUENCY=0.0(高频必拦) |
| 6. 建模打分 | ModelCheckServiceImpl + ModelLocalCache | Spark MLlib 模型本地缓存在线预测(LR/DT/RF/GBDT/GBDTLR/SVM) | 产出风险标签/分数与轨迹模型命中码 |
| 7. 落库 | CaptchaInfoServiceImpl.save | 行为日志写入 ES 日索引 captcha_info_yyyy-MM-dd | 字段名即 ES 模板契约,改名等于破坏历史查询 |
| 8. 出证 | CheckControllerV2 | 返回一次性 validate | 客户端拿它去客户后端换最终结论 |
| 9. 二次确认 | VerifyControllerV2 | 校验 validate 真伪与时效 | 防前端伪造,verify 是唯一可信结论来源 |
checkInner 的语义等价是纯重构红线:SUCCESS_RATE_FREQUENCY=0.0、SUCCESS_RATE_DATA_ERROR=0.15 不可调整,降级只允许改复杂度、不允许改判定结果。
7. 核心链路三:离线预生成 → image → ES
captcha-schedule由Main启动ClassPathXmlApplicationContext("scheduler-application-context.xml"),无 Web 容器、无 HTTP 端口(captcha-schedule/src/main/java/com/netease/is/nc/scheduler/Main.java:11-15)。- Spring
@Scheduled触发各类型*ProduceWorker.produce(),例如拼图:@Scheduled(fixedDelay = 60000)(captcha-schedule/src/main/java/com/netease/is/nc/scheduler/produce/worker/PuzzleProduceWorker.java:66-73)。 - Worker 遍历所有
ImageSet→ 计算缺口 → 若库存低于水位则投递*FillTask到线程池(exec.submit(fillTask))批量生成(同文件:80-99)。 *CreateService调captcha-base:imagemeta/*算法生成图片与题面元数据(缺口坐标、点位、答案),写ElementCacheService缓存。- 图片经
captcha-base:upload/S3UploadService上传对象存储,或在纯私有化模式下按image.upload.server逐节点推送到每个captcha-image(该键必须写 IP,不能写域名,见.codemaker/codeindex/_architecture.md:51)。 - 清理与同步链:
ImageCleanUpWorker(30s)→ImageStatusSyncJob(5s)→ImageDeleteSync(5s),保证 DB 记录与磁盘一致。 - ES 生命周期:行为日志由
captcha-server实时写入captcha_info_yyyy-MM-dd;EsIndexCleanUpWorker每天 02:00 按elasticsearch.index.retention.days(默认 180)整体删除过期索引(captcha-schedule/src/main/java/com/netease/is/nc/scheduler/job/clean/EsIndexCleanUpWorker.java:37)。清理必须按索引命名规则推导目标,禁止通配符批量删除。 - 版本侧:
SubVersionSwitchJob(60s)按nc_version的status/gray_ratio灰度切换 active 的 JS SDK 版本,getConf据此返回对应资源。
8. 部署形态与单副本约束
| 组件 | 副本约束 | 根因 |
|---|---|---|
captcha-schedule | 强制 1 副本 + strategy: Recreate | 全部为 Spring @Scheduled,仓库内无任何分布式锁(captcha-schedule 全模块检索 tryLock/setnx/synchronized 零命中);多副本会重复生成图片、重复写统计、重复执行删除 |
captcha-admin | 建议 1 副本 | PrometheusMetricsJob 每 5s 拉 Prometheus 判告警并发邮件(captcha-admin/src/main/java/com/netease/is/nc/admin/job/PrometheusMetricsJob.java:56),副本数 = 邮件重复倍数;仅当 is.backup=true 时才跳过(:58,默认 base.properties:58 为 false) |
captcha-server | 可多副本,但需知悉 | 6 处 @Scheduled 为请求量统计聚合(DeviceStatHandler:82、LocationStatHandler:154、StatHourly:104、HourlyIpService:131、ZhuoWangIpEnrichmentService:120、AbstractListManagementSync:155),多副本会按副本数重复累加 |
captcha-image | 可多副本,需 RWX 卷 | 图片落本地 /java-web-dir/custom-image/;若已有 S3,则不应部署该模块 |
容器编排要点(仓库内无现成 k8s 清单,以下为交付要求而非仓库文件):
- 只有
captcha-image需要持久卷(image.properties的upload_file_path);日志、.runtime/applib、/tmp用 emptyDir。 - 探针:
captcha-server→GET /metrics(captcha-server/src/main/webapp/WEB-INF/web.xml:51-58映射 PrometheusMetricsServlet);captcha-admin→GET /login.html;captcha-image→GET /;captcha-schedule无端口,只能 exec 探针。 captcha-schedule的启动脚本会把加密lib.enc解密到运行目录再拉起 JVM,密钥随镜像分发(captcha-schedule/docker/entrypoint.sh:36-48, 61-83),无需注入环境变量;classpath 顺序为approot/conf→approot→lib/*,因此外部conf/*.properties天然覆盖 jar 内同名文件。- WAR 模块覆盖配置只能用「同名文件遮蔽」:ConfigMap +
subPath挂到/java-web-dir/webroot/WEB-INF/classes/{base,elasticsearch,frequency,image}.properties,因为WEB-INF/classes优先于WEB-INF/lib;-D参数不能覆盖已有 key(见第 9 节)。
9. 配置源与生效语义
| 配置 | 位置 | 生效方式 |
|---|---|---|
| 唯一静态配置源 | captcha-base/src/main/resources/base.properties | 由 basePropertyConfigurer 加载(captcha-base/src/main/resources/base-application-context.xml:16-19),全模块共享 |
| 动态配置 | MySQL sys_conf 表 | SysConfLoader 每 30s 轮询刷新(captcha-base/src/main/java/com/netease/is/nc/base/sys/conf/SysConfLoader.java:44, 91-99),无 properties 回退 |
| 限流专用 Redis | captcha-server/src/main/resources/frequency.properties | 独立第二套 Redis,硬编码 classpath 读取,只能同名文件遮蔽 |
| 图片存储 | captcha-image/src/main/resources/image.properties | 本地路径 + 多节点配置 |
| 生产参数 | captcha-schedule/src/main/resources/produce.properties | 可用 approot/conf 外部覆盖 |
| 许可 | nc_licence + nc_license 两张表 | 改库即生效(5s / 60s 各自轮询),见风险清单 R-03 |
一个反直觉但必须记住的事实:DecryptingPropertyPlaceholderConfigurer 只重写了 convertPropertyValue 做 Enc(...) 密文解密(captcha-base/src/main/java/com/netease/is/nc/base/filter/DecryptingPropertyPlaceholderConfigurer.java:16-24),并未改 systemPropertiesMode,因此保持 SYSTEM_PROPERTIES_MODE_FALLBACK——-D/环境变量只在 key 缺失时兜底,无法覆盖 base.properties 里已存在的键。