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.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。
* 部分开放平台校验可能使用 POSTJSON 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<String> callbackEndpointInfoPage() {
return KdocsCallbackProbeResponses.callbackReadyPage();
private ResponseEntity<String> callbackEndpointInfoPage(HttpServletRequest request) {
return KdocsCallbackProbeResponses.callbackReadyJson(request, null);
}
}

View File

@@ -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 = "<!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>"
+ "<body style='font-family:sans-serif;text-align:center;padding:40px;color:#333'>"
+ "<h2>金山文档授权回调</h2>"
@@ -23,9 +33,70 @@ public final class KdocsCallbackProbeResponses {
+ "<p>请在系统中点击「连接金山文档」或「授权」后,由金山文档页面自动跳转到此处。</p>"
+ "</body></html>";
public static ResponseEntity<String> callbackReadyPage() {
/**
* 浏览器直接打开回调页时使用Accept 偏 HTML
*/
public static ResponseEntity<String> 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<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 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;
}
}
}