From 918f737c9485e6279a7dd6fe7c796017ec2ee524 Mon Sep 17 00:00:00 2001 From: van Date: Mon, 23 Mar 2026 23:22:20 +0800 Subject: [PATCH] 1 --- .../jarvis/KdocsCallbackController.java | 62 ++++++++++++--- .../jarvis/KdocsCallbackProbeResponses.java | 79 ++++++++++++++++++- ...s365ToKdocsCallbackRedirectController.java | 28 ++++++- 3 files changed, 152 insertions(+), 17 deletions(-) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/KdocsCallbackController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/KdocsCallbackController.java index e0c7dfb..3340249 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/KdocsCallbackController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/KdocsCallbackController.java @@ -14,6 +14,13 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; + /** * 金山文档 OAuth 回调(独立路径,避免前端路由拦截) * 回调地址示例:https://your-domain/kdocs-callback @@ -30,26 +37,57 @@ public class KdocsCallbackController extends BaseController { @Anonymous @GetMapping - public ResponseEntity oauthCallbackGet(@RequestParam(value = "code", required = false) String code, + public ResponseEntity oauthCallbackGet(HttpServletRequest request, + @RequestParam(value = "code", required = false) String code, @RequestParam(value = "state", required = false) String state, @RequestParam(value = "error", required = false) String error, @RequestParam(value = "error_description", required = false) String errorDescription) { - return handleOAuthCallback(code, state, error, errorDescription); + return handleOAuthCallback(request, code, state, error, errorDescription); } /** - * 部分开放平台校验可能使用 POST。 + * 部分开放平台校验可能使用 POST;JSON body 时需回显 challenge 等字段。 */ @Anonymous @PostMapping - public ResponseEntity oauthCallbackPost(@RequestParam(value = "code", required = false) String code, - @RequestParam(value = "state", required = false) String state, - @RequestParam(value = "error", required = false) String error, - @RequestParam(value = "error_description", required = false) String errorDescription) { - return handleOAuthCallback(code, state, error, errorDescription); + public ResponseEntity oauthCallbackPost(HttpServletRequest request) throws IOException { + String ct = StringUtils.defaultString(request.getContentType()).toLowerCase(); + if (ct.contains("application/json")) { + StringBuilder sb = new StringBuilder(); + try (BufferedReader r = request.getReader()) { + char[] buf = new char[4096]; + int n; + while ((n = r.read(buf)) != -1) { + sb.append(buf, 0, n); + } + } + String raw = sb.toString(); + if (StringUtils.isNotBlank(raw)) { + try { + JSONObject o = JSON.parseObject(raw); + if (o != null && o.containsKey("code")) { + Object cv = o.get("code"); + if (cv != null) { + String c = String.valueOf(cv); + if (StringUtils.isNotBlank(c) && !"null".equals(c)) { + return handleOAuthCallback(request, c, o.getString("state"), o.getString("error"), o.getString("error_description")); + } + } + } + } catch (Exception e) { + log.debug("解析 OAuth POST JSON: {}", e.getMessage()); + } + } + return KdocsCallbackProbeResponses.callbackReadyJson(request, raw); + } + String code = request.getParameter("code"); + String state = request.getParameter("state"); + String error = request.getParameter("error"); + String errorDescription = request.getParameter("error_description"); + return handleOAuthCallback(request, code, state, error, errorDescription); } - private ResponseEntity handleOAuthCallback(String code, String state, String error, String errorDescription) { + private ResponseEntity handleOAuthCallback(HttpServletRequest request, String code, String state, String error, String errorDescription) { try { if (error != null) { String msg = errorDescription != null ? errorDescription : error; @@ -58,7 +96,7 @@ public class KdocsCallbackController extends BaseController { } // 无 code:多为平台校验回调可达性,或用户直接打开本地址(非授权失败) if (StringUtils.isBlank(code)) { - return callbackEndpointInfoPage(); + return callbackEndpointInfoPage(request); } log.info("金山文档授权回调 code 已收到 state={}", state); KdocsTokenInfo tokenInfo = kdocsOAuthService.getAccessTokenByCode(code); @@ -100,7 +138,7 @@ public class KdocsCallbackController extends BaseController { /** * 无授权参数时的占位页:HTTP 200,避免被误判为「回调不可用」,也不向 opener 误发失败消息。 */ - private ResponseEntity callbackEndpointInfoPage() { - return KdocsCallbackProbeResponses.callbackReadyPage(); + private ResponseEntity callbackEndpointInfoPage(HttpServletRequest request) { + return KdocsCallbackProbeResponses.callbackReadyJson(request, null); } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/KdocsCallbackProbeResponses.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/KdocsCallbackProbeResponses.java index 1a9c3f6..9fd522a 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/KdocsCallbackProbeResponses.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/KdocsCallbackProbeResponses.java @@ -1,21 +1,31 @@ package com.ruoyi.web.controller.jarvis; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.utils.StringUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; + /** - * 开放平台校验回调 URL 时多为 GET、无 code,需直接 200;勿对校验请求返回 302。 + * 金山文档 ChallengeURLValidator 等校验会解析响应为 JSON;返回 HTML 会报 unmarshal challenge response json invalid。 + * 无 OAuth code 时返回与开放平台风格接近的 JSON,并回显 query / JSON body 中的字段(如 challenge)。 */ public final class KdocsCallbackProbeResponses { private KdocsCallbackProbeResponses() { } + private static final MediaType JSON_UTF8 = MediaType.parseMediaType("application/json;charset=UTF-8"); + private static final MediaType HTML_UTF8 = MediaType.parseMediaType("text/html;charset=UTF-8"); - private static final String BODY = "" + private static final String HTML_BODY = "" + "金山文档授权回调" + "" + "

金山文档授权回调

" @@ -23,9 +33,70 @@ public final class KdocsCallbackProbeResponses { + "

请在系统中点击「连接金山文档」或「授权」后,由金山文档页面自动跳转到此处。

" + ""; - public static ResponseEntity callbackReadyPage() { + /** + * 浏览器直接打开回调页时使用(Accept 偏 HTML)。 + */ + public static ResponseEntity callbackReadyHtmlPage() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(HTML_UTF8); - return new ResponseEntity<>(BODY, headers, HttpStatus.OK); + return new ResponseEntity<>(HTML_BODY, headers, HttpStatus.OK); } + + /** + * 平台 URL 校验:合法 JSON,兼容 WebOffice/开放平台常见 envelope,并回显校验参数。 + * + * @param jsonBody POST application/json 时的原始 body,可为 null + */ + public static ResponseEntity callbackReadyJson(HttpServletRequest request, String jsonBody) { + JSONObject data = new JSONObject(); + + Enumeration names = request.getParameterNames(); + while (names.hasMoreElements()) { + String n = names.nextElement(); + data.put(n, request.getParameter(n)); + } + + mergeJsonPrimitivesIntoData(data, jsonBody); + + JSONObject root = new JSONObject(); + root.put("code", 0); + root.put("message", ""); + root.put("result", "ok"); + root.put("data", data); + + if (data.containsKey("challenge")) { + root.put("challenge", data.get("challenge")); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(JSON_UTF8); + return new ResponseEntity<>(root.toJSONString(), headers, HttpStatus.OK); + } + + private static void mergeJsonPrimitivesIntoData(JSONObject data, String jsonBody) { + if (StringUtils.isBlank(jsonBody)) { + return; + } + try { + JSONObject in = JSON.parseObject(jsonBody); + if (in == null) { + return; + } + for (String k : in.keySet()) { + Object v = in.get(k); + if (v == null) { + continue; + } + if (v instanceof JSONObject || v instanceof JSONArray) { + continue; + } + if (!data.containsKey(k)) { + data.put(k, v); + } + } + } catch (Exception ignored) { + // 非 JSON 则忽略 + } + } + } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/Wps365ToKdocsCallbackRedirectController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/Wps365ToKdocsCallbackRedirectController.java index c610418..f4379ce 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/Wps365ToKdocsCallbackRedirectController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/Wps365ToKdocsCallbackRedirectController.java @@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; import java.net.URI; /** @@ -41,11 +43,35 @@ public class Wps365ToKdocsCallbackRedirectController { private ResponseEntity handleWps365(HttpServletRequest request, String code, String error) { if (StringUtils.isBlank(code) && StringUtils.isBlank(error)) { - return KdocsCallbackProbeResponses.callbackReadyPage(); + String jsonBody = readJsonBodyIfPost(request); + return KdocsCallbackProbeResponses.callbackReadyJson(request, jsonBody); } String q = request.getQueryString(); HttpHeaders headers = new HttpHeaders(); headers.setLocation(URI.create(KdocsCallbackUrlBuilder.absoluteKdocsCallback(request, q))); return new ResponseEntity<>(null, headers, HttpStatus.FOUND); } + + private static String readJsonBodyIfPost(HttpServletRequest request) { + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return null; + } + String ct = StringUtils.defaultString(request.getContentType()).toLowerCase(); + if (!ct.contains("application/json")) { + return null; + } + try { + StringBuilder sb = new StringBuilder(); + try (BufferedReader r = request.getReader()) { + char[] buf = new char[4096]; + int n; + while ((n = r.read(buf)) != -1) { + sb.append(buf, 0, n); + } + } + return sb.toString(); + } catch (IOException e) { + return null; + } + } }