Repository navigation
feat: 新增可选的签名预览,预览请求可携带带有效期的签名令牌(JWT),默认关闭 - #806
Open
yuhaibohotmail wants to merge 1 commit into
Open
yuhaibohotmail wants to merge 1 commit into
yuhaibohotmail wants to merge 1 commit into
Conversation
把 kkFileView 嵌入业务系统时,「谁能预览哪个文件、带什么水印」通常由业务系统决定。
新增 signed.preview.enabled(默认 false)。关闭时请求原样往后传,行为与参数和之前
完全一样;开启后 /onlinePreview、/picturesPreview 只接受带有效签名令牌的请求:
- 令牌由接入方用约定的密钥签发,必须带 exp;算法支持 HS256、HS384、HS512,
令牌头里的 alg 必须与配置一致。只用 JDK 自带的 HMAC 与已有的 Jackson,不加依赖。
- 文件地址、文件名、水印、是否强制重新转换取令牌里的;浏览器带的参数只保留放行名单
(signed.preview.allow.params)里的,名单可以限定取值(officePreviewType=image|pdf)。
trust.host、not.trust.host、kk.key 照常生效。
- 强制重新转换每枚令牌只执行一次,转换等待页带着同一枚令牌刷新时不会反复重新转换。
- 压缩包预览页拼包内文件链接时带上 kkFileView 自己签发的令牌,只对该压缩包解开的
目录有效,水印沿用打开压缩包时令牌里的,接入方不需要为包内文件再签。
- 令牌缺失、无效、过期时返回 403 提示页(web/signedPreviewDenied.html)。
三种令牌(接入方签的预览令牌、kkFileView 签的压缩包令牌与取件令牌)靠用途字段区分。
字段名、三种用途的值、预览令牌是否必须写用途都可配;三个值为空或有重复时入口拒绝
全部请求、也不签发自己的令牌,不影响应用启动。
另增 signed.fetch.enabled(默认 false,独立开关):kkFileView 自己下载原文件时在请求头
里带一枚自己签发的令牌,载荷里是本次请求的地址,文件来源一方可用同一把密钥校验。
/getCorsFile 代浏览器取文件时不带。令牌只给原始地址:带令牌的那次请求不自动跟随跳转,
遇到跳转且 kk.enable.redirect 允许时,另发一次不带令牌的请求去取;开关关着时跳转行为不变。
密钥、算法、令牌所在的参数名、令牌里的字段名(五个单独配置项加一张映射表
signed.preview.claim.map)、放行名单、有效期(压缩包令牌与取件令牌可各自单独配)、
时钟误差、取件请求头的名字与前缀(可配成 Authorization: Bearer,此时以取件令牌为准)、
取件令牌里地址所在的字段(支持 payload.url 这样的点分嵌套)均可配置,缺省值即上述行为,
已有自己令牌格式的接入方可以直接对上。写法为 ${KK_XXX:默认值},application.properties 与 test.properties 同步增加,
并接入 ConfigConstants 与 ConfigRefreshComponent。
新增测试 SignedPreviewFilterTests、SignedPreviewOptionsTests、SignedPreviewArchiveTests、
SignedFetchTests、SignedPreviewCustomFormatTests、SignedPreviewConfigTests,测试里的令牌按 JWT 规范独立签发与校验,不经过被测代码。
Closes kekingcn#805
Signed-off-by: yuhaibo <yuhaibo@hotmail.com>
Contributor
Author
|
补充一次实际启动的验证:用本 PR 的最终版本打包启动,不配置任何
服务日志中没有 ERROR。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
对应 #805 的功能建议。把 kkFileView 嵌入业务系统时,「谁能预览哪个文件、带什么水印」通常由业务系统决定。接入方希望预览链接有有效期,并且链接里「看哪个文件、带什么水印」由业务系统说了算。
这个 PR 增加一个可选的、默认关闭的「签名预览」。不配置任何新项时,行为、接口参数与现在完全一样。
设计
1. 预览入口校验令牌(
signed.preview.enabled,默认false)新增过滤器
SignedPreviewFilter,挂在/onlinePreview、/picturesPreview上,排在ChinesePathFilter之后,信任站点、信任目录、水印三个过滤器之前。token)里取令牌,校验签名、算法、有效期。令牌头里的alg必须与配置的算法一致;必须带数字形式的exp;带了nbf时也会校验。officePreviewType,tifPreviewType,page,highlightall,filePassword,key),其余丢弃。由令牌决定的参数即使写进放行名单也不从浏览器取。officePreviewType=image|pdf|html,page:带=的只放过列出的值(区分大小写;值不在其中,或同一个参数给了多个值时,丢掉这个参数,不拒绝整个请求),不带=的任意值。FileHandlerService、水印过滤器都不用改,读到的就是重建后的参数。web/signedPreviewDenied.html,三种情况文案不同)。开启了但没有配置密钥时,全部请求返回 403 并提示联系管理员。waiting.ftl)会整页刷新当前地址,令牌仍在地址里;如果每次都执行,就会每刷新一次重新转换一次。2. 压缩包内的文件
压缩包预览页里,包内文件的预览链接是页面自己拼的。开启签名预览后,
CompressFilePreviewImpl在出页面时由 kkFileView 自己签一枚令牌交给页面,compress.ftl拼链接时带上:kkCompressfileKey必须是这个目录,kkCompressfilepath必须落在目录内;signed.preview.archive.ttl,没单独配时用signed.preview.ttl;接入方只需要为压缩包本身签一次。压缩包里再套压缩包时同样适用。
3. 取原文件时附带令牌(
signed.fetch.enabled,默认false,独立开关)开启后,kkFileView 自己用 http/https 下载原文件时(
DownloadUtils.downLoad),在请求头(默认kk-fetch-token)里带一枚自己签发的令牌,缺省的载荷为:{"use":"fetch","url":"<本次请求的地址>","iat":1767225000,"exp":1767225600}请求头的写法和令牌的形状可以按文件来源一方已有的校验方式来配:
signed.fetch.header加signed.fetch.header.prefix:例如配成Authorization和Bearer,发出的是Authorization: Bearer <令牌>。请求头配成Authorization时以取件令牌为准,地址上的 basic 认证参数和转发来的kk-proxy-authorization不会覆盖它;请求头是别的名字时,这些逻辑照旧。signed.fetch.claim.url:地址放在载荷的哪个字段,支持点分的对象嵌套,例如payload.url签成{"payload":{"url":"<地址>"}}。signed.fetch.ttl:取件令牌的有效期,没单独配时用signed.preview.ttl。文件来源一方可以用同一把密钥校验,确认请求来自这台预览服务、且就是为了取这个地址。
/getCorsFile是代浏览器取文件,不带这枚令牌。令牌绑定的是原始请求地址,只发给原始地址那一方:带令牌的那次请求不自动跟随跳转;文件来源回 3xx 且
kk.enable.redirect允许跳转时,kkFileView 另发一次不带令牌的请求去取跳转目标。取件签名关着时,跳转的处理与现在完全一样。4. 三种令牌互不通用
接入方签的预览令牌、kkFileView 签的压缩包令牌和取件令牌用同一把密钥,靠载荷里的用途字段区分。缺省字段名是
use:预览令牌不写或写"preview",另两种分别是"archive"、"fetch",入口只把前者当预览令牌。signed.preview.claim.use)和三种用途的值(signed.preview.use.preview/.archive/.fetch)都可配,接入方已有自己的用途字段时不用改签发那一侧。signed.preview.use.required=true时,预览令牌必须写用途,不写就拒绝;缺省false。5. 字段名可以对上接入方已有的令牌格式
令牌所在的参数名、文件地址、多图地址、文件名、水印文字、强制重新转换这几个字段各有单独的配置项。其余九个水印参数和
currentUrl(对应现有的同名请求参数,现有的多图预览页面目前并不使用它)用一张映射表signed.preview.claim.map改名,格式为请求参数名:令牌字段名,例如watermarkAlpha:wmAlpha,currentUrl:cur;没写进表的仍与请求参数同名。有单独配置项的那五个字段不走这张表,表里写了也以单独那一项为准。压缩包令牌沿用水印时用的是同一套字段名。6. 实现上的取舍
JwtUtils,约 150 行),没有新增依赖。目前支持 HS256、HS384、HS512。TrustHostFilter、TrustDirFilter、OnlinePreviewController、FileHandlerService。配置项
application.properties与test.properties同步增加,都支持环境变量;已接入ConfigConstants与ConfigRefreshComponent(改配置文件后动态生效)。signed.preview.enabledKK_SIGNED_PREVIEW_ENABLEDfalsesigned.preview.secretKK_SIGNED_PREVIEW_SECRETfalse(未配置)signed.preview.algorithmKK_SIGNED_PREVIEW_ALGORITHMHS256HS256、HS384、HS512signed.preview.token.paramKK_SIGNED_PREVIEW_TOKEN_PARAMtokensigned.preview.claim.urlKK_SIGNED_PREVIEW_CLAIM_URLurlsigned.preview.claim.urlsKK_SIGNED_PREVIEW_CLAIM_URLSurlssigned.preview.claim.filenameKK_SIGNED_PREVIEW_CLAIM_FILENAMEfullfilenamesigned.preview.claim.watermarkKK_SIGNED_PREVIEW_CLAIM_WATERMARKwatermarkTxtsigned.preview.claim.forceKK_SIGNED_PREVIEW_CLAIM_FORCEforceUpdatedCachesigned.preview.claim.mapKK_SIGNED_PREVIEW_CLAIM_MAPdefault(同名)currentUrl的字段名映射,请求参数名:令牌字段名,逗号分隔signed.preview.claim.useKK_SIGNED_PREVIEW_CLAIM_USEusesigned.preview.use.previewKK_SIGNED_PREVIEW_USE_PREVIEWpreviewsigned.preview.use.archiveKK_SIGNED_PREVIEW_USE_ARCHIVEarchivesigned.preview.use.fetchKK_SIGNED_PREVIEW_USE_FETCHfetchsigned.preview.use.requiredKK_SIGNED_PREVIEW_USE_REQUIREDfalsesigned.preview.allow.paramsKK_SIGNED_PREVIEW_ALLOW_PARAMSofficePreviewType,tifPreviewType,page,highlightall,filePassword,key参数名=值1|值2表示只放过列出的值signed.preview.ttlKK_SIGNED_PREVIEW_TTL600signed.preview.archive.ttlKK_SIGNED_PREVIEW_ARCHIVE_TTLdefault(用signed.preview.ttl)signed.preview.leewayKK_SIGNED_PREVIEW_LEEWAY30signed.fetch.enabledKK_SIGNED_FETCH_ENABLEDfalsesigned.fetch.headerKK_SIGNED_FETCH_HEADERkk-fetch-tokensigned.fetch.header.prefixKK_SIGNED_FETCH_HEADER_PREFIXdefault(不加)Bearersigned.fetch.claim.urlKK_SIGNED_FETCH_CLAIM_URLurlpayload.urlsigned.fetch.ttlKK_SIGNED_FETCH_TTLdefault(用signed.preview.ttl)配置项的名字、默认值都可以按维护者的习惯调整。
令牌里的字段
接入方签发的预览令牌(下表是缺省的字段名,除
exp、nbf外都可以改名):expurl/onlinePreview必填urls/picturesPreview必填|分隔的字符串currentUrlcurrentUrl请求参数;现有的多图预览页面目前并不使用它fullfilenamefullfilename参数时,按现有约定接在地址后面watermarkTxtwatermarkXSpace、watermarkYSpace、watermarkFont、watermarkFontsize、watermarkColor、watermarkAlpha、watermarkWidth、watermarkHeight、watermarkAngleforceUpdatedCachetrue时强制重新转换,每枚令牌只执行一次nbfuse"preview";signed.preview.use.required=true时必须写接入方已有自己的令牌格式时的一组配置示例(名字都是示意):
这时预览令牌的载荷形如
{"kind":"view","fileUrl":"...","mark":"...","exp":...},取原文件的请求带Authorization: Bearer <令牌>,令牌载荷为{"kind":"pull","payload":{"url":"..."},"iat":...,"exp":...}。签发示例(Java,只用 JDK;用任何 JWT 库签出来的 HS256 令牌也一样):
切换预览方式、翻页等仍然直接加在地址上,例如
...onlinePreview?token=<令牌>&officePreviewType=image。兼容性
false。关闭时SignedPreviewFilter直接放行原请求;压缩包页面不签发令牌,模板输出不变;取原文件的请求不多带请求头。HttpRequestUtils.executeHttpRequest原来的四个参数的方法保留,行为不变;新增一个多带signFetch参数的重载,只有DownloadUtils传true。trust.host/not.trust.host:叠加。令牌里的文件地址照常经过信任站点、信任目录检查。kk.key:并存。key在默认放行名单里,原有的校验照常执行。/getCorsFile、/addTask:不在这次范围内,仍由各自的开关控制。验证
单元测试
新增 6 个测试类共 49 条(JUnit 5,不需要 LibreOffice)。测试里的令牌由测试自己按 JWT 规范用
javax.crypto.Mac签发和校验,不经过被测的JwtUtils。SignedPreviewFilterTests(19 条):关闭时带不带令牌请求都原样通过(同一个请求对象、参数不变);开启后无令牌、过期、签名不对、载荷被替换、算法与配置不一致(含alg=none、头里写 HS256 实际用 HS512 签)、不带exp、未配置密钥都返回 403;时钟误差;令牌里的地址与浏览器另带的url、urls、currentUrl、urlPath冲突时以令牌为准;信任站点检查仍然作用在令牌里的地址上;水印与强制重新转换取令牌里的,强制重新转换每枚令牌只执行一次;放行名单以外的参数被丢弃,放行名单可配,且不能把由令牌决定的参数放回给浏览器;文件名字段;参数名、字段名可配;多图预览;其它用途的令牌不能当预览令牌用。SignedPreviewOptionsTests(7 条):用途字段名和三种用途的值可配,自己签发的两种令牌用的也是配置的值;要求必须写用途时不写就拒绝;三个用途值有重复或为空、用途字段名为空时,入口全部拒绝并提示配置有误,压缩包令牌和取件令牌都不签发;映射表给其余水印参数和currentUrl改名,改名后原来的名字不再认,压缩包令牌沿用水印时用映射后的名字;有单独配置项的字段不受映射表影响;放行名单限定取值(不在名单里的值、大小写或空格不同的、同一参数多个值的都丢掉参数而不拒绝请求);压缩包令牌与取件令牌各自的有效期,没单独配时用共用的那一项。SignedPreviewArchiveTests(5 条):关闭时压缩包页面没有令牌;不在已校验的请求里时不签发、页面照常出;开启后页面拿到的令牌内容正确、模板把它带进了链接;页面签出的令牌能打开本压缩包里的文件并带上水印;拿它去开别的压缩包、目录以外的路径(含多重编码)、普通文件地址、多图预览,以及过期后,都返回 403。SignedFetchTests(11 条,起一个本机的com.sun.net.httpserver.HttpServer当文件来源):开启后收到的请求头能用同一把密钥验过,载荷里是本次请求的地址;请求头名可配;地址可以放在嵌套字段里;请求头的值可以带前缀;请求头配成Authorization时以取件令牌为准(地址上带 basic 认证参数、同时转发了kk-proxy-authorization的情况下只有令牌那一个值),请求头是别的名字或不签名时这些逻辑照旧;关闭或未配置密钥时不带;代浏览器取文件时不带;DownloadUtils.downLoad下载原文件时带;文件来源回 302 时,原始地址收到令牌、跳转目标收不到、内容照常取回;取件签名关着时跳转的处理不变;不允许跳转时带令牌的请求也不跟随。SignedPreviewCustomFormatTests(1 条):把整套配成上面「配置示例」那种格式,用测试自己签的令牌过预览入口,再用入口交出来的地址经DownloadUtils.downLoad向本机文件服务取原文件,核对文件来源收到的是Authorization: Bearer <令牌>、令牌里地址在payload.url、用途值和 120 秒有效期都对;缺省格式的令牌、不写用途的令牌、放在缺省参数名里的令牌在这套配置下都不通过。SignedPreviewConfigTests(6 条):两份配置文件里两个开关默认关闭,各可配项的默认值与说明一致;过滤器挂在两个预览入口上,顺序在信任站点、信任目录、水印过滤器之前。每一组都是先写测试、看到失败,再写实现:
Failures: 20, Errors: 5;通过的 8 条是「关闭时不变」「默认值为关闭」这一类,空实现下本来就该通过。redirectTargetDoesNotReceiveTheFetchToken先失败(跳转目标收到了原来那枚令牌),实现后通过。url、不按放行名单过滤、不限制包内文件地址、关闭时也签压缩包令牌、带令牌的请求自动跟随跳转、不检查三个用途值是否互不相同、用途值配置有误时仍签发、请求头用追加而不是覆盖、映射表盖过单独配置项、取值限定不生效或放过多个值。49 条里 47 条至少失败过一次,剩下 2 条是test.properties的默认值检查,与application.properties那两条是同样的断言。mvn -B -pl server test -Dtest='!ServerMainTests,!EncodingTests'(Windows,JDK 25):Tests run: 135, Failures: 0, Errors: 1。唯一的 1 条是ArchiveDirectorySecurityTests.cachedArchivePreviewRendersItsTreeWithoutAnEnumerationRequest,它要创建文件名带双引号的文件,Windows 不允许,在未修改的 master 上同样失败(master 用-Dtest='!ServerMainTests'是 87 条、同样 1 条 Error),与本次修改无关。EncodingTests(1 条,按默认配置启动整个应用)在较早的一版上通过,最终版没有再单独跑它,最终版能正常启动由下面的实际启动验证。实际启动
用这个分支打出的
kkFileView-5.0.3.jar在 Windows 上启动(JDK 25,仓库自带的 LibreOfficePortable),另起一个只监听本机的小文件服务当文件来源并记录收到的请求头,用 curl 请求。trust.host配成文件服务所在的主机。下面「开启」那一组是在最终版上做的;在此之前的构建上也做过同样的一遍,两次结果逐条一致(包括跳转那一条)。「不配置任何新项」那一组是在较早的构建上做的,最终版没有重做。不配置任何新项:
/onlinePreview?url=<base64>预览 txttoken参数kk-fetch-token头KK_SIGNED_PREVIEW_ENABLED=true、KK_SIGNED_FETCH_ENABLED=true并配置密钥:url=指向另一个文件officePreviewType=imageofficePreviewType=pdf)forceUpdatedCache=true,同一地址连续请求 4 次kk-fetch-token,同一把密钥验签通过,载荷里的url就是被请求的地址kk.enable.redirect = true,文件服务对某个地址回 302预览页切换预览方式的脚本(
pdf.ftl的goForImage、officePicture.ftl的changePreviewType)都是在当前地址上追加或替换officePreviewType,令牌留在地址里;上表按脚本拼出的地址请求过,都是 200。另按一套自定义的名字与形状配置后重跑(最终版):令牌参数名、三种用途值都换成自定义的,预览令牌必须写用途,取件用
Authorization: Bearer <令牌>、地址放在嵌套字段里、有效期 120 秒,放行名单对officePreviewType限定取值。jti)forceUpdatedCache=true和jti),同一地址请求 3 次officePreviewType=allImages(不在限定的取值里)officePreviewType=imageAuthorization: Bearer <令牌>(没有缺省的那个请求头),同一把密钥验签通过;载荷里是配置的用途值、嵌套字段里的地址(与被请求的地址一致)、iat和exp,exp距收到请求正好 120 秒这套配置的放行名单里没有
key,压缩包页面拼链接时带的key被丢弃,包内文件照常预览(当时kk.key没有开启);同时开启kk.key的部署需要把key留在放行名单里。浏览器
最终版、缺省格式,用 Chrome(Windows)实际打开:
forceUpdatedCache=true的 docx:先出「文件转换中」的等待页,页面自己刷新后出预览,没有反复重转;officePreviewType=pdf;officePreviewType=image,请求通过了校验,页面显示的是 [ISSUE] PDF 方式预览 Office 文件后,再切换为图片预览,预览失败 #754 的「Office转换缓存异常」(与本 PR 无关,不开签名预览时同样存在);用 curl 实际启动时也遇到过 #754 的同一个现象:同一个 docx 先用 PDF 方式预览、再切到图片方式,页面提示「Office转换缓存异常,请联系管理员」。
没有做的验证:没有跑
tests/e2e(需要另外安装 Playwright 浏览器和 Python、7z 等依赖)。Closes #805