主题
captcha-base 基础设施
captcha-base 是四个运行模块共用的基础设施层:MyBatis + Druid 数据访问、Redis 多模式封装、Elasticsearch 7.17 新版客户端、图片处理与 S3 上传、配置解密与动态配置加载——它不承载 HTTP 接口,但决定了所有上层模块的运行边界。
一、包结构与职责
captcha-base/src/main/java/com/netease/is/nc/base/ 下按「业务域 + 技术设施」混合分包,业务域包内部固定为 实体 / Mapper / Service / ServiceImpl / QueryXxx 五件套结构(例如 app/、icon/、image/、frequency/)。
| 包 | 职责 | 典型内容 |
|---|---|---|
abst | 抽象基类与通用抽象 | BaseEntity、MyBatisMapper 注解、AbstractRedisService、AbstractCacheRedisService、SyncAbstractRedisService |
app | 业务应用(产品)实体与缓存 | App、AppMapper、AppCacheService、RiskWarnConf |
image / imageoutput / imageset / imagemeta | 图片、图片输出、图片组、验证码元数据 | Image、ImageServiceImpl、ImageConf、各 *Create / *Builder |
frequency | 频率限制策略引擎 | Frequency、FrequencyCondition、FrequencyMatchers、FrequencyValueResolver、StrategyDetailAssembler |
feature | LSH 特征(历史遗留) | LshFeature*(写入源已废,见表增长) |
redis | Redis 客户端装配与 key 规范 | RedisKeyFormat、模板配置 |
filter | 配置解密过滤器 | DecryptingPropertyPlaceholderConfigurer、PropertiesEncryptUtil |
sys | 动态配置 | SysConfLoader(30s 轮询 sys_conf 表) |
licence / license | 两套 License 实现 | licence 走拦截器 + 后台导入;license 走取图鉴权 |
interceptor | 通用拦截器基座 | 供 server 侧 IP/用户/License 拦截复用 |
metrics / prometheus | 监控埋点 | 计数器、告警判定 |
upload | 对象存储 | S3UploadService(当前唯一生效实现) |
utils / email / log / request / version / ua / space / product / namelist / device / collect / clickconf / audit / model / managedip / managedfingerprint | 工具、邮件、日志、请求上下文、版本、UA 解析、空间推理、产品、名单、设备统计、SDK 采集、点选配置、审核流、模型、IP/指纹托管 | — |
值得先记住的两个「双份」现象:licence 与 license 是两套互不相通的 License 表与代码;image 与 imageoutput 是组配置与图片明细两层数据。
二、MyBatis 装配
装配入口是 captcha-base/src/main/resources/base-db-application-context.xml:35-47,要点三条:
- Mapper 扫描靠自定义注解
@MyBatisMapper,不用@Mapper。扫描器是MapperScannerConfigurer,注解类在abst/MyBatisMapper.java。这意味着漏标注解的 Mapper 不会被注册,且启动期不报错,调用时才抛Invalid bound statement。 VendorDatabaseIdProvider按DatabaseMetaData.getDatabaseProductName()判定厂商,加载<databaseId>分支的 SQL。同一个 Mapper XML 内为 MySQL / PanWeiDB / Oracle 各写一份语句,由 databaseId 选择。私有化交付时「SQL 跑出别的方言」几乎都是这里没配databaseIdProvider的property映射。Query*基类统一分页:offset / limit 传-1表示不分页,SQL 层用<if>判断跳过LIMIT。业务代码里看到-1不要当异常值,这是约定。
xml
<!-- base-db-application-context.xml:35-47 结构示意 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.netease.is.nc.base"/>
<property name="annotationClass"
value="com.netease.is.nc.base.abst.MyBatisMapper"/>
</bean>
<bean id="vendorDatabaseIdProvider"
class="org.apache.ibatis.mapping.VendorDatabaseIdProvider">
<property name="properties">
<props>
<prop key="MySQL">mysql</prop>
<prop key="Oracle">oracle</prop>
</props>
</property>
</bean>三、Druid 连接池
数据源定义在 captcha-base/src/main/resources/base-db-application-context.xml:13-33,全部参数硬编码在 XML,不读 properties。因此调整连接池只能改文件 + 重新打包,或者用 ConfigMap 遮蔽 base.properties——但后者改不到 XML。
| 参数组 | 作用 | 调参注意 |
|---|---|---|
initialSize / minIdle / maxActive | 池容量三件套 | 单实例连接数 × 副本数必须小于 DB 侧 max_connections 的安全水位 |
maxWait | 取连接超时 | 过大会让请求长时间挂住,掩盖下游慢查询 |
validationQuery / testWhileIdle / testOnBorrow | 连接可用性探测 | 云上 DB 有空闲断连策略时必须保持探测,否则夜里首请求必失败 |
timeBetweenEvictionRunsMillis / minEvictableIdleTimeMillis | 空闲回收 | 与 DB 侧 wait_timeout 对齐 |
filters | 统计与防火墙 | 开启 stat 才有 Druid 监控数据;开启 wall 会拦截部分私有化方言 SQL |
具体数值见该文件 13-33 行。判据:maxActive 的取值与 captcha-server 的实际并发能力强相关,扩容副本前必须先算 DB 连接总量,这是私有化交付里最常见的翻车点。
四、Redis
连接池与五种部署模式
底层用 Jedis(3.9.0)+ JedisPoolConfig,核心参数:
| 参数 | 值 | 含义 |
|---|---|---|
maxTotal | 300 | 单实例最大连接数 |
maxIdle | 300 | 最大空闲连接 |
minIdle | 50 | 最小空闲连接 |
maxWaitMillis | 2000 | 取连接最长等待(毫秒),超时直接抛异常 |
redisType 取值 1~5,对应五种部署形态,同一套代码按配置切换:
redisType | 形态 | 交付注意 |
|---|---|---|
| 1 | 单机 | 仅测试/小规模 |
| 2 | 主从 | 无自动故障转移 |
| 3 | Sentinel | 需提供 master name 与哨兵列表 |
| 4 | Cluster(分片集群) | key 必须落在同一 slot 的操作要避免 |
| 5 | Sharding(客户端分片) | 分片规则在客户端,扩缩容需重算 |
maxWaitMillis=2000 小于常见业务超时,这是「Redis 抖动会直接变成接口错误而不是慢请求」的原因;反过来也意味着 Redis 侧一旦打满,captcha-server 的错误率会立刻抬升。
两套序列化:StringRedisTemplate 与 Kryo
| 模板 | 序列化 | 用途 |
|---|---|---|
StringRedisTemplate | 纯字符串 | 计数、队列长度、简单缓存、分布式标记 |
ImageOutputRedisTemplate | Kryo | 图片输出对象等复杂结构,需与生产侧/消费侧完全一致 |
Kryo 序列化的强约束:类结构变更(加字段、改字段类型)会让旧数据反序列化失败。私有化升级时如果改了 ImageOutput 相关 DTO,必须先清队列或做版本化 key,否则实时取图会大量报错。
Redisson 零引用
pom.xml 里引了 Redisson 3.27.2,但代码中零引用:没有 RedissonClient Bean,没有分布式锁、没有 RBucket。所有「锁」语义都是自实现的(Redis setnx 或进程内 AtomicBoolean)。
这直接解释了 captcha-schedule 为什么必须单副本——如果真要水平扩容,第一步应是引入 Redisson 分布式锁,而不是加副本。
五、Elasticsearch
客户端选型
用的是 co.elastic.clients.ElasticsearchClient(ES 7.17 起的新版 Java API Client),不是 RestHighLevelClient。这带来两个连锁结果:
- 查询构造是 builder 风格(
Query.of(q -> q.match(...))),与网上大量基于QueryBuilders的示例代码不通用; - 客户端启动时会校验响应头
X-Elastic-Product: Elasticsearch,缺失即抛product check失败。部分兼容 ES 协议的国产/云上搜索服务不返回该头,因此代码里存在显式补该头的兼容 hack——这是私有化交付必须知道的一条:换非官方 ES 实现时,不要急着改业务代码,先确认这层 hack 是否生效。
配置与索引
- ES 配置在独立文件
captcha-base/src/main/resources/elasticsearch.properties,与base.properties分离。私有化覆盖时两者都要挂。 - 索引命名
captcha_info_yyyy-MM-dd,按天滚动。 - 索引模板与 ILM 策略在
build/es/目录,属于部署硬前置的一部分(与build/db_init/同级重要性)。 - 清理是双机制:
EsIndexCleanUpWorker(应用内定时删)+ ILM(ES 侧自动删)。客户环境若不支持 ILM,应用侧任务成为唯一手段;若两者都开,应用侧的删除通常是冗余兜底。
六、图片处理封装
| 能力 | 实现位置 | 说明 |
|---|---|---|
| 缩放 / 裁剪 / 格式转换 | Thumbnails(thumbnailator 0.4.x) | 预生成与实时 resize 共用 |
| 滤镜扰动 | ExposureFilter / RGBAdjustFilter | 拼图产线用于抗模板匹配 |
| 元数据构建 | imagemeta 包下 *Create / *Builder | 按 CaptchaType 分派 |
| 上传 | upload/S3UploadService | 唯一生效实现,本地 HTTP 上传已禁用 |
| 本地存储 | captcha-image 的 image.properties 路径 | 仅 image 模块需要持久卷 |
注意 JavaCV / OpenCV 4.9.0 虽在依赖里,服务端生产代码零调用,只在 captcha-test 的破解用例中使用。
七、配置加密:Enc(...) 是 SM2 国密
base.properties 中密码类字段可写成 Enc(...) 密文。链路由两个类构成:
filter/DecryptingPropertyPlaceholderConfigurer:继承 Spring 的PropertyPlaceholderConfigurer,在processProperties()阶段把值里的Enc(...)解出来;filter/PropertiesEncryptUtil:实际加解密实现,算法为 SM2(国密椭圆曲线),不是 AES/RSA。
私钥来源有两条,优先级为:
- JVM 参数
-Dconfig.decrypt.key(推荐,通过JVM_EXTRA_CONFIG注入); - 类内 硬编码
DEFAULT_PRIVATE_KEY作为兜底。
第 2 条意味着:只要拿到 jar 就能解密所有 Enc(...) 密文,配置加密在此实现下只防「明文泄露在配置文件里」,不防「拿到制品的人」。安全评审时按此定性。
三个必须区分的边界
| 场景 | 是否解密 | 说明 |
|---|---|---|
Spring 占位符 ${key} 引用的值 | 是 | 走 DecryptingPropertyPlaceholderConfigurer |
<util:properties> 直接加载的文件 | 否 | 不经过该 Configurer,Enc(...) 原样保留 |
Java 代码 Properties.load() 读取 | 否 | 需自行调用 PropertiesEncryptUtil |
CryptUtils | 否 | 另一套独立工具,硬编码密钥,与 SM2 链路无关 |
CryptUtils 硬编码密钥是独立风险项:它被用于部分内部字段加解密,密钥不进配置、无法轮换,属于「改代码才能换密钥」的历史包袱。
八、base.properties / irisk.properties 关键项
base.properties(唯一配置源)
| 分组 | 关键项 | 说明 |
|---|---|---|
| 数据库 | jdbc.url / jdbc.username / jdbc.password / jdbc.driver | 支持 MySQL / PanWeiDB / Oracle |
| Redis | redis.host / redis.port / redis.password / redis.type / 哨兵与集群相关 | redis.type 1~5,见第四节 |
| ES | 索引前缀、集群地址(部分在 elasticsearch.properties) | 两文件需一起覆盖 |
| S3 | endpoint / bucket / accessKey / secretKey | S3UploadService 唯一消费方 |
| License | license.publicKey | 为空不阻塞启动,但取图 100% 失败 |
| 解密 | config.decrypt.key(JVM 参数,非本文件) | 见第七节 |
| 取图服务 | getconf.api.server / getconf.static.servers | 为空会让 captcha-server 启动失败 |
| 邮件 | SMTP host / port / 账号 | admin 告警邮件使用 |
irisk.properties(风险检测)
| 分组 | 内容 | 说明 |
|---|---|---|
| 模型与阈值 | 各风险维度的判定阈值、权重 | 与 Spark MLlib 模型配套 |
| 开关 | 风控总开关、分维度开关 | 关闭后降级为纯行为校验 |
| 外部依赖 | 风控服务地址与超时 | 超时值需小于主链路超时 |
这两份文件是「改动即生效边界」的分水岭:properties 里的值需要重启;sys_conf 表里的值 30s 轮询生效。排查「改了配置没反应」第一步永远是确认配置落在哪一侧。
九、PropertyPlaceholderConfigurer 单列说明
captcha-base/src/main/resources/base-application-context.xml:16-22 定义了全局 PropertyPlaceholderConfigurer。它是整套配置体系的唯一权威解释器,三个后果必须背下来:
- 唯一配置源是 classpath 上的
base.properties。elasticsearch.properties、frequency.properties、image.properties、produce.properties各自独立加载,互不覆盖。 systemPropertiesMode未改,默认SYSTEM_PROPERTIES_MODE_FALLBACK:-D参数和环境变量只在 key 缺失时兜底,不能覆盖已有 key。所以「用环境变量覆盖数据库地址」在这套代码里不生效,这是所有 K8s 交付踩过的坑。- 解密与占位符替换在同一个 Configurer 里完成,所以
Enc(...)只能在经过它的路径上生效(见第七节表格)。
正因如此,WAR 模块(server / admin / image)的唯一正确覆盖手段是 ConfigMap + subPath 挂载到 /java-web-dir/webroot/WEB-INF/classes/{base,elasticsearch,frequency,image}.properties,靠 WEB-INF/classes 优先于 WEB-INF/lib 遮蔽 jar 内同名文件;而 captcha-schedule 天然支持外部 conf/*.properties(classpath 最优先),是最省事的一个。
十、复用与改造建议
| 目标 | 建议路径 | 风险 |
|---|---|---|
| 水平扩容 schedule | 先引入 Redisson 分布式锁替换进程内 AtomicBoolean | 依赖已在 pom,改造量小但需全链路验证 |
| 换非官方 ES 实现 | 保留 X-Elastic-Product 兼容 hack,勿改业务查询 | 客户端为 co.elastic.clients,示例代码不通用 |
| 加密配置 | 用 -Dconfig.decrypt.key 注入,勿依赖 DEFAULT_PRIVATE_KEY | 硬编码私钥可被逆向 |
| 连接池调优 | 直接改 base-db-application-context.xml | 参数不在 properties,无法热改 |
| 移除 OpenCV 依赖 | 需同步处理 captcha-test 破解用例 | 生产侧确实零调用 |