DataEase v3.1.0 分享链接静态资源 401 问题

版本信息: DataEase v3.1.0
部署方式: dectl upgrade( v3.0.0 → v3.1.0)

错误信息: 数据大屏分享链接静态资源加载 401
影响面: 所有分享链接 / 嵌入页面中的全部静态资源(图片、SVG 等)
拦截条件: requestURI.contains("/static-resource/") 且 无登录态 且 无有效 Cookie 令牌
根因: v3.1.0 的 CommunityTokenFilter 新增「静态资源独立鉴权分支」,将 /static-resource/ 排除在白名单外并强制校验 Cookie 令牌;但全链路无任何代码写入该 Cookie

临时补丁: BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js

// 原逻辑
e.headers["x-de-link-token"] && R0.setLinkToken(e.headers["x-de-link-token"])

// 补丁后
e.headers["x-de-link-token"] && (R0.setLinkToken(e.headers["x-de-link-token"]),
  document.cookie = "DE-LINK-TOKEN=" + e.headers["x-de-link-token"] + "; path=/; SameSite=Lax")

@Steven 哥,怎么更新这个补丁,麻烦介绍一下,不太熟悉docker操作。谢谢

操作步骤(可复制执行)

下文用 TS 代表时间戳;先执行 TS=$(date +%Y%m%d_%H%M%S) && echo $TS,后续命令引用同一 TS。

步骤 1|定位补丁点

# 1) 把容器内的 app.jar 复制到宿主机(228 MB,秒级)
docker cp dataease:/opt/apps/app.jar /tmp/app.jar.work
md5sum /tmp/app.jar.work          # 记下来,等下核对

# 2) 确认目标 chunk 存在(版本升级后文件名中的版本号会变)
unzip -l /tmp/app.jar.work | grep 'static/assets/chunk/locale-' | head

# 3) 解出目标文件(★ 保留路径,不能用 -j,回写时要靠这个相对路径)
mkdir -p /tmp/patchwork && cd /tmp/patchwork
unzip -o -q /tmp/app.jar.work "BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js"

# 4) ★ 确认目标串唯一出现 1 次(不是 1 就必须停下来人工检查)
grep -c -o 'setLinkToken(e){e!==this.linkToken&&(this.visitorPermissions=0),this.linkToken=e}' \
  BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js

若换了大版本,用 grep -l setLinkToken *.js 反查是哪个 chunk 定义了这个方法(修正环境为 locale-3.0.0-dataease.js)。

步骤 2|备份(三个都要做)

# ① compose 文件
cp -a /opt/dataease3.0/docker-compose.yml /root/docker-compose.yml.bak-$TS

# ② 原始 app.jar(回滚的唯一依据)
docker cp dataease:/opt/apps/app.jar /root/app.jar.v3.1.0.orig-$TS

# ③ 记录校验值
md5sum /root/app.jar.v3.1.0.orig-$TS /opt/dataease3.0/docker-compose.yml

原环境实测:app.jar md5 = af7acda865ec8eeccd991b7562e00048(228,897,023 字节)、compose md5 = 4e353f9ec13e376d8845df6086296a92。

步骤 3|制作补丁 jar

cd /tmp/patchwork

# ① 字节级替换(用 python 而非 sed,避免转义地狱;断言保证只替换 1 处)
python3 - <<'PYEOF'
p = 'BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js'
b = open(p, 'rb').read()
old = b'setLinkToken(e){e!==this.linkToken&&(this.visitorPermissions=0),this.linkToken=e}'
new = (b'setLinkToken(e){try{e&&(document.cookie="DE-LINK-TOKEN="+e+"; path=/; SameSite=Lax")}'
       b'catch(t){}e!==this.linkToken&&(this.visitorPermissions=0),this.linkToken=e}')
assert b.count(old) == 1, '目标串出现次数异常,终止'
b2 = b.replace(old, new)
open(p, 'wb').write(b2)
print('size: %d -> %d (delta %+d)' % (len(b), len(b2), len(b2) - len(b)))
PYEOF
# 期望输出:size: 197856 -> 197935 (delta +79)

# ② 生成待发布的补丁副本(从原始备份复制,保证基线干净)
mkdir -p /opt/dataease3.0/patch
cp -a /root/app.jar.v3.1.0.orig-$TS /opt/dataease3.0/patch/app.jar

# ③ ★ 只替换 jar 内那一个条目(不要整体解压重打包!)
zip -X /opt/dataease3.0/patch/app.jar \
  "BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js"

zip 会打印一行 zip warning: Local Entry CRC does not match CD —— 属正常现象(原 jar 该条目用了 streamed 写法),不影响结果,继续即可。

步骤 4|校验补丁 jar(不做这步不要上线)

cd /opt/dataease3.0/patch

