
Release 包在开发机上一切正常审核环境却卡在登录页。最后发现不是接口逻辑而是提审域名少了一段中间证书链另一个备用地址还保留着http://。这些问题等审核反馈再处理代价往往是一整轮版本等待。TlsGate 把应用真实使用的端点抽成清单在 Hvigor Release 构建前检查协议、证书链和剩余有效期。一、浏览器能开不代表审核设备一定能连这次故障来自一套审核专用环境。开发团队日常访问api.example.com提审包则切到review-api.example.com。运维更新证书时只在主域名补齐了中间证书部分浏览器能依靠缓存自动补链干净设备却返回 TLS 握手失败。为了“临时兜底”旧配置里还留着一个http://review-fallback.example。开发阶段没有人注意到因为模拟器长期使用同一网络环境主接口也没有故障。审核设备第一次安装、没有历史证书缓存又恰好走审核端点问题才完整暴露。日志只看到NETWORK_UNAVAILABLE看不出是 DNS、握手还是接口返回。我没有继续往登录页里加重试而是做了一个发布前工具TlsGate。它读取应用自己的端点清单对 Release 环境的 6 个地址逐一检查是否 HTTPS、证书是否被系统信任、证书链是否完整、剩余天数是否满足阈值、最终跳转有没有降级到 HTTP。固定批次为REL-1002结果PASS4、WARN1、BLOCK1发布状态RELEASE_BLOCKED阻断原因TLS_CHAIN_INCOMPLETE。二、端点先从代码里收口如果域名散落在 ArkTS、JSON 和三方 SDK 初始化代码里脚本只能靠正则猜漏检是迟早的事。第一步不是写 TLS 探针而是让应用和门禁读取同一份端点事实。当前问题是 Debug、Review、Release 三套环境不能自由混用Release 产物也不能回退到明文地址。下面的注册表把用途和环境一起声明。exporttypeEndpointEnvDEBUG|REVIEW|RELEASEexportinterfaceEndpointSpec{name:stringenv:EndpointEnv url:stringrequired:boolean}exportconstENDPOINTS:EndpointSpec[][{name:login,env:REVIEW,url:https://review-api.example.com,required:true},{name:upload,env:REVIEW,url:https://review-upload.example.com,required:true},{name:notice,env:REVIEW,url:https://review-notice.example.com,required:false}]exportfunctionassertReleaseEndpoint(spec:EndpointSpec):void{constparsednewURL(spec.url)if(parsed.protocol!https:)thrownewError(PLAINTEXT_ENDPOINT:${spec.name})if(parsed.hostname.includes(localhost))thrownewError(LOCAL_ENDPOINT:${spec.name})}应用启动时按当前构建环境选取端点CI 工具则读取同一份导出数据。assertReleaseEndpoint()只做本地静态判断不发网络请求能够立即拦住 HTTP 和 localhost。当前 Demo 用.example域名展示不代表真实项目应硬编码正式工程可以在构建阶段生成只读清单但生成结果必须进入产物证据。不要在接口失败后自动把 HTTPS 替换成 HTTP这种“降级”会把证书问题变成明文传输问题。端点注册表是长期对象没有资源释放风险在于业务绕过注册表拼接新域名因此代码检查还应扫描 Network Kit 调用点是否引用统一客户端。三、TLS 探针要看授权结果也要保存链路证据门禁运行在构建机上使用 Node.jstls模块发起握手。只读取叶子证书的到期日不够证书仍在有效期内链条缺少中间证书一样会在干净设备失败。探针需要记录authorized、authorizationError、有效期、指纹和颁发者。当前问题是每次连接都必须有超时并可靠销毁 Socket否则 6 个端点里一个无响应就能拖死流水线。importtlsfromnode:tlsexportasyncfunctionprobeTls(host:string,port:number443):PromiseTlsFact{returnnewPromise((resolve,reject){constsockettls.connect({host,port,servername:host,rejectUnauthorized:true})consttimersetTimeout(()socket.destroy(newError(TLS_TIMEOUT)),5000)socket.once(secureConnect,(){constcertsocket.getPeerCertificate(true)clearTimeout(timer)constfact:TlsFact{host,authorized:socket.authorized,authorizationError:socket.authorizationError??,validTo:cert.valid_to,fingerprint256:cert.fingerprint256,issuer:cert.issuer?.O??}socket.end()resolve(fact)})socket.once(error,(error:Error){clearTimeout(timer)reject(error)})})}方法在 Release 门禁阶段对每个 HTTPS 端点执行。servername显式传入 SNI避免多域名证书返回错误站点。rejectUnauthorizedtrue让 Node 使用可信根校验链路若缺少中间证书Promise 进入 error 分支并保留错误码。每次成功或失败都要清理定时器Socket 用end()或destroy()结束。正式工具还应限制并发避免同时探测几十个域名触发安全设备限流。不要把完整证书或任何密钥写入日志只保存公开指纹、颁发者、有效期和错误码。网络抖动可重试一次但相同链路错误不能用无限重试掩盖。四、有效期不是“过期/没过期”两档证书剩余 9 天时今天能连审核周期和灰度发布却可能跨过到期日。TlsGate 定义两条阈值少于 30 天给 WARN少于 14 天直接 BLOCK链路不可信、明文地址和 HTTPS 跳回 HTTP 也直接阻断。当前问题是把探针事实转换成稳定的发布判定不能在日志里靠人肉找日期。exporttypeGateLevelPASS|WARN|BLOCKexportfunctionevaluateTls(fact:TlsFact,nowMs:number):GateFinding{if(!fact.authorized){return{host:fact.host,level:BLOCK,code:TLS_CHAIN_INCOMPLETE}}constremainDaysMath.floor((Date.parse(fact.validTo)-nowMs)/86400000)if(remainDays14){return{host:fact.host,level:BLOCK,code:CERT_EXPIRING,remainDays}}if(remainDays30){return{host:fact.host,level:WARN,code:CERT_RENEW_SOON,remainDays}}return{host:fact.host,level:PASS,code:TLS_OK,remainDays}}REL-1002中review-api.example.com因链路不完整被 BLOCK另一个端点剩余 24 天被标记 WARN其余 4 个通过。只要存在 required 端点 BLOCK最终状态就是RELEASE_BLOCKED。阈值属于发布策略不应散落在脚本里正式项目可放入版本化配置。86400000只用于证书 UTC 时间差不做日历日期推算因此不会遇到业务时区边界。还要区分“探针所在构建机不信任企业私有根”和“公网链路真的不完整”审核环境使用公网域名时应以公开信任链为准。五、重定向也可能把 HTTPS 偷偷降级TLS 握手通过并不代表最终请求仍是 HTTPS。某些旧网关把/login302 到明文地址普通接口测试只看最终 200忽略了中间跳转。TlsGate 因此逐跳记录 Location并限制最多 5 次。当前问题是发现任何https → http跳转就立即阻断同时避免重定向环路。exportasyncfunctioninspectRedirects(startUrl:string):PromiseRedirectFact{letcurrentnewURL(startUrl)constvisitednewSetstring()for(lethop0;hop5;hop){if(visited.has(current.href))thrownewError(REDIRECT_LOOP)visited.add(current.href)constresponseawaitrequestHead(current)constlocationresponse.headers.locationif(!location)return{finalUrl:current.href,hops:[...visited]}constnextnewURL(location,current)if(current.protocolhttps:next.protocolhttp:){thrownewError(HTTPS_DOWNGRADE:${current.host})}currentnext}thrownewError(REDIRECT_LIMIT_EXCEEDED)}方法只对清单中的健康检查路径执行 HEAD 请求不抓取用户数据。每次跳转都放进visited既用于证据也用于检测环路。失败后记录起点、跳数和错误码不记录查询参数避免把临时令牌带入构建日志。requestHead()要设置连接和响应超时并在异常时释放请求资源。部分服务不支持 HEAD可在配置中改用不带敏感参数的 GET 健康检查路径。门禁不应该扫描任意互联网地址只访问项目明确授权的端点。六、把门禁接到 Release而不是提醒群消息脚本验证完成后我把它接到 Release 构建前置任务。Debug 构建不阻断Release 与 Review 构建必须产生 JSON 证据存在 BLOCK 时用非零退出码停止产物发布。聚合逻辑很简单6 个端点依次探测输出pass4 warn1 block1证据文件记录批次REL-1002、扫描时间、域名、公开指纹、剩余天数与判定码。证据中不包含 Cookie、Authorization 或响应正文。项目目录为entry/src/main/ets/network/EndpointRegistry.ets、scripts/tls/probeTls.mjs、scripts/tls/inspectRedirects.mjs、scripts/tls/evaluateTls.mjs、config/tls-gate.json与build-profile.json5。这样应用端点与工具代码职责清晰。DevEco Studio 图右侧模拟器显示 TlsGate 的扫描结果中间是evaluateTls()底部 HiLog/构建日志明确显示REL-1002 pass4 warn1 block1和RELEASE_BLOCKED TLS_CHAIN_INCOMPLETE。红圈只标注阻断端点和最终退出码。七、手机端复现的是审核环境不是后台报表为了让开发同学在真机上复核我做了一个只在内部构建开放的Review Network Check页面。它显示 6 个端点的摘要不展示证书完整内容也不允许修改域名。主域名 4 个通过1 个即将续期review-api.example.com因链路不完整被阻断。页面状态为RELEASE_BLOCKED批次REL-1002错误TLS_CHAIN_INCOMPLETE。它的价值是证明审核设备会走哪套环境并把“网络不可用”细化为可以行动的证书问题。正式商店包应移除诊断入口门禁证据保留在 CI而不是暴露给普通用户。八、结语上架网络问题经常被误认为“审核环境偶发”其实大多可以在发布前变成确定检查。端点先收口TLS 探针验证可信链和有效期重定向检查阻止明文回退Release 任务再用非零退出码把问题挡在产物之前。TlsGate 也有边界它不能替代服务端安全评估不能预测审核网络的所有差异更不能通过关闭证书校验来换取通过。它解决的是最基础、也最容易被遗漏的一层——确保提审包实际访问的每个必要端点在一台干净设备上仍有完整、可信、不会突然过期的 HTTPS 路径。