JeecgBoot + EuroOffice 协同文档在线编辑全栈接入指南(支持 JWT 安全校验)

发布时间:2026/10/21 11:33:53
JeecgBoot + EuroOffice 协同文档在线编辑全栈接入指南(支持 JWT 安全校验) 前排提示本文面向有 Spring Boot Vue 基础的开发者全流程覆盖 Docker 部署、服务端接口、前端集成、JWT 安全校验、回调保存机制。可直接移植到 JeecgBoot 项目。一、整体架构说明核心思路后端不存储文档的编辑状态只在用户打开时生成带 JWT 签名的editorConfigEuroOffice 编辑完成后通过回调通知后端下载最新版本并落盘。二、EuroOffice Document Server 部署2.1 准备条件Linux / MacDocker 已安装一个可用的域名开发环境可用localhost开放端口80882.2 一键启动docker run -d \ --name euro-office \ --restartunless-stopped \ -p 8088:80 \ --add-hosthost.docker.internal:host-gateway \ -e JWT_SECREThCc0UPuZ0dPcuE6El2GL3iVqUqFWGNW7 \ -e JWT_HEADERAuthorization \ -e EXAMPLE_ENABLEDtrue \ -v /home/app/oa/eurooffice/data:/var/lib/euro-office/documentserver \ ghcr.io/euro-office/documentserver:latest关键参数说明参数含义必须性JWT_SECRET前后端通信签名密钥必须否则编辑器无法加载JWT_HEADERJWT 在 HTTP 头中的字段名可选默认AuthorizationEXAMPLE_ENABLED是否允许无 JWT 访问仅开发环境生产必须falsehost.docker.internalDocker 容器内访问宿主机Mac/Win Docker Desktop 调试必填--add-hosthost.docker.internal:host-gateway 的作用是让容器里能用主机名 host.docker.internal 访问到宿主机即让 Euro Office 容器通过 host.docker.internal 回调/访问宿主机上的 OA 服务。⚠️ 坑点预警JWT_SECRET前后端必须完全一致且生产环境务必关闭EXAMPLE_ENABLED。2.3 验证部署是否成功访问http://服务器IP:8088/或http://localhost:8088/看到编辑器界面即表示部署成功。2.4 生产环境 HTTPS 部署Nginx 反向代理生产环境的 EuroOffice 通过 域名 提供服务三、数据库建表在 MySQL 中执行以下 SQL四、后端 Spring Boot 集成4.1 添加 Maven 依赖!-- JWT 签名编辑配置 回调鉴权 -- dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency4.2 配置文件application-dev.yml / application-prod.yml# 核心配置顶级 key 与 ConfigurationProperties(prefix eurooffice) 对应 eurooffice: # EuroOffice Document Server 地址浏览器访问的地址 docs-url: http://localhost:8088 # 开发环境 # docs-url: https://www.minglina.com/ds # 生产环境Nginx 代理后的 HTTPS 地址 # OA 后端暴露给 Document Server 的回调地址必须公网可访问 public-base-url: http://host.docker.internal:18080/oa-boot # JWT 密钥必须与 Docker 启动时的 JWT_SECRET 环境变量完全一致 jwt-secret: hCc0UPuZ0dPcuE6El2GL3iVqUqFWGNW7 # JWT 在 HTTP Header 中的字段名 jwt-header: Authorization # 下载短链有效期秒默认 600 download-token-ttl-seconds: 600 # Shiro 放行回调接口匿名访问 jeecg: shiro: excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/api/getUserInfo,/eurooffice/callback,/eurooffice/file/**4.3 核心配置类package com.ynfy.buss.eurooffice.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; Data Component ConfigurationProperties(prefix eurooffice) public class EuroOfficeProperties { /** 浏览器访问 Document Server 的地址 */ private String docsUrl http://localhost:8088; /** Document Server 回调 OA 的基址 */ private String publicBaseUrl http://host.docker.internal:18080/oa-boot; /** JWT 密钥必须与 Docker JWT_SECRET 一致 */ private String jwtSecret; /** JWT Header默认 Authorization */ private String jwtHeader Authorization; /** 下载短链有效期秒 */ private long downloadTokenTtlSeconds 600; }4.4 JWT 工具类package com.ynfy.buss.eurooffice.util; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Map; Component RequiredArgsConstructor public class EuroOfficeJwtHelper { private final ObjectMapper objectMapper; /** 为 editorConfig 签名发送给浏览器由 EuroOffice 校验 */ public String signConfig(MapString, Object payload, String secret) { return JWT.create() .withPayload(payload) .sign(Algorithm.HMAC256(secret)); } /** 校验回调 JWTEuroOffice → 后端 */ SuppressWarnings(unchecked) public MapString, Object verifyCallback(String token, String secret) { DecodedJWT jwt JWT.require(Algorithm.HMAC256(secret)).build().verify(token); String json new String( Base64.getUrlDecoder().decode(jwt.getPayload()), StandardCharsets.UTF_8); return objectMapper.readValue(json, Map.class); } /** 生成下载短 token有时间戳签名防篡改 */ public String createDownloadToken(String docId, String secret, long ttlSeconds) { long exp System.currentTimeMillis() / 1000 ttlSeconds; String raw docId | exp; String sig hmacSha256(raw, secret); return Base64.getUrlEncoder().withoutPadding() .encodeToString((raw | sig).getBytes(StandardCharsets.UTF_8)); } /** 验证下载短 token */ public String verifyDownloadToken(String token, String secret) { String decoded new String( Base64.getUrlDecoder().decode(token), StandardCharsets.UTF_8); String[] p decoded.split(\\|); if (p.length ! 3) throw new IllegalArgumentException(invalid token); long exp Long.parseLong(p[1]); if (System.currentTimeMillis() / 1000 exp) throw new IllegalArgumentException(token expired); if (!hmacSha256(p[0] | p[1], secret).equals(p[2])) throw new IllegalArgumentException(signature mismatch); return p[0]; } private String hmacSha256(String data, String secret) { try { Mac mac Mac.getInstance(HmacSHA256); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HmacSHA256)); StringBuilder sb new StringBuilder(); for (byte b : mac.doFinal(data.getBytes(StandardCharsets.UTF_8))) sb.append(String.format(%02x, b)); return sb.toString(); } catch (Exception e) { throw new IllegalStateException(e); } } }4.5 Controller 对外接口Tag(name EuroOffice协同文档) RestController RequestMapping(/eurooffice) Slf4j RequiredArgsConstructor public class EuroOfficeController extends JeecgControllerEoDocument, IEoDocumentService { private final IEoDocumentService eoDocumentService; /** 文档分页列表 */ GetMapping(/list) public ResultIPageEoDocument list(...) { ... } /** 创建空白文档docx/xlsx/pptx */ PostMapping(/create) public ResultEoDocument create( RequestParam(defaultValue docx) String fileType, RequestParam(required false) String title) { ... } /** 上传已有文件创建文档 */ PostMapping(/upload) public ResultEoDocument upload( RequestParam(file) MultipartFile file, RequestParam(required false) String title) { ... } /** 获取编辑器配置含 JWT 签名 */ GetMapping(/config/{docId}) public ResultMapString, Object config( PathVariable String docId, RequestParam(defaultValue edit) String mode, RequestParam(defaultValue desktop) String type) { LoginUser user (LoginUser) SecurityUtils.getSubject().getPrincipal(); return Result.OK(eoDocumentService.buildEditorPayload(docId, mode, type, user)); } /** 删除文档 */ DeleteMapping(/delete) { ... } /** ---------- 以下为 Document Server 回调接口匿名 ---------- */ /** Document Server 下载源文件短 token 鉴权 */ GetMapping(/file/{docId}) public void download(PathVariable String docId, RequestParam String token, HttpServletResponse response) { eoDocumentService.streamFile(docId, token, response); } /** Document Server 保存回调EuroOffice JWT 鉴权 */ PostMapping(/callback) public MapString, Object callback(RequestParam String docId, RequestBody String body, RequestHeader(value Authorization, required false) String authorization) { return eoDocumentService.handleCallback(docId, body, authorization); } }4.6 Service 核心逻辑public MapString, Object buildEditorPayload(String docId, String mode, String editorType, LoginUser user) { EoDocument doc getById(docId); boolean canEdit !view.equalsIgnoreCase(mode); // 生成下载短 token防爬虫 String downloadToken jwtHelper.createDownloadToken( docId, properties.getJwtSecret(), properties.getDownloadTokenTtlSeconds()); String fileUrl trimSlash(properties.getPublicBaseUrl()) /eurooffice/file/ docId ?token URLEncoder.encode(downloadToken, StandardCharsets.UTF_8); String callbackUrl trimSlash(properties.getPublicBaseUrl()) /eurooffice/callback?docId docId; // 构建 document 部分 MapString, Object document new LinkedHashMap(); document.put(fileType, doc.getFileType()); document.put(key, doc.getEditorKey()); // EuroOffice 用 key 定位文档 document.put(title, doc.getTitle()); document.put(url, fileUrl); document.put(permissions, Map.of( edit, canEdit, comment, true, download, true, print, true )); // 构建 editorConfig 部分 MapString, Object editorConfig new LinkedHashMap(); editorConfig.put(mode, canEdit ? edit : view); editorConfig.put(callbackUrl, callbackUrl); editorConfig.put(lang, zh-CN); editorConfig.put(user, Map.of( id, user ! null ? user.getId() : anonymous, name, user ! null ? user.getRealname() : Guest )); editorConfig.put(customization, Map.of(forcesave, true, autosave, true)); editorConfig.put(coEditing, Map.of(mode, fast, change, true)); // 构建完整配置并签名 MapString, Object config new LinkedHashMap(); config.put(type, desktop); // desktop | mobile config.put(documentType, documentType(doc.getFileType())); // word | cell | slide config.put(document, document); config.put(editorConfig, editorConfig); // JWT 签名EuroOffice 会校验 if (StringUtils.isNotBlank(properties.getJwtSecret())) { config.put(token, jwtHelper.signConfig(config, properties.getJwtSecret())); } MapString, Object result new LinkedHashMap(); result.put(documentServerApiJs, trimSlash(properties.getDocsUrl()) /web-apps/apps/api/documents/api.js?shardkey URLEncoder.encode(doc.getEditorKey(), StandardCharsets.UTF_8)); result.put(config, config); result.put(docId, docId); result.put(title, doc.getTitle()); return result; }2处理 Document Server 回调public MapString, Object handleCallback(String docId, String body, String authorizationHeader) { MapString, Object ok Map.of(error, 0); // 必须返回 error:0 表示成功 try { // JWT 鉴权EuroOffice 回调时带 Authorization: Bearer token if (StringUtils.isNotBlank(properties.getJwtSecret())) { String token extractBearer(authorizationHeader); MapString, Object payload jwtHelper.verifyCallback(token, properties.getJwtSecret()); // 提取 status/key/url } JsonNode root objectMapper.readTree(body); int status root.path(status).asInt(-1); String key root.path(key).asText(null); String url root.path(url).asText(null); // status: 2正常关闭保存 3错误保存 6forcesave if ((status 2 || status 3 || status 6) StringUtils.isNotBlank(url)) { downloadAndSave(doc, url, status 2 || status 3); } return ok; } catch (Exception e) { log.error(EuroOffice callback failed: docId{}, docId, e); return ok; // 仍返回 ok避免编辑器无限重试 } }五、前端集成Vue 35.1 引入编辑器 API5.2 桌面端编辑器组件template div ideurooffice-editor stylewidth:100%;height:100vh; / /template script setup import { ref, onMounted } from vue import axios from axios const props defineProps({ docId: { type: String, required: true }, mode: { type: String, default: edit } // edit | view }) const editorApiJs ref() onMounted(async () { // 1. 获取编辑配置含 JWT 签名 const { data } await axios.get(/eurooffice/config/${props.docId}?mode${props.mode}) const config data.result // 2. 动态加载 api.js await loadScript(config.documentServerApiJs) // 3. 渲染编辑器 new DocsAPI.DocEditor(eurooffice-editor, { ...config.config, width: 100%, height: 100%, events: { onDocumentReady() { console.log(编辑器已就绪) }, onError(err) { console.error(编辑器异常, err) }, // 编辑器关闭时会触发 forcesave 回调后端 } }) }) function loadScript(src) { return new Promise((resolve, reject) { if (window.DocsAPI) return resolve() const s document.createElement(script) s.src src s.onload resolve s.onerror () reject(new Error(加载 api.js 失败)) document.head.appendChild(s) }) } /script六、常见踩坑与排错症状原因解决方案打开编辑器报token invalid前后端jwt-secret不一致检查application.yml和 DockerJWT_SECRET环境变量回调返回 401Shiro 拦截了回调接口在excludeUrls中加入/eurooffice/callback,/eurooffice/file/**Docker 内访问不到host.docker.internalpublic-base-url用了localhost改为host.docker.internal:端口移动端编辑器加载空白type: mobile配置缺失buildEditorPayload传typemobile七、效果pc端小程序

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询