This commit is contained in:
van
2026-03-23 23:22:20 +08:00
parent ef286d3bd2
commit 918f737c94
3 changed files with 152 additions and 17 deletions

View File

@@ -14,6 +14,13 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; 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 回调(独立路径,避免前端路由拦截) * 金山文档 OAuth 回调(独立路径,避免前端路由拦截)
* 回调地址示例https://your-domain/kdocs-callback * 回调地址示例https://your-domain/kdocs-callback
@@ -30,26 +37,57 @@ public class KdocsCallbackController extends BaseController {
@Anonymous @Anonymous
@GetMapping @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 = "state", required = false) String state,
@RequestParam(value = "error", required = false) String error, @RequestParam(value = "error", required = false) String error,
@RequestParam(value = "error_description", required = false) String errorDescription) { @RequestParam(value = "error_description", required = false) String errorDescription) {
return handleOAuthCallback(code, state, error, errorDescription); return handleOAuthCallback(request, code, state, error, errorDescription);
} }
/** /**
* 部分开放平台校验可能使用 POST。 * 部分开放平台校验可能使用 POSTJSON body 时需回显 challenge 等字段
*/ */
@Anonymous @Anonymous
@PostMapping @PostMapping
public ResponseEntity<?> oauthCallbackPost(@RequestParam(value = "code", required = false) String code, public ResponseEntity<?> oauthCallbackPost(HttpServletRequest request) throws IOException {
@RequestParam(value = "state", required = false) String state, String ct = StringUtils.defaultString(request.getContentType()).toLowerCase();
@RequestParam(value = "error", required = false) String error, if (ct.contains("application/json")) {
@RequestParam(value = "error_description", required = false) String errorDescription) { StringBuilder sb = new StringBuilder();
return handleOAuthCallback(code, state, error, errorDescription); 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 { try {
if (error != null) { if (error != null) {
String msg = errorDescription != null ? errorDescription : error; String msg = errorDescription != null ? errorDescription : error;
@@ -58,7 +96,7 @@ public class KdocsCallbackController extends BaseController {
} }
// 无 code多为平台校验回调可达性或用户直接打开本地址非授权失败 // 无 code多为平台校验回调可达性或用户直接打开本地址非授权失败
if (StringUtils.isBlank(code)) { if (StringUtils.isBlank(code)) {
return callbackEndpointInfoPage(); return callbackEndpointInfoPage(request);
} }
log.info("金山文档授权回调 code 已收到 state={}", state); log.info("金山文档授权回调 code 已收到 state={}", state);
KdocsTokenInfo tokenInfo = kdocsOAuthService.getAccessTokenByCode(code); KdocsTokenInfo tokenInfo = kdocsOAuthService.getAccessTokenByCode(code);
@@ -100,7 +138,7 @@ public class KdocsCallbackController extends BaseController {
/** /**
* 无授权参数时的占位页HTTP 200避免被误判为「回调不可用」也不向 opener 误发失败消息。 * 无授权参数时的占位页HTTP 200避免被误判为「回调不可用」也不向 opener 误发失败消息。
*/ */
private ResponseEntity<String> callbackEndpointInfoPage() { private ResponseEntity<String> callbackEndpointInfoPage(HttpServletRequest request) {
return KdocsCallbackProbeResponses.callbackReadyPage(); return KdocsCallbackProbeResponses.callbackReadyJson(request, null);
} }
} }

View File

@@ -1,21 +1,31 @@
package com.ruoyi.web.controller.jarvis; 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.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; 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 { public final class KdocsCallbackProbeResponses {
private 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 MediaType HTML_UTF8 = MediaType.parseMediaType("text/html;charset=UTF-8");
private static final String BODY = "<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='robots' content='noindex'>" private static final String HTML_BODY = "<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='robots' content='noindex'>"
+ "<title>金山文档授权回调</title></head>" + "<title>金山文档授权回调</title></head>"
+ "<body style='font-family:sans-serif;text-align:center;padding:40px;color:#333'>" + "<body style='font-family:sans-serif;text-align:center;padding:40px;color:#333'>"
+ "<h2>金山文档授权回调</h2>" + "<h2>金山文档授权回调</h2>"
@@ -23,9 +33,70 @@ public final class KdocsCallbackProbeResponses {
+ "<p>请在系统中点击「连接金山文档」或「授权」后,由金山文档页面自动跳转到此处。</p>" + "<p>请在系统中点击「连接金山文档」或「授权」后,由金山文档页面自动跳转到此处。</p>"
+ "</body></html>"; + "</body></html>";
public static ResponseEntity<String> callbackReadyPage() { /**
* 浏览器直接打开回调页时使用Accept 偏 HTML
*/
public static ResponseEntity<String> callbackReadyHtmlPage() {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(HTML_UTF8); 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<String> callbackReadyJson(HttpServletRequest request, String jsonBody) {
JSONObject data = new JSONObject();
Enumeration<String> 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 则忽略
}
}
} }

View File

@@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URI; import java.net.URI;
/** /**
@@ -41,11 +43,35 @@ public class Wps365ToKdocsCallbackRedirectController {
private ResponseEntity<?> handleWps365(HttpServletRequest request, String code, String error) { private ResponseEntity<?> handleWps365(HttpServletRequest request, String code, String error) {
if (StringUtils.isBlank(code) && StringUtils.isBlank(error)) { if (StringUtils.isBlank(code) && StringUtils.isBlank(error)) {
return KdocsCallbackProbeResponses.callbackReadyPage(); String jsonBody = readJsonBodyIfPost(request);
return KdocsCallbackProbeResponses.callbackReadyJson(request, jsonBody);
} }
String q = request.getQueryString(); String q = request.getQueryString();
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(KdocsCallbackUrlBuilder.absoluteKdocsCallback(request, q))); headers.setLocation(URI.create(KdocsCallbackUrlBuilder.absoluteKdocsCallback(request, q)));
return new ResponseEntity<>(null, headers, HttpStatus.FOUND); 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;
}
}
} }