# ① 条目总数、名称、压缩方法必须与原件完全一致
unzip -v app.jar | awk 'NR>3 && NF>=8 {print $2"|"substr($0,index($0,$8))}' | sort > /tmp/after.txt
unzip -v /root/app.jar.v3.1.0.orig-$TS | awk 'NR>3 && NF>=8 {print $2"|"substr($0,index($0,$8))}' | sort > /tmp/before.txt
diff /tmp/before.txt /tmp/after.txt && echo "✅ 条目与压缩方法完全一致"

# ② STORED(未压缩)条目数必须不变 —— Spring Boot 硬性要求,少了会启动失败
awk -F'|' '$1=="Stored"' /tmp/after.txt | wc -l        # v3.1.0 应为 503

# ③ 包内补丁确实写入
unzip -p app.jar "BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js" \
  | grep -c 'DE-LINK-TOKEN="+e+; path=/; SameSite=Lax'  # 应为 1

# ④ 单条目完整性测试
unzip -t app.jar "BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js"

# ⑤ 补丁包总校验值(本次实测 a09982e1577f314c240abfa2ee10b43b)
md5sum app.jar

本次实测结果:2262 个条目、仅 1 条 CRC 变化、503 个 STORED 不变 → 打包安全。

步骤 5|部署(挂载 + 重建容器)

编辑 /opt/dataease3.0/docker-compose.yml,在 dataease.volumes: 首行插入(建议连注释一起留档):

    volumes:
      # === [PATCH-20261008] 分享链接/嵌入静态资源 401 修复,官方 v3.1.0 CommunityTokenFilter 回归 ===
      # 补丁:前端 setLinkToken 处补写 DE-LINK-TOKEN Cookie。回滚方式:删除下面一行后重新 up -d
      # 原始备份:/root/app.jar.v3.1.0.orig-<TS>(md5 af7acda865ec8eeccd991b7562e00048)
      # ⚠ 升级 DataEase 时必须先移除本行与 /opt/dataease3.0/patch/app.jar,否则会挂载旧版 jar
      - ${DE_BASE}/dataease3.0/patch/app.jar:/opt/apps/app.jar
cd /opt/dataease3.0

# ① 先校验 compose 语法与变量插值(不启动任何东西)
docker compose -f docker-compose.yml -f docker-compose-mysql.yml config > /dev/null && echo "compose OK"

# ② 重建容器(配置变更 → 只重建 dataease,mysql-de 保持 Running)
docker compose -f docker-compose.yml -f docker-compose-mysql.yml up -d

# ③ 确认挂载生效 + 容器内 jar 就是补丁包
docker inspect dataease --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' | grep app.jar
docker exec dataease md5sum /opt/apps/app.jar     # 应等于步骤 4 ⑤ 的值

步骤 6|验证(三层,缺一不可)

# ① 应用就绪(约 30 秒)
for i in $(seq 1 60); do
  [ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8100/de2api/dekey)" = "200" ] && \
  { echo "就绪(约 $((i*3)) 秒)"; break; }; sleep 3
done

# ② 服务端提供的前端 chunk 已是补丁版(大小应为 197935)
curl -s -o /dev/null -w '/assets/chunk/locale-3.0.0-dataease.js => %{http_code} size=%{size_download}\n' \
  http://127.0.0.1:8100/assets/chunk/locale-3.0.0-dataease.js

# ③ 无 Cookie 时仍 401 —— 这是**预期**的,说明服务端过滤逻辑未被改动,修复完全靠前端写 Cookie
curl -s -o /dev/null -w 'jpg(无 Cookie) => %{http_code}\n' \
  http://127.0.0.1:8100/de2api/static-resource/7404398862594478080.jpg

第四层:浏览器实测(唯一能证明"真的修好了"的方式)。用无头浏览器打开分享页,检查 Cookie、资源状态码与真实渲染:

// verify.js —— 运行:NODE_PATH=<playwright 路径> node verify.js
const { chromium } = require('playwright');
const BASE = 'http://192.168.1.243:8100';
(async () => {
  const b = await chromium.launch({ headless: true, channel: 'chrome' });
  const ctx = await b.newContext();
  const p = await ctx.newPage();
  p.on('response', (r) => r.url().includes('static-resource') &&
    console.log(r.status(), r.url().replace(BASE, ''), r.headers()['content-type'], r.headers()['content-length']));
  await p.goto(BASE + '/#/de-link/LBU2yjBI', { waitUntil: 'domcontentloaded', timeout: 60000 });
  await p.waitForTimeout(15000);
  console.log('COOKIES:', JSON.stringify(await ctx.cookies()));
  console.log('IMG:', JSON.stringify(await p.evaluate(() =>
    Array.from(document.querySelectorAll('img')).map(i => ({ src: i.src, w: i.naturalWidth, h: i.naturalHeight })))));
  await b.close();
})();

非常感谢回复,很耐心写得很详细,必须点赞