一个基于 Node.js 的 MIIT 备案查询服务,接收 GET 请求中的 domain、unitName、licence 参数,返回工信部备案详细信息,适配了最新版工信部查询系统的滑块验证。
部分思路来源于 Mxmilu666/fuckmiit,本项目在此基础上进行了全面重构和扩展,补充了输入校验、限流、缓存、错误脱敏、环境预检和失败冷却等机制,以降低上游风控风险。
- 基于 HTTP GET 的简单调用方式,支持
domain、unitName、licence三种查询参数。 - 纯 Node.js 实现,仅依赖
sharp用于图像解码。 - 内置工信部接口请求头、Cookie 会话、鉴权流程。
- 保留滑块验证码识别能力,并将验证码选择逻辑重构为 challenge 级独立决策与显式候选排序。
- 增加 domain 规范化与基础格式校验。
- 增加全局、每 IP、每 domain 的文件限流与失败冷却;可选切换为 Redis 后端,支持分布式限流、缓存和互斥锁。
- 增加同域 singleflight 查询锁,避免缓存未命中时并发击穿上游。
- 增加成功缓存和空结果短缓存,减少重复请求上游。
- 三种查询方式(domain/unitName/licence)的缓存互通:list 查询结果自动填充 domain 缓存,unitName 和 licence 结果互相写入对方缓存 key。
- 错误对外脱敏,对内写入服务端日志。
- 日志写入采用 best-effort 策略,日志失败不会破坏 API 响应。
- 将用户可调参数迁移到内置默认配置,支持通过
.env环境变量覆盖。 - 增加 queryByCondition 候选项诊断、标识符字段变体兼容和列表详情兜底。
- 增加验证码候选置信度日志与可选样本落盘,便于离线比对真实 challenge。
- 增加缓存 schema version、响应编码保护、错误分类、环境预检与基础测试骨架。
- 增加可选 API key 鉴权,可在配置中开启并要求请求通过
api_key查询参数或x-api-key请求头提供密钥。 - 响应中包含
cache(hit/miss)、duration(耗时)、cached_at(缓存时间)、cache_expires_at(过期时间)等状态字段。 - 每条请求自动输出 access log 到终端(stdout),格式为北京时间。
.
|- src/
| |- Api/
| | |- authApi.js
| | |- captchaApi.js
| | |- icpApi.js
| | `- miitClient.js
| |- Cache/
| | |- fileCache.js
| | `- queryCache.js
| |- Captcha/
| | |- captchaChallenge.js
| | |- captchaCore.js
| | |- captchaSolver.js
| | |- detectionCandidate.js
| | |- imageDecoder.js
| | `- rect.js
| |- Config/
| | `- appConfig.js
| |- controllers/
| | `- queryController.js
| |- Exception/
| | `- miitException.js
| |- Http/
| | `- jsonResponse.js
| |- RateLimit/
| | |- domainQueryLock.js
| | |- fileRateLimiter.js
| | `- queryGuard.js
| |- Service/
| | `- miitQueryService.js
| |- Support/
| | |- appPaths.js
| | |- clientIp.js
| | |- debug.js
| | |- detailSanitizer.js
| | |- environmentGuard.js
| | |- fileLock.js
| | |- fileStoreUtils.js
| | |- hash.js
| | |- logger.js
| | |- responseFormatter.js
| | |- time.js
| | `- utils.js
| |- Validation/
| | `- domainNormalizer.js
| |- app.js
| `- server.js
|- .env.example
|- storage/
| |- cache/
| |- locks/
| |- logs/
| `- ratelimit/
|- tests/
| |- appConfig.test.js
| |- captchaSolver.test.js
| |- domainNormalizer.test.js
| |- environmentGuard.test.js
| |- miitQueryService.test.js
| |- queryCache.test.js
| |- recordNotFoundException.test.js
| |- responseFormatter.test.js
| `- run.js
|- package.json
`- README.md
项目执行链路如下:
- 客户端发起请求:
GET /?domain=example.com(或?unitName=xxx、?licence=xxx) src/app.js创建 HTTP server,src/controllers/queryController.js处理请求EnvironmentGuard在入口阶段检查 Node 版本(>=18.18)和sharp可用性- 根据参数类型分流:
- domain 查询:
DomainNormalizer执行域名规范化与校验,然后执行步骤 5–19 - unitName/licence 查询:执行
MiitQueryService.queryList(),流程类似步骤 5–13,但返回完整列表而非单条详情;查询成功后自动将结果写入list:{unitName}和list:{mainLicence}缓存,并逐条填充success:{domain}缓存
- domain 查询:
QueryCache优先命中成功缓存或空结果缓存DomainQueryLock为同一 domain/singleflight key 提供查询锁- 获取锁成功后再次读取缓存,避免锁等待后的重复上游查询
- 只有真正准备访问上游时,
QueryGuard才执行全局、IP、domain 频控与冷却判断 MiitQueryService执行完整查询流程AuthApi请求/auth获取Token(含指数退避重试)CaptchaApi请求/image/getCheckImagePoint获取验证码挑战(含指数退避重试)CaptchaSolver会优先基于bigImage的灰色缺口检测生成候选,并辅以模板对比和近似兜底生成备选坐标;每个 challenge 只提交一个当前最优候选,如果校验失败则立即重新获取新的 challenge,再切换到下一类候选假设继续识别,避免同一个captchaUUID在首次失败后继续提交而被上游直接判定为过期IcpApi请求/icpAbbreviateInfo/queryByCondition和queryDetail(含指数退避重试)MiitQueryService对列表结果执行精确匹配,并优先选择具备有效标识符的候选项- 使用返回的
mainId、domainId、serviceId请求详情接口;若列表项已包含完整详情字段,可在详情标识缺失或详情接口失败时回退使用列表项 MiitQueryService对详情结果执行必填字段规范化与校验ResponseFormatter在真正写成功缓存前再次校验详情字段完整性- 成功结果进入带 schema version 的缓存并返回
- 失败按异常类型分类,分别映射为参数错误、频控错误、存储错误、环境错误、上游错误或内部错误
- 每条请求通过
respond闭包自动输出 access log 到终端(stdout),包含 IP、查询关键字、HTTP 状态、业务码、缓存状态和耗时
-
src/app.js/src/server.jsHTTP 入口,负责创建 server、路由分发、错误兜底和进程启动。 -
src/controllers/queryController.js请求处理管线,负责参数读取、缓存命中、singleflight、限流、上游查询和错误分类响应输出。 -
src/Validation/domainNormalizer.js负责域名规范化、长度限制、字符合法性和标签校验。 -
src/Config/appConfig.js负责加载内置默认配置,并支持通过.env环境变量覆盖默认值。同时对关键整数配置做上下界夹紧,避免 0、负数或异常大值破坏运行语义。 -
src/RateLimit/queryGuard.js负责全局、IP、domain 限流和失败冷却策略。当前通过consumeAll()实现多维限流的原子消费,避免单维失败污染其他维度计数。 -
src/RateLimit/domainQueryLock.js负责同一 domain 查询过程的 singleflight 控制。 -
src/Cache/queryCache.js负责成功缓存、空结果缓存和列表缓存,通过 schema version 隔离未来结构变化。提供 getSuccess/putSuccess、getMiss/putMiss、getList/putList 和 getStale 方法,内部复用 getDetail/putDetail 公共逻辑。 -
src/Cache/fileCache.js负责缓存文件的加锁读取、完整性校验写入和带锁轻量级过期清理。继承FileStoreBase。 -
src/Support/fileStoreUtils.js文件存储基类,为FileCache和FileRateLimiter提供共享的ensureDir(带缓存)、fileForKey、lockForFile、readJson、writeJson、removeExpired、gc(概率触发的过期清理,子类通过shouldDelete回调控制删除策略)方法,消除重复代码。 -
src/Api/miitClient.js通用 HTTPS 客户端,维护请求头、Cookie、超时控制和上游错误截断。 -
src/Api/authApi.js封装auth接口和authKey生成逻辑,并把鉴权协议失败统一归类为上游错误。 -
src/Api/captchaApi.js封装验证码获取与校验接口,并把验证码协议失败统一归类为上游错误。 -
src/Captcha/captchaSolver.js验证码识别核心模块,负责按单个 challenge 组织灰色缺口检测、模板候选生成、近似兜底、候选排序和 challenge 失败后的重新获取,并返回实际成功的captchaUuid供后续请求头写回。图像解码使用sharp。 -
src/Captcha/captchaCore.js验证码底层算法模块,提供连通域 flood-fill、方块判定、缺口颜色自适应采样、多组预置色域回退轮询和窗口评分等纯函数。 -
src/Api/icpApi.js封装备案列表和详情查询接口。 -
src/Service/miitQueryService.js业务编排层,串起完整的 MIIT 查询流程,并补充关键字段校验、列表精确匹配、有效标识符优先选择和列表详情兜底策略。 -
src/Support/logger.js负责将详细错误和 debug 诊断信息写入本地日志,并在日志失败时降级到process.stderr。 -
src/Http/jsonResponse.js负责响应输出,并在 JSON 编码失败时输出保底错误 JSON。 -
src/Support/environmentGuard.js负责运行前检查 Node 版本(>=18.18)和sharp可用性。 -
src/Support/responseFormatter.js负责最终成功响应封装,并显式校验详情必填字段存在性。 -
src/Storage/redisBackend.jsRedis 存储后端实现,包含RedisCache(带逻辑过期的双层 TTL 缓存)、RedisRateLimiter(基于 Lua 脚本的固定窗口原子限流)和RedisLockProvider(带 watchdog 续期的分布式互斥锁)。Redis 客户端使用 Promise 单例模式避免并发初始化竞态,并注册error事件监听防止未捕获异常。通过closeRedisClient()支持优雅关闭。
运行环境要求:
- Node.js 18 或更高版本。
- 已安装
sharp依赖(npm install)。 - 运行用户需要对项目目录下的
storage/有读写权限。 - 建议保留仓库内的
.gitignore和storage/.gitkeep文件,避免运行产物被误提交。 - 如需调整缓存时长、限流阈值、等待时间等参数,通过
.env环境变量设置,避免直接改源码逻辑。
建议在 Linux 或具备完整 Node.js 环境的服务器上运行。
git clone https://github.com/weyeahh/Get-miit-ICP.git
cd Get-miit-ICP
cp .env.example .env
# 编辑 .env 按需修改配置
npm install
npm start编辑监听地址、端口、调试开关或切换存储后端等,均可通过
.env设置。若未创建.env,服务将使用内置默认值运行。
启动后访问:
http://127.0.0.1:8080/?domain=baidu.com
所有配置项通过环境变量设置,推荐使用 .env 文件。将 .env.example 复制为 .env 并按需修改即可。
未设置环境变量时,服务使用代码内置默认值。所有整型配置都会经过 AppConfig 的上下界夹紧,不会直接相信原始值(0、负数或极大值会被强制压回安全范围)。
| 变量名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
HOST |
string | 127.0.0.1 |
服务监听地址 |
PORT |
integer | 8080 |
服务监听端口 |
MIIT_CACHE_SCHEMA_VERSION |
string | v1 |
缓存结构版本,修改后旧缓存自动失效 |
MIIT_CACHE_SUCCESS_TTL |
integer | 259200 |
成功查询结果缓存时长(秒),范围 60–604800 |
MIIT_CACHE_MISS_TTL |
integer | 43200 |
空结果缓存时长(秒),范围 30–86400 |
MIIT_CACHE_LIST_TTL |
integer | 86400 |
unitName/licence 列表缓存时长(秒),范围 60–604800 |
MIIT_CACHE_SUCCESS_STALE_TTL |
integer | 604800 |
成功结果 Redis 物理保留时长(秒),仅 redis 后端生效,范围 300–2592000 |
MIIT_CACHE_MISS_STALE_TTL |
integer | 86400 |
空结果 Redis 物理保留时长(秒),仅 redis 后端生效,范围 60–604800 |
MIIT_RATE_LIMIT_GLOBAL_QPS |
integer | 5 |
全局每秒最大上游查询数,范围 1–1000 |
MIIT_RATE_LIMIT_IP_PER_MINUTE |
integer | 60 |
单 IP 每分钟最大上游查询数,范围 1–10000 |
MIIT_RATE_LIMIT_DOMAIN_PER_WINDOW |
integer | 10 |
单 domain 每窗口最大查询数,范围 1–1000 |
MIIT_RATE_LIMIT_DOMAIN_WINDOW_SECONDS |
integer | 120 |
domain 限流窗口大小(秒),范围 1–86400 |
MIIT_RATE_LIMIT_DOMAIN_COOLDOWN_SECONDS |
integer | 60 |
单 domain 上游失败后冷却时长(秒),范围 1–3600 |
MIIT_RATE_LIMIT_GLOBAL_COOLDOWN_SECONDS |
integer | 10 |
全局冷却时长(秒),范围 1–3600 |
MIIT_RATE_LIMIT_DOMAIN_WAIT_TIMEOUT_SECONDS |
integer | 3 |
singleflight 等待窗口(秒),范围 0–10 |
MIIT_RATE_LIMIT_DOMAIN_WAIT_INTERVAL_MILLISECONDS |
integer | 250 |
singleflight 等待期间缓存轮询间隔(毫秒),范围 10–1000 |
MIIT_API_KEY_ENABLED |
boolean | false |
是否开启 API key 鉴权 |
MIIT_API_KEY |
string | 无 | API key 值,开启鉴权时必填 |
MIIT_DEBUG_ENABLED |
boolean | false |
是否启用流程调试日志 |
MIIT_DEBUG_STORE_CAPTCHA_SAMPLES |
boolean | false |
是否保存验证码 challenge 样本到磁盘,仅 debug.enabled=true 时生效 |
MIIT_LOG_MAX_DETAIL_LENGTH |
integer | 512 |
日志详情最大截断长度,范围 64–4096 |
MIIT_STORAGE_BACKEND |
string | file |
存储后端,可选 file 或 redis |
MIIT_STORAGE_REDIS_URL |
string | redis://127.0.0.1:6379 |
Redis 连接地址 |
MIIT_STORAGE_REDIS_KEY_PREFIX |
string | miit: |
Redis key 前缀,用于命名空间隔离 |
MIIT_STORAGE_REDIS_CONNECT_TIMEOUT |
integer | 3000 |
Redis 连接超时(毫秒) |
参见项目根目录下的 .env.example 文件,也可直接参考以下内容:
# 复制此文件为 .env 并按需修改
# 所有变量均为可选,未设置时使用代码内置默认值
# 服务监听
HOST=127.0.0.1
PORT=8080
# 缓存
MIIT_CACHE_SCHEMA_VERSION=v1
MIIT_CACHE_SUCCESS_TTL=259200
MIIT_CACHE_MISS_TTL=43200
MIIT_CACHE_LIST_TTL=86400
MIIT_CACHE_SUCCESS_STALE_TTL=604800
MIIT_CACHE_MISS_STALE_TTL=86400
# 限流
MIIT_RATE_LIMIT_GLOBAL_QPS=5
MIIT_RATE_LIMIT_IP_PER_MINUTE=60
MIIT_RATE_LIMIT_DOMAIN_PER_WINDOW=10
MIIT_RATE_LIMIT_DOMAIN_WINDOW_SECONDS=120
MIIT_RATE_LIMIT_DOMAIN_COOLDOWN_SECONDS=60
MIIT_RATE_LIMIT_GLOBAL_COOLDOWN_SECONDS=10
MIIT_RATE_LIMIT_DOMAIN_WAIT_TIMEOUT_SECONDS=3
MIIT_RATE_LIMIT_DOMAIN_WAIT_INTERVAL_MILLISECONDS=250
# 鉴权
MIIT_API_KEY_ENABLED=false
MIIT_API_KEY=
# 调试
MIIT_DEBUG_ENABLED=false
MIIT_DEBUG_STORE_CAPTCHA_SAMPLES=false
# 日志
MIIT_LOG_MAX_DETAIL_LENGTH=512
# 存储
MIIT_STORAGE_BACKEND=file
MIIT_STORAGE_REDIS_URL=redis://127.0.0.1:6379
MIIT_STORAGE_REDIS_KEY_PREFIX=miit:
MIIT_STORAGE_REDIS_CONNECT_TIMEOUT=3000- 单机低流量场景:适当提高
MIIT_CACHE_SUCCESS_TTL,降低MIIT_RATE_LIMIT_GLOBAL_QPS,优先保护上游。 - 内网受控调用场景:适当提高
MIIT_RATE_LIMIT_IP_PER_MINUTE,保持较短MIIT_RATE_LIMIT_DOMAIN_COOLDOWN_SECONDS。 - 上游明显风控:调大
MIIT_RATE_LIMIT_DOMAIN_COOLDOWN_SECONDS和MIIT_RATE_LIMIT_GLOBAL_COOLDOWN_SECONDS,同时降低MIIT_RATE_LIMIT_GLOBAL_QPS和MIIT_RATE_LIMIT_DOMAIN_PER_WINDOW。 - 并发等待过多:缩短
MIIT_RATE_LIMIT_DOMAIN_WAIT_TIMEOUT_SECONDS或增大MIIT_RATE_LIMIT_DOMAIN_WAIT_INTERVAL_MILLISECONDS。 - 日志膨胀明显:降低
MIIT_LOG_MAX_DETAIL_LENGTH。
Method:
GET /Query Parameters(三选一):
-
domain要查询的域名。系统会自动执行规范化与格式校验。 -
unitName按主体名(公司/组织全称)精确查询该主体下的所有备案域名/APP 信息(由于个人主体有重名可能性,所以不建议用于个人备案查询,仅适用于企业/组织为主体的查询)。 -
licence按备案号查询。支持两种格式:- 主体备案号(如
粤ICP备2025407341号):返回该主体下的所有域名列表,与unitName等效。 - 网站备案号(如
粤ICP备2025407341号-1):返回该网站对应的单条域名详情,与domain等效且共享缓存。
- 主体备案号(如
三个参数互斥,优先级为 unitName > licence > domain。
HTTP status: 200
实时查询(cache 为 miss):
{
"code": 200,
"message": "successful",
"cache": "miss",
"duration": "1523ms",
"data": {
"Domain": "baidu.com",
"UnitName": "北京百度网讯科技有限公司",
"MainLicence": "京ICP证030173号",
"ServiceLicence": "京ICP证030173号-1",
"NatureName": "企业",
"LeaderName": "李彦宏",
"UpdateRecordTime": "2026-01-01 00:00:00"
}
}命中缓存(cache 为 hit),额外返回 cached_at 和 cache_expires_at:
{
"code": 200,
"message": "successful",
"cache": "hit",
"cached_at": "2026-05-03T20:00:00.000+08:00",
"cache_expires_at": "2026-05-04T20:00:00.000+08:00",
"duration": "3ms",
"data": {
"Domain": "baidu.com",
"UnitName": "北京百度网讯科技有限公司",
"MainLicence": "京ICP证030173号",
"ServiceLicence": "京ICP证030173号-1",
"NatureName": "企业",
"LeaderName": "李彦宏",
"UpdateRecordTime": "2026-01-01 00:00:00"
}
}HTTP status: 200
{
"code": 200,
"message": "successful",
"cache": "miss",
"duration": "1523ms",
"data": {
"UnitName": "东莞市云上望远网络科技有限公司",
"MainLicence": "粤ICP备2025407341号",
"NatureName": "企业",
"LeaderName": "",
"UpdateRecordTime": "2025-09-01 08:57:56",
"Total": 8,
"Records": [
{ "domain": "yunvon.cn", "serviceLicence": "粤ICP备2025407341号-1" },
{ "domain": "yunvon.com", "serviceLicence": "粤ICP备2025407341号-4" },
{ "domain": "uciu.cn", "serviceLicence": "粤ICP备2025407341号-6" },
{ "domain": "iusi.cn", "serviceLicence": "粤ICP备2025407341号-2" }
]
}
}data 中 UnitName、MainLicence 等为主体公共信息,Records 为该主体下所有域名列表,每条包含 domain 和 serviceLicence。
HTTP status: 200
{
"code": 200,
"message": "successful",
"cache": "miss",
"duration": "642ms",
"data": {
"Domain": "yunvon.cn",
"UnitName": "东莞市云上望远网络科技有限公司",
"MainLicence": "粤ICP备2025407341号",
"ServiceLicence": "粤ICP备2025407341号-1",
"NatureName": "企业",
"LeaderName": "",
"UpdateRecordTime": "2025-09-01 08:57:56"
}
}网站备案号查询返回单条域名详情,与 domain 查询共享缓存。例如先查询 ?licence=粤ICP备2025407341号-1 后,再查询 ?domain=yunvon.cn 会直接命中缓存。
参数非法时,HTTP status: 400
{
"code": 400,
"message": "domain format is invalid",
"data": null
}域名误传入 unitName 或 licence 参数时,HTTP status: 400
{
"code": 400,
"message": "domain format input is not allowed for unitName/licence, use the domain parameter instead",
"data": null
}API key 无效或缺失时,HTTP status: 401(仅当开启 API key 鉴权时)。可通过 api_key 查询参数或 x-api-key 请求头提供。
{
"code": 401,
"message": "unauthorized",
"data": {
"domain": "",
"detail": "invalid or missing API key"
}
}未找到备案记录时,HTTP status: 404
{
"code": 404,
"message": "no ICP record found",
"data": {
"domain": "example.com",
"detail": "no ICP record found for example.com"
}
}请求过于频繁或处于冷却中时,HTTP status: 429
{
"code": 429,
"message": "too many requests",
"data": {
"domain": "example.com"
}
}上游接口异常或被风控等系统错误时,HTTP status: 500
{
"code": 500,
"message": "upstream query failed",
"data": {
"domain": "example.com",
"detail": "upstream query failed"
}
}本地存储或环境未就绪时,HTTP status: 500
{
"code": 500,
"message": "service environment is not ready",
"data": {
"domain": "example.com",
"detail": "service environment is not ready"
}
}内部错误时,HTTP status: 500
{
"code": 500,
"message": "internal server error",
"data": {
"domain": "example.com",
"detail": "the service encountered an internal error"
}
}data 字段基于工信部详情接口响应中的 params 进行映射:
domain->DomainunitName->UnitNamemainLicence->MainLicenceserviceLicence->ServiceLicencenatureName->NatureNameleaderName->LeaderNameupdateRecordTime->UpdateRecordTime
data 字段包含主体公共信息和域名列表:
unitName->UnitName(主体名)mainLicence->MainLicence(主体备案号)natureName->NatureName(主体性质)leaderName->LeaderName(负责人)updateRecordTime->UpdateRecordTime(最近更新时间)total->Total(域名总数)records->Records(域名列表,每条包含domain和serviceLicence)
cache 字段位于响应顶层(与 code、message、data 同级),值为 hit 表示命中缓存,miss 表示实时查询。当 cache 为 hit 时,同级还会返回 cached_at(ISO 8601 北京时间),表示缓存写入时间;cache_expires_at 表示缓存过期时间。duration 为查询耗时(毫秒),所有响应均包含。
为了降低上游风控风险,当前版本增加了治理层:
- domain 参数进入主链路前会做规范化与格式校验。
EnvironmentGuard会在入口层主动检查 Node 版本(>=18.18)和sharp可用性,避免服务运行到中途才因环境缺陷崩溃。- 缓存优先于上游频控,缓存命中不会消耗上游配额。
- 同一 domain 的并发请求先竞争 singleflight 锁,只有真正准备访问上游的请求才在锁内执行频控计数,避免“未出站先扣额度”的限流语义污染。
- 全局、IP、domain 限流都通过
AppConfig配置化,而不是硬编码在业务逻辑里,并支持通过环境变量覆盖默认值。 AppConfig会对 TTL、QPS、等待时间、冷却时间、日志长度等整数配置做边界夹紧,防止 0、负数或异常大值导致全量拒绝、无限近似放行或 worker 长时间占用。QueryGuard通过consumeAll()一次性消费多维限流状态,避免 global 先扣、IP 后失败、domain 再失败时的计数污染。- 上游失败后会进入短暂冷却,避免连续打上游。
FileRateLimiter的频控窗口文件和 cooldown 文件使用mkdir原子文件锁保护写入,读取 cooldown 时也加锁避免 TOCTOU 竞争。consumeAll()在多文件加锁过程中使用单独的已加锁句柄列表,即使中途打开或加锁失败,也会在finally中释放已持有的锁,避免锁泄漏。FileCache读取缓存时使用共享锁,写入缓存时使用独占锁,并校验编码、截断、写入长度和 flush 完整性,避免并发读写读到半写入内容。FileCache和FileRateLimiter继承FileStoreBase共享轻量级随机 GC,通过shouldDelete回调控制各自的删除策略;GC 只在拿到非阻塞独占锁后才会清理文件,避免并发删除其他请求正在使用的状态文件。- 同域请求在拿到 domain 锁后会再次读取 success/miss cache,降低锁等待期间的重复上游访问。
- 同域等待窗口不再固定为 2 秒,而是通过配置化的超时和轮询间隔控制;当前默认值已收紧为更保守的等待窗口,以减少 worker 长时间占用。
- 成功结果默认缓存 24 小时。
- 无备案记录默认缓存 30 分钟,但只有“列表接口成功且真正无记录”的
404才默认写入 miss cache;精确匹配失败产生的404已标记为不可缓存,避免字段名或上游格式差异放大为持续的错误缓存。 - 缓存条目携带
_schema_version,未来响应结构变化时可以通过版本变更使旧缓存自动失效。 - 验证码求解会优先从
bigImage的topHint区域自适应采样实际缺口背景色,并配置 4 组预置颜色回退轮询;同时将模板搜索改为两步粗精扫描(粗搜 step=12 选 top-3 → 精搜 ±12px step=3),降低事件循环阻塞时间。 - 由于当前上游在同一个
captchaUUID首次校验失败后通常会直接把后续提交判定为过期,CaptchaSolver现在改为每个 challenge 只提交一个候选;一旦失败,立即获取新的 challenge,并轮换到下一类候选假设继续尝试。 - 验证码求解成功后,业务层会写回实际成功 challenge 对应的
Uuid和Sign,避免内部重试获取新 challenge 后 header 仍沿用第一次 uuid 的状态错配问题。 - 上游接口调用(auth、getCheckImagePoint、queryByCondition、queryDetail)均带指数退避重试(500ms → 1000ms → 2000ms,最多 3 次),应对工信部服务端的临时性波动。
MiitQueryService对列表结果不再机械相信第一条,而是优先寻找与查询 domain 精确匹配且具备有效标识符的项;找不到精确匹配时返回404,避免错误主体写入成功缓存。MiitQueryService会兼容常见标识符字段变体,例如mainID、main_id、ids.mainId;如果上游列表项已经包含完整详情字段,则可在标识符缺失或详情接口异常时使用列表项兜底。- 错误被分成参数错误、频控错误、存储错误、环境错误、上游错误和内部错误,不再把所有异常粗暴归类为上游失败。
AuthApi、CaptchaApi、MiitClient和MiitQueryService中的上游协议错误统一升级为UpstreamException,保证冷却策略只针对真正的上游故障触发。- 验证码识别失败、图片解码失败、
checkImage偏移尝试耗尽等路径也统一归类为UpstreamException,不再错误落入内部错误分支。 MiitClient会截断写入日志的上游错误详情,防止异常响应体无限放大日志体积。DetailSanitizer做字符串截断,防止异常响应体无限放大日志体积。Logger采用 best-effort 策略,日志目录不可写时会降级尝试写入process.stderr,不会再向外抛异常。Debug会把流程诊断写入结构化日志,并同步尝试写入 stderr;HTTP 响应仍保持错误脱敏,不会因为开启 debug 而暴露内部异常。JsonResponse会检查JSON.stringify()结果,编码失败时输出保底 JSON,并同步把 HTTP 状态修正为500,避免 HTTP 状态和 JSON body code 矛盾。ResponseFormatter会校验必填详情字段,不再用空字符串静默吞掉字段缺失;同时先格式化、后写成功缓存,避免不可渲染数据进入 success cache。- storage 目录在运行时会校验可创建、可写,避免限流与缓存静默失效。
- 初始化阶段异常也会进入统一 JSON 错误出口,避免 API 返回非 JSON 错误页。
- unitName/licence 列表查询复用 domain 查询的全部保护措施:IP + 全局限流(QueryGuard)、成功缓存(
list:{keyword})、singleflight 锁(DomainQueryLock)、等待锁期间轮询缓存。 - list 查询成功后自动将结果写入
list:{unitName}和list:{mainLicence}两个缓存 key,并逐条填充success:{domain}缓存;后续 domain 查询直接命中缓存,无需验证码和上游请求。 - 每条请求通过
respond闭包自动向终端(stdout)输出 access log,格式为[ISO北京时间] IP 查询关键字 HTTP状态 业务码 缓存状态 耗时。
当前版本沿用了以下几个核心策略:
-
使用固定请求头模拟浏览器环境。
-
使用 Cookie 持续维持服务端会话状态。
-
使用
md5("testtest" + timestamp)构造authKey。 -
使用验证码大图中的缺口区域进行本地识别。
-
验证码候选横坐标会优先来自
bigImage中灰色缺口块的本地检测,并辅以模板对比和estimate近似兜底生成备选坐标;缺口颜色通过自适应采样 + 4 组预置色域回退轮询确定,不再依赖单一硬编码颜色;模板匹配使用两步粗精搜索降量约 90%。每个 challenge 只提交一个当前最优候选,如果失败则重新获取新的验证码并切换到下一类候选继续尝试,以规避上游对同一 challenge 二次提交直接判定过期的行为。 -
列表查询后优先进行精确匹配,并优先选择具备有效详情标识符的候选项,而不是盲目回退第一条结果。
-
只有在列表接口本身成功且结果为空,或精确匹配失败时,才返回
404;其中精确匹配失败默认不会写入 miss cache。 -
如果列表候选项已经包含完整成功响应所需字段,详情标识符缺失或详情接口异常时可以回退使用该列表项。
-
上游异常、签名失效、鉴权失败、风控等情况统一落到
UpstreamException路径,而本地存储与环境问题会走不同分类;上游调用均带指数退避重试,应对临时性波动。 -
入口层的组件初始化、环境预检、缓存、锁、限流和查询都走统一异常出口。
-
日志系统是辅助能力,失败时不会反向影响主响应契约。
-
调试输出默认关闭,是否启用只由配置文件或环境变量控制,不再接受 URL 参数切换。
-
当前
EnvironmentGuardTest会真实调用EnvironmentGuard.assertRuntimeReady(),根据当前环境中的 Node 版本断言预检行为。 -
服务支持 SIGTERM / SIGINT 优雅关闭,关闭时会等待 HTTP 连接排空并断开 Redis 连接;日志按日期轮转并自动清理 7 天前的文件;
storage/debug/captcha/最多保留 20 个样本目录,防止磁盘无限增长。 -
请求处理管线中的
AppConfig、CacheStore、RateLimiter、LockProvider使用模块级懒加载单例,避免每请求重复创建对象和重复执行配置解析。handleError路径复用同一AppConfig实例,不再每次新建。 -
HTTP 服务器设置了 30 秒请求超时、15 秒 header 超时和 10 秒 keep-alive 超时,防止慢客户端无限占用连接。
项目会在运行时自动创建 storage/ 目录,用于:
-
storage/cache/保存成功缓存、空结果缓存和列表缓存。文件名为 SHA1 哈希,通过 schema version 前缀隔离。list 查询结果会自动填充 domain 缓存(success:{domain}),unitName 和 licence 查询结果互相写入对方缓存 key(list:{unitName}↔list:{mainLicence})。 -
storage/ratelimit/保存频控窗口与冷却状态。 -
storage/locks/保存 singleflight 锁文件。 -
storage/logs/保存结构化错误日志和 debug 诊断日志。 -
storage/debug/captcha/当同时开启debug.enabled=true和debug.store_captcha_samples=true时,保存验证码 challenge 的调试样本,包括big.png、small.png和metadata.json。
这些文件都是本地文件实现。当前默认实现适合单机部署;缓存、限流和锁模块均通过内部接口隔离,可替换为 Redis 等共享存储后端。
当 storage.backend 设为 redis 时,缓存、限流和分布式锁均切换为 Redis 实现:
- 缓存:使用双层 TTL 策略。Redis 键的物理 TTL 为
success_stale_ttl(默认 7 天),逻辑过期由success_ttl(默认 24 小时)控制。过期后数据仍保留在 Redis 中,上游故障时可作为 stale 降级响应返回。 - 限流:基于 Lua 脚本实现固定窗口原子计数,窗口对齐到
floor(now/window)*window,与文件限流器语义一致。支持 domain、全局 QPS 和 IP 三个维度的原子多规则消费。 - 分布式锁:使用
SET NX PX实现互斥,TTL 60 秒,带 watchdog 定时续期(每 30 秒),acquire()设 15 秒超时上限。释放使用 Lua 脚本保证仅释放自己持有的锁。 - 连接管理:Redis 客户端使用 Promise 单例模式,避免并发请求重复创建连接。注册
error事件监听防止连接断开时未捕获异常。服务关闭时通过quit()优雅断开连接。enableOfflineQueue=false防止 Redis 不可用时命令队列无限增长。
项目默认通过 .gitignore 忽略 storage/ 下的运行产物,并使用 .gitkeep 保留必要目录结构。
测试目录中的缓存版本测试会写入 storage/test-cache/,并在测试结束后主动清理该目录中的文件和目录本身,降低测试对运行目录的污染。
该项目本质上仍然依赖目标站点当前的协议和验证码样式,因此存在天然脆弱性。
主要限制包括:
- 如果工信部接口字段发生变化,服务可能失效。
- 如果验证码颜色、形状或返回数据结构改变,识别逻辑可能失效。
- 当前缓存、锁和频控默认基于本地文件实现,锁使用
mkdir原子操作保证跨进程互斥。可通过storage.backend=redis切换为 Redis 后端,支持分布式部署。 - 列表结果虽然增加了精确匹配、有效标识符优先和列表详情兜底,但仍受上游字段质量影响。
- 上游失败时会优先回放已过期的 stale cache 作为降级数据,标记
stale: true。在 Redis 模式下,通过双层 TTL 策略保证过期数据在success_stale_ttl时间窗口内仍可被降级读取。 - 同域 singleflight 当前是等待后回读缓存的模式,不是长轮询队列或作业系统。
- 仓库附带了
package.json和基础测试骨架,测试使用 Node 内置node:test运行。 - 当前测试已覆盖域名规范化、环境预检行为、缓存版本、响应字段完整性、配置边界、
404可缓存标志、列表候选项选择、标识符字段变体、列表详情兜底,以及验证码偏移展开顺序规则,但仍不足以替代完整的并发、锁竞争和真实上游集成测试。
这意味着该项目更适合作为特定场景下的工程化工具,而非长期稳定的官方兼容方案。
当配置文件中的 debug.enabled=true 时,服务会把流程日志写入 storage/logs/app-YYYY-MM-DD.log,并同步尝试输出到 stderr。HTTP 响应仍然保持脱敏,不会因为开启 debug 而把内部异常直接返回给浏览器。
常见流程日志包括:
step=authstep=getCheckImagePointstep=detect method=...step=checkImage attempt_left=...step=checkImage rejectedstep=getCheckImagePoint retrystep=querystep=queryByCondition success=truestep=queryByCondition selected_matchstep=queryByCondition exact_matches_without_valid_identifiersstep=queryByCondition missing_valid_identifiersstep=queryDetailstep=queryByCondition fallback=list_item_detailstep=queryDetail fallback=list_item_detail
queryByCondition 相关 debug 日志会记录列表数量、候选项 key、原始标识符值和归一化后的标识符,用于排查上游字段变更、列表项缺少 mainId / domainId / serviceId、详情接口不可用等问题。
验证码相关 debug 日志会记录本轮选中的检测方法、候选坐标、候选排序、当前验证码尝试序号、是否使用自适应颜色采样(sampled: true),以及每次 checkImage 拒绝时的上游返回码和消息,用于排查灰色缺口检测失效、模板候选偏差、left=0 异常值主导和验证码窗口过期等问题。
如果同时开启 debug.enabled=true 和 debug.store_captcha_samples=true,服务会把当前 challenge 的 big.png、small.png 和 metadata.json 落盘到 storage/debug/captcha/,用于离线比对真实 challenge。
服务端详细错误和 debug 诊断都会写入 storage/logs/,用于排查验证码识别失败、接口返回异常、频控触发、上游风控和列表字段结构变化问题。
基础测试骨架可在具备 Node.js 环境的情况下运行:
node tests/run.js