
上周帮朋友排查了一个挺典型的故障他们内部管理系统原本跑在 http 上文件在线预览用的是 kkfilekkFileView上线两年一直没出过问题。后来整个系统升级成 https登录、接口、下载都正常唯独点开附件那一瞬间预览区域一片空白控制台里刷出一行红色报错。这个场景其实非常有代表性——kkfile 配置 https 预览文件难点从来不在证书怎么申请而在整条预览链路上有四五个环节各自带着自己的协议和地址只要有一处还是 http浏览器就会把它拦掉。这篇就把我实际处理过的几种部署形态、配置项改法、以及踩过的坑完整梳理一遍适合正在做内网系统 https 改造的后端、运维以及需要把 kkfile 嵌进业务系统的前端同学参考从零开始跟着走也能跑通。1. 先搞清楚一次预览请求在链路上要经过哪几段 HTTPS 检查1.1 从点击附件到看见内容中间发生了什么很多人一上来就问kkfile 怎么开 https其实这个问题问得不够精确。真正需要回答的是这次请求里哪些地址是由浏览器发起的哪些是由服务器发起的。这两种发起方对 https 的要求完全不一样。一次完整的在线预览大致是这样流动的用户在业务系统里点击某个.docx附件前端拿到这个文件的访问地址按 kkfile 的规则拼出一个预览入口地址然后通过 iframe 或者新窗口打开它kkfile 收到请求后先去把源文件下载到本地如果传给它的就是一个远程 URL再调用 LibreOffice 之类的转换引擎把它渲染成 PDF 或者图片落到file.dir指定的缓存目录最后浏览器再去拉取这份转换后的结果文件用 pdf.js 之类的组件渲染出画面。这四步里浏览器发起的请求有两段一是加载 kkfile 的预览入口页面二是拉取转换后的结果文件服务器发起的请求有一段kkfile 自己去下载源文件。还有一个隐形的第五段如果结果文件在返回的 HTML 里是以绝对地址写死的那这个绝对地址的协议也必须跟当前页面一致。把这几段分开看问题的定位范围立刻就小了一大半。1.2 混合内容拦截才是预览空白的第一号原因如果你的业务系统已经是https://oa.example.com而 kkfile 的入口地址还是http://10.0.0.20:8012那浏览器会直接判定这是Mixed Content混合内容。早期浏览器对 http 的图片、脚本还比较宽容只给个黄色警告现在的主流浏览器早就把 http 的 iframe 归到主动混合内容里直接阻断加载页面控制台里会出现类似Mixed Content: The page at https://... was loaded over HTTPS, but requested an insecure frame http://...的提示。表现就是外层的业务页面好好的中间那块预览区域白茫茫一片什么错误都不显示。还有一种更隐蔽的情况入口页面本身已经是 https 了但 kkfile 返回的结果文件地址仍然是 http。这时候页面能打开pdf.js 的容器也在就是一直转圈或者报文件加载失败。原因在于 kkfile 内部会根据配置项base.url拼接出返回给前端的预览地址如果你的base.url还写着http://127.0.0.1:8012那它返回的就是 http 地址浏览器一样拦。这两个坑经常同时出现而且互相掩盖所以排查时一定要分开验证。1.3 证书不信任只在服务器主动拉取文件时才会发作第三种失败模式跟浏览器无关纯粹是 Java 侧的问题。当你在预览地址里传的是一个远程文件 URL并且那个文件服务器用的是自签证书或者企业内部 CA 签发的证书时kkfile 在下载阶段会抛出javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: unable to find valid certification path to requested target这个报错的意思是JVM 的信任库通常是$JAVA_HOME/lib/security/cacerts里没有对方证书链上的根证书。它跟浏览器里点继续访问完全是两回事浏览器认了不代表 Java 认。解决办法是把根证书导入信任库后面第 6 节会给出具体命令。顺便提醒一句如果你的 kkfile 发行包里自带了一份 jre 目录部分打包版本确实这么干那证书必须导入到它实际使用的那个 jre里导到系统的 JDK 上是没用的这个细节我见过至少两个人卡了大半天。2. 两种 HTTPS 落地方案先把选型定下来再动手2.1 方案 A让 kkfile 自己扛证书Spring Boot 内嵌 Tomcat 原生就支持开 SSL只需要在配置文件里加几行server.ssl.enabledtrue server.ssl.key-store/opt/kkfile/config/keystore.p12 server.ssl.key-store-password你的密码 server.ssl.key-store-typePKCS12 server.ssl.key-aliaskkfile证书文件用 keytool 从现有的 pem 转出来就行openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem \ -out keystore.p12 -name kkfile -CAfile chain.pem -caname root keytool -list -v -keystore keystore.p12 -storetype PKCS12这个方案的好处是链路短没有中间层配置项少坏处也很明显证书续期要重启服务多个节点要各自维护一份证书而且如果前面还有别的网关端口和协议会打架。所以它更适合单机部署、内部测试环境、或者只有一台机器的小团队。2.2 方案 B前置 Nginx 终止 TLS转发到本机 8012推荐绝大多数生产环境走的是这条路Nginx 监听 443把证书和 TLS 握手全部接管然后以明文 http 转发给本机127.0.0.1:8012上的 kkfile。浏览器看到的是 httpskkfile 自己还是 http两边都省心。证书续期只需要 reload Nginx不动 Java 进程将来要加节点Nginx 那层做负载均衡就行。但这条路有一个必须记住的前提Nginx 转发时要把X-Forwarded-Proto这个头带过去否则后端以为自己还是被 http 访问的某些场景下生成的重定向地址、返回的链接就会退回 http那前面所有的努力就白费了。第 3 节会详细说这件事。2.3 两种方案的对照与两个常见组合错误对比项方案 Akkfile 直挂证书方案 BNginx 终止 TLS配置复杂度低改 4 行 properties中需要写 server 块证书续期要重启 Java 进程reload Nginx 即可多节点扩展每台都要维护证书Nginx 统一处理与现有网关共存容易冲突天然融合排查难度报错直接日志清晰涉及转发头链路略长组合上的两个典型错误一是AB 同时开Nginx 用proxy_pass https://127.0.0.1:8012去连一个已经开了 SSL 的后端但端口又写错报 502二是 Nginx 转发到后端时用了 https然而后端并没有开 SSL握手失败。我的建议很明确要么全在 kkfile要么全在 Nginx不要两头都动。如果已经上了云负载均衡或者别的网关那就干脆让 kkfile 保持 http只监听 127.0.0.1连端口都不要对外暴露。3. Nginx 这一层把转发头一路传到后端别在斜杠上栽跟头3.1 证书与 server 块的基础配置先看一份可以直接抄的基础配置。假定对外域名是preview.example.comkkfile 还是默认的 8012 端口上下文路径是/fileserver { listen 80; server_name preview.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; http2 on; server_name preview.example.com; ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; client_max_body_size 200m; location /file/ { proxy_pass http://127.0.0.1:8012/file/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header Connection ; proxy_buffering off; proxy_request_buffering off; proxy_connect_timeout 30s; proxy_send_timeout 300s; proxy_read_timeout 300s; } }http2 on;是 Nginx 1.25.1 之后的写法老版本要写成listen 443 ssl http2;这一点升级过 Nginx 的同学应该都踩过。3.2 location 后面的斜杠是 404 的常见来源proxy_pass http://127.0.0.1:8012/file/;和proxy_pass http://127.0.0.1:8012/;是两个完全不同的结果。前者会把原始路径/file/xxx原样透传后端收到的还是/file/xxx后者会把匹配到的/file/前缀剥掉后端收到的是/xxx如果你的server.servlet.context-path还想让后端认/file就会 404。判断方法很简单先决定后端到底有没有上下文路径。有就用带/file/的写法没有把它改成了/就用不带前缀的写法。这里不要凭记忆直接curl一下后端本机地址就能确认curl -I http://127.0.0.1:8012/file/ curl -I http://127.0.0.1:8012/哪个返回 200 或 302就说明后端认哪个路径照着配就行。3.3 超时和体积两个不设就会半夜被叫醒的参数client_max_body_size必须大于等于kkfile 自己配置的上传上限。如果 Nginx 设了 50m而 kkfile 的spring.servlet.multipart.max-file-size是 100M那用户传一个 60M 的文件会直接在 Nginx 层拿到 413连后端日志都不会出现排查时很容易以为是程序问题。反过来Nginx 设得比后端大请求会打到后端再被拒绝至少日志里有迹可循。所以这两个值建议先对齐再留一点余量。超时也是一样。文档转换是个重活一个上百页的 PPT 转 PDF 花掉一两分钟很正常。Nginx 默认的proxy_read_timeout是 60 秒如果你没改就会出现小文件正常、大文件必然 504的规律。我一般会把读超时放到 300 秒同时确认 kkfile 那侧没有更短的超时限制。3.4 为什么要关掉 proxy_buffering默认情况下 Nginx 会把后端的响应先缓冲到磁盘攒够了再发给客户端。对于几 KB 的 HTML 这没问题但 kkfile 返回的可能是几十兆的 PDF缓冲会导致首字节迟迟不来用户以为卡死了。proxy_buffering off;让数据边收边发配合proxy_request_buffering off;让上传也走流式大文件的体验会好很多。代价是 Nginx 和 Java 进程之间的连接占用时间变长所以并发特别高的场景要留意连接数必要时调大worker_connections。4. kkfile 自己的配置base.url 决定了返回地址是 http 还是 https4.1 config 目录的覆盖机制改对文件才算数kkfile 的发行包里通常有两处配置文件一处打在 jar 包里一处是外置的config/application.properties。外置的优先级更高重启后生效升级版本时也不会被覆盖。所以一定要改外置那份改 jar 里的那份不但麻烦下次升级还会丢。另外注意版本差异4.x 用的是server.servlet.context-path早期的 3.x 用的是server.context-path。照着网上的老教程改完发现路径没变多半就是踩了这个坑先用unzip -p看一眼包里是哪套写法再动手。4.2 base.url 是整件事的关键一环这个配置项的作用是告诉 kkfile对外暴露的根地址是什么。它在生成预览相关地址、拼接结果文件链接时都会用到。如果你的部署是 Nginx 转发那就必须把它改成对外域名base.urlhttps://preview.example.com改完之后一定要重启然后清一次浏览器缓存或者强制刷新。如果这里还留着http://127.0.0.1:8012页面上就会出现入口是 https、内部资源是 http的混合内容表现就是预览容器在、内容不出来。注意末尾不要带斜杠协议和域名要和用户实际访问的完全一致包括是否是二级路径部署。如果 kkfile 是部署在子路径下比如https://oa.example.com/preview/那base.url要写成完整的子路径前缀同时 Nginx 的location和上下文路径要跟着对齐这三处必须像三把钥匙一样严丝合缝。4.3 端口、上下文路径与缓存目录几个我觉得值得一起检查的配置项server.port8012 server.servlet.context-path/file file.dir/opt/kkfile/file cache.typedefault spring.forward-headers-strategyframeworkspring.forward-headers-strategyframework这行很容易被忽略。它让 Spring 在处理请求时采纳X-Forwarded-*系列头部日志里记录的客户端 IP 才是真实用户 IP涉及重定向的逻辑也才会用对协议。不设的话日志里全是 Nginx 的127.0.0.1出问题的时候完全没法定位是谁在访问。file.dir是转换结果的缓存目录建议单独挂一块盘或者至少给足空间因为 PDF 和图片的缓存增长比想象中快。这个目录只允许 kkfile 进程读写就够了不要顺手挂到 Web 目录对外暴露。4.4 上传相关的体积参数要对齐spring.servlet.multipart.max-file-size200MB spring.servlet.multipart.max-request-size200MB这两个值必须一致而且要和 Nginx 的client_max_body_size匹配。我遇到过一种情况三个值分别是 200M、200M、100M结果是一个 150M 的文件上传失败报错信息指向后端实际是 Nginx 先拦下来的。把这三处并排写在纸上对齐一次能省掉很多来回折腾的时间。5. 预览空白的完整排查链路从报错信息倒推到根因5.1 第一步打开控制台看 Console 而不是 Network页面空白的时候人的本能是去 Network 面板找失败请求但混合内容拦截的问题在 Network 里往往什么都看不到因为请求压根没发出去。所以第一步永远是看 Console。关键词就盯这几个Mixed Content、blocked、insecure、net::ERR_SSL_PROTOCOL_ERROR。有 Mixed Content 提示就说明页面上有资源在用 http顺着提示里那行地址去看是哪个环节。如果 Console 干净但 Network 里有个请求一直 pending 或者 504那就不是协议问题而是超时或者后端卡住了去翻 kkfile 的转换日志。如果 Network 里看到一个 200 的请求但页面还是空的那问题多半出在结果文件的 Content-Type 或者渲染组件上。5.2 第二步用 curl 验证返回的内容里到底写的是什么协议浏览器会拦截但 curl 不会所以 curl 是穿过现象看本质的最好工具curl -k -I https://preview.example.com/file/ curl -k -o /dev/null -w %{http_code} %{url_effective}\n \ https://preview.example.com/file/test?urlhttps%3A%2F%2Ffiles.example.com%2Fa.docx重点看返回体去掉-I里出现的所有http://。如果有一段资源地址是http://开头那就基本锁定是base.url没改对或者某处硬编码。我还习惯顺手看一眼响应头里的Location和Content-Security-Policy前者决定有没有跳回 http后者决定页面能不能被 iframe 嵌。5.3 第三步看后端日志和转发头把 kkfile 的日志级别临时调到 DEBUG然后观察一次预览请求。要确认两件事一是它下载源文件时用的是不是你期望的地址二是它生成的预览地址前缀是不是 https。如果日志里显示X-Forwarded-Proto没有被识别就在 Nginx 侧确认这个头有没有漏配或者spring.forward-headers-strategy有没有生效。5.4 排查对照表现象大概率原因验证方式处理预览区域全白Console 报 Mixed Contentkkfile 入口仍是 http看 Console 提示中的地址统一走 https或改base.url页面能开内容一直转圈结果文件地址是 httpcurl 看返回体里的http://改base.url为 https 域名502 Bad GatewayNginx 转发地址或协议写错curl -I 127.0.0.1:8012修正proxy_pass404 Not Found斜杠或上下文路径不匹配分别 curl 两种路径对齐三处路径配置413 上传失败Nginx 体积限制偏小对比三个参数三处对齐大文件必 504读超时太短查看 Nginx error.log调大proxy_read_timeout后端日志全是 127.0.0.1未采纳转发头看 access log开启 forward-headers-strategy服务端拉取远程文件报 PKIX信任库缺根证书看 Java 异常栈导入 cacerts这张表我基本是照着实际处理过的工单整理的按现象直接对号入座效率比盲猜高得多。6. HTTPS 改造之后才会冒出来的几个坑6.1 证书链不完整桌面端正常但手机端报错配置里写ssl_certificate /etc/nginx/cert/fullchain.pem的时候一定要确认这个文件里包含了中间证书而不只是服务器证书。桌面浏览器有时候会自己补链所以看起来一切正常但部分移动端浏览器、小程序内置的 WebView、以及某些 HTTP 客户端不会补直接报证书无效。验证方法openssl s_client -connect preview.example.com:443 \ -servername preview.example.com -showcerts /dev/null输出的证书链里应该有至少两张证书只有一张就说明缺中间证书。把中间证书按顺序拼到服务器证书后面即可。6.2 X-Frame-Options 和 CSP让 iframe 直接被拒kkfile 本身默认不会禁止被嵌入但如果你的 Nginx 里加了统一的安全响应头比如某个全局add_header X-Frame-Options DENY;那业务系统里的 iframe 会被浏览器拒绝渲染Console 里报Refused to display ... in a frame because it set X-Frame-Options to deny。这个坑很阴因为它不是协议问题http 时代同样存在只是改造过程中顺手加安全头才暴露出来。处理方式是把 kkfile 这个 server 块的X-Frame-Options改成SAMEORIGIN或者干脆去掉用 CSP 的frame-ancestors精确指定允许嵌入的域名比一刀切的 DENY 合理得多。6.3 HTTP/2 下 Connection 头要清空前面配置里那行proxy_set_header Connection ;不是可有可无的。HTTP/2 协议本身就禁止使用 Connection 这个逐跳头如果在 h2 场景下还带着它转发某些 Nginx 版本会直接报 400。加上这行等于告诉 Nginx跟后端保持长连接但别把这个头传过去配合proxy_http_version 1.1一起用。6.4 中文文件名在 https 下更容易暴露编码问题http 时代用 GBK 混过去的情况在 https 和现代浏览器的组合下会原形毕露文件名里的中文变成百分号乱码或者下载时提示文件不存在。根因是 URL 编码不一致。稳妥的做法有两处一是前端拼预览地址时对 url 参数做一次 encodeURIComponent二是在 Nginx 的 server 块里显式声明编码。测试时不要图省事用英文文件名一定要拿中文名文件、带空格的文件名各测一遍这两个才是最容易出问题的。6.5 会话 Cookie 的 Secure 属性与同域混用如果业务系统和 kkfile 在同一个域名下通过不同路径区分而且你给会话 Cookie 加了Secure属性那么任何一次走 http 的访问都会丢掉会话。表现为有时能预览有时提示未登录。改造期间如果还有一部分老入口没切到 https就会间歇性出现这种问题。处理原则是切换要一次性切干净不要 http 和 https 长期并存否则这类问题会一直间歇性复现特别消耗排查精力。7. 上线前的分层验证清单与参数复核7.1 按层次依次验证别跳步我一般的验证顺序是这样的每一步失败了都不会影响下一步的判断效率最高证书层用openssl s_client确认证书链完整、有效期正常、域名匹配。入口层浏览器直接访问https://preview.example.com/file/能打开预览首页且地址栏没有证书警告。单文件层用一个几百 KB 的 docx 走完整流程确认预览画面正常。大文件层用一个 50M 以上的 PDF 或上百页的 PPT观察是否 504、是否有明显卡顿。嵌入层在业务系统的 iframe 里跑一遍看 Console 是否干净。移动端至少用一台手机浏览器跑一遍这一步能筛出证书链问题。六步走完再上线比直接甩给用户试要省事得多。7.2 上线前一晚最后核对一遍的参数位置参数建议值说明Nginxclient_max_body_size200m不小于后端的最大上传Nginxproxy_read_timeout300s大文件转换需要时间Nginxproxy_bufferingoff避免大文件首字节延迟NginxX-Forwarded-Proto$scheme后端判断协议的依据kkfilebase.urlhttps://你的域名决定返回地址的协议kkfilemax-file-size200MB与 Nginx 对齐kkfilemax-request-size200MB与上一项一致kkfileforward-headers-strategyframework获取真实客户端 IPkkfileserver.servlet.context-path/file与 Nginx 的 location 对齐这张表我自己是当检查单用的每次部署新环境过一遍能挡掉八成低级问题。最后分享一个我觉得挺省事的小技巧把 kkfile 的访问入口和业务系统的入口都放在同一个域名下、用不同路径区分可以绕开相当多的跨域和证书匹配问题配置上只需要在 Nginx 里多加一个 location。代价是 Nginx 的配置会更集中风险是一处改错影响面更大。所以改完别急着 reload先nginx -t检查语法再nginx -s reload真的出问题还能从容回滚。至于base.url这个配置项我个人经验是——只要预览出现任何说不清的现象先去核对它一遍十次里有三次就是它。