diff --git a/backend/api-gateway/pom.xml b/backend/api-gateway/pom.xml index 96e0854dd..9bc17361c 100644 --- a/backend/api-gateway/pom.xml +++ b/backend/api-gateway/pom.xml @@ -33,6 +33,10 @@ spring-boot-starter-web org.springframework.boot + + spring-ai-starter-mcp-server-webmvc + org.springframework.ai + @@ -57,6 +61,12 @@ com.baomidou mybatis-plus-spring-boot3-starter + + + org.springframework.boot + spring-boot-starter-web + + com.baomidou @@ -109,6 +119,10 @@ mockito-junit-jupiter test + + org.apache.tomcat.embed + tomcat-embed-core + diff --git a/backend/api-gateway/src/main/java/com/datamate/gateway/ApiGatewayApplication.java b/backend/api-gateway/src/main/java/com/datamate/gateway/ApiGatewayApplication.java index 7ab4fdda9..ee2e0b016 100644 --- a/backend/api-gateway/src/main/java/com/datamate/gateway/ApiGatewayApplication.java +++ b/backend/api-gateway/src/main/java/com/datamate/gateway/ApiGatewayApplication.java @@ -7,6 +7,8 @@ import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.ComponentScan.Filter; /** * API Gateway & Auth Service Application @@ -14,7 +16,13 @@ * 提供路由、鉴权、限流等功能 */ @SpringBootApplication -@ComponentScan(basePackages = {"com.datamate"}) +@ComponentScan( + basePackages = {"com.datamate"}, + excludeFilters = @Filter( + type = FilterType.REGEX, + pattern = "com\\.datamate\\.common\\.infrastructure\\.config\\..*" + ) +) @MapperScan(basePackages = {"com.datamate.**.mapper"}) public class ApiGatewayApplication { @@ -68,9 +76,10 @@ public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { .filters(f -> f.stripPrefix(1).prefixPath("/api")) .uri("http://deer-flow-backend:8000")) - // 网关服务(用户) + // 网关内部服务(用户) + // 使用 no-op 触发 GlobalFilter 执行,然后由本地 Controller 处理 .route("gateway", r -> r.path("/api/user/**") - .uri("http://localhost:8080")) + .uri("http://localhost:8080")) // 其他后端服务 .route("default", r -> r.path("/api/**") diff --git a/backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/AuthFilter.java b/backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/AuthFilter.java index 676ec1a09..89b620a54 100644 --- a/backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/AuthFilter.java +++ b/backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/AuthFilter.java @@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -29,7 +30,7 @@ @Slf4j @Component @RequiredArgsConstructor -public class AuthFilter implements GlobalFilter { +public class AuthFilter implements GlobalFilter, Ordered { private static final String AUTH_HEADER = "Authorization"; private static final String TOKEN_PREFIX = "Bearer "; @@ -95,4 +96,14 @@ private Mono sendUnauthorizedResponse(ServerWebExchange exchange) { DataBuffer buffer = response.bufferFactory().wrap(bytes); return response.writeWith(Mono.just(buffer)); } + + /** + * JWT 认证优先级低于 SSO + * + * @return order value (2 = lower priority than SSO filter) + */ + @Override + public int getOrder() { + return 2; + } } diff --git a/backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/OmsAuthFilter.java b/backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/OmsAuthFilter.java index 0e182dcd1..f260efb16 100644 --- a/backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/OmsAuthFilter.java +++ b/backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/OmsAuthFilter.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -20,13 +21,13 @@ /** * OmsAuthFilter is a global filter that authenticates requests to the OMS service. - * + * * @author songyongtan * @date 2026-03-16 */ @Slf4j @Component -public class OmsAuthFilter implements GlobalFilter { +public class OmsAuthFilter implements GlobalFilter, Ordered { private static final String USER_NAME_HEADER = "X-User-Name"; private static final String USER_GROUP_ID_HEADER = "X-User-Group-Id"; private static final String AUTH_TOKEN_KEY = "__Host-X-Auth-Token"; @@ -122,7 +123,7 @@ private String getRealIp(ServerHttpRequest request) { /** * getToken gets the token value from cookies. - * + * * @param cookies the cookies map * @param tokenKey the token key * @return the token value @@ -133,4 +134,14 @@ private String getToken(MultiValueMap cookies, String tokenK } return ""; } + + /** + * SSO 认证优先级最高 + * + * @return order value (1 = highest priority for auth filters) + */ + @Override + public int getOrder() { + return 1; + } } diff --git a/backend/api-gateway/src/main/java/com/datamate/gateway/infrastructure/client/impl/OmsServiceImpl.java b/backend/api-gateway/src/main/java/com/datamate/gateway/infrastructure/client/impl/OmsServiceImpl.java index 40cbaff1e..c1ec584c5 100644 --- a/backend/api-gateway/src/main/java/com/datamate/gateway/infrastructure/client/impl/OmsServiceImpl.java +++ b/backend/api-gateway/src/main/java/com/datamate/gateway/infrastructure/client/impl/OmsServiceImpl.java @@ -21,7 +21,7 @@ /** * OmsServiceImpl is a service that interacts with the OMS service. - * + * * @author songyongtan * @date 2026-03-16 */ @@ -66,7 +66,6 @@ public String getUserNameFromOms(String authToken, String csrfToken, String real CloseableHttpResponse response = httpClient.execute(httpPost); String responseBody = EntityUtils.toString(response.getEntity()); - log.info("response code: {}", response.getCode()); try { JSONObject jsonObject = JSON.parseObject(responseBody); diff --git a/backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/dto/UserResponse.java b/backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/dto/UserResponse.java new file mode 100644 index 000000000..16d6f3079 --- /dev/null +++ b/backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/dto/UserResponse.java @@ -0,0 +1,45 @@ +package com.datamate.gateway.interfaces.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户信息响应 + * + * 支持双认证模式: + * - SSO: 通过 OMS 单点登录 + * - JWT: 通过本地 JWT Token 认证 + * - NONE: 未登录 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserResponse { + /** + * 用户名 + */ + private String username; + + /** + * 邮箱(JWT 模式可用) + */ + private String email; + + /** + * 用户组 ID(SSO 模式可用) + */ + private String groupId; + + /** + * 是否已认证 + */ + private Boolean authenticated; + + /** + * 认证模式 + */ + private String authMode; // "SSO" | "JWT" | "NONE" +} diff --git a/backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/rest/UserController.java b/backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/rest/UserController.java index b755be065..79afd82ca 100644 --- a/backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/rest/UserController.java +++ b/backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/rest/UserController.java @@ -4,19 +4,28 @@ import com.datamate.common.infrastructure.common.Response; import com.datamate.common.infrastructure.exception.CommonErrorCode; import com.datamate.gateway.application.UserApplicationService; +import com.datamate.gateway.domain.service.UserService; +import com.datamate.gateway.infrastructure.client.OmsExtensionService; +import com.datamate.gateway.infrastructure.client.OmsService; import com.datamate.gateway.interfaces.dto.LoginRequest; import com.datamate.gateway.interfaces.dto.LoginResponse; import com.datamate.gateway.interfaces.dto.RegisterRequest; +import com.datamate.gateway.interfaces.dto.UserResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.MultiValueMap; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpCookie; /** * UserController @@ -30,6 +39,45 @@ @RequiredArgsConstructor public class UserController { private final UserApplicationService userApplicationService; + private final UserService userService; + private final OmsService omsService; + private final OmsExtensionService omsExtensionService; + + private static final String AUTH_TOKEN_KEY = "__Host-X-Auth-Token"; + private static final String CSRF_TOKEN_KEY = "__Host-X-Csrf-Token"; + + /** + * 从 cookies 中获取 token 值 + */ + private String getToken(MultiValueMap cookies, String tokenKey) { + if (cookies.containsKey(tokenKey)) { + return cookies.getFirst(tokenKey).getValue(); + } + return ""; + } + + /** + * 获取真实 IP 地址 + */ + private String getRealIp(ServerHttpRequest request) { + String ip = request.getHeaders().getFirst("X-Real-IP"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeaders().getFirst("X-Forwarded-For"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeaders().getFirst("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeaders().getFirst("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddress() != null ? request.getRemoteAddress().getAddress().getHostAddress() : ""; + } + if (ip != null && ip.contains(",")) { + ip = ip.split(",")[0].trim(); + } + return ip != null ? ip : ""; + } @PostMapping("/login") @IgnoreResponseWrap @@ -48,4 +96,84 @@ public ResponseEntity> register(@Valid @RequestBody Regi .orElseGet(() -> ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Response.error(CommonErrorCode.SIGNUP_ERROR))); } + + /** + * 获取当前登录用户信息(支持双模式) + * 优先级: + * 1. SSO 模式:从 cookies 读取 OMS token 并调用 OMS 服务验证 + * 2. JWT 模式:检查 Authorization Bearer Token + * 3. 未登录:返回 authenticated=false + * + * @param request HTTP 请求 + * @return 用户信息(包含认证模式) + */ + @GetMapping("/me") + public Response getCurrentUser(ServerHttpRequest request) { + log.info("=== /api/user/me called ==="); + + // 优先检查 SSO 模式(从 cookies 读取 OMS token) + MultiValueMap cookies = request.getCookies(); + String authToken = getToken(cookies, AUTH_TOKEN_KEY); + String csrfToken = getToken(cookies, CSRF_TOKEN_KEY); + + log.info("Cookies present - __Host-X-Auth-Token: {}, __Host-X-Csrf-Token: {}", + StringUtils.isNotBlank(authToken), StringUtils.isNotBlank(csrfToken)); + + if (StringUtils.isNotBlank(authToken)) { + try { + // 获取真实 IP + String realIp = getRealIp(request); + log.info("Calling OMS service with realIp: {}", realIp); + + // 调用 OMS 服务验证 + String username = omsService.getUserNameFromOms(authToken, csrfToken, realIp); + if (StringUtils.isNotBlank(username)) { + log.info("SSO mode: user={}", username); + + // 获取用户组 ID(可能为 null) + String groupId = null; + try { + groupId = omsExtensionService.getUserGroupId(username); + log.info("User groupId: {}", groupId); + } catch (Exception e) { + log.warn("Failed to get user group ID: {}", e.getMessage()); + } + + return Response.ok(UserResponse.builder() + .username(username) + .groupId(groupId) + .authenticated(true) + .authMode("SSO") + .build()); + } else { + log.warn("OMS service returned null username"); + } + } catch (Exception e) { + log.error("SSO authentication failed", e); + } + } + + // 检查独立登录模式(JWT Token) + String authHeader = request.getHeaders().getFirst("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + String username = userService.validateToken(token); + + if (StringUtils.isNotBlank(username)) { + log.info("JWT mode: user={}", username); + return Response.ok(UserResponse.builder() + .username(username) + .authenticated(true) + .authMode("JWT") + .build()); + } + } + + // 未登录 + log.debug("User not authenticated"); + return Response.ok(UserResponse.builder() + .authenticated(false) + .authMode("NONE") + .build()); + } } diff --git a/backend/api-gateway/src/main/resources/application.yml b/backend/api-gateway/src/main/resources/application.yml index 99167734b..056397b45 100644 --- a/backend/api-gateway/src/main/resources/application.yml +++ b/backend/api-gateway/src/main/resources/application.yml @@ -1,6 +1,7 @@ spring: main: allow-circular-references: true + allow-bean-definition-overriding: true application: name: datamate-gateway # 必须设置应用名 diff --git a/deployment/helm/datamate/values.yaml b/deployment/helm/datamate/values.yaml index e990d04fd..b2843e9d1 100644 --- a/deployment/helm/datamate/values.yaml +++ b/deployment/helm/datamate/values.yaml @@ -169,6 +169,8 @@ gateway: value: "default-insecure-key-change-in-production" - name: datamate.jwt.enable value: *DATAMATE_JWT_ENABLE + - name: OMS_AUTH_ENABLED + value: "false" volumes: - *logVolume volumeMounts: diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 98806f31d..b15ca689c 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -7,15 +7,22 @@ "actions": { "login": "Login", "register": "Register", - "logout": "Logout" + "logout": "Logout", + "gotoLogin": "Go to Login" }, + "authMode": { + "sso": "SSO Login", + "jwt": "JWT Login" + }, + "group": "Group", "messages": { "loginSuccess": "Login successful", "loginFailed": "Login failed, please try again later", "signupSuccess": "Registration successful, auto-login", "signupFailed": "Registration failed, please try again later", "logoutSuccess": "Logged out successfully", - "passwordMismatch": "Passwords do not match" + "passwordMismatch": "Passwords do not match", + "useSSO": "Please use ModelEngine single sign-on to register" }, "loginDialog": { "title": "Login", diff --git a/frontend/src/i18n/locales/zh/common.json b/frontend/src/i18n/locales/zh/common.json index 34521c6a7..f738656ee 100644 --- a/frontend/src/i18n/locales/zh/common.json +++ b/frontend/src/i18n/locales/zh/common.json @@ -7,15 +7,22 @@ "actions": { "login": "登录", "register": "注册", - "logout": "退出登录" + "logout": "退出登录", + "gotoLogin": "前往登录" }, + "authMode": { + "sso": "单点登录", + "jwt": "令牌登录" + }, + "group": "用户组", "messages": { "loginSuccess": "登录成功", "loginFailed": "登录失败,请稍后重试", "signupSuccess": "注册成功,已自动登录", "signupFailed": "注册失败,请稍后重试", "logoutSuccess": "已退出登录", - "passwordMismatch": "两次输入的密码不一致" + "passwordMismatch": "两次输入的密码不一致", + "useSSO": "请使用 ModelEngine 单点登录进行注册" }, "loginDialog": { "title": "登录", diff --git a/frontend/src/pages/Layout/Header.tsx b/frontend/src/pages/Layout/Header.tsx index 71f6a95da..37359fb0c 100644 --- a/frontend/src/pages/Layout/Header.tsx +++ b/frontend/src/pages/Layout/Header.tsx @@ -1,14 +1,22 @@ -import { User, Globe, LogIn, UserPlus, Sparkles } from "lucide-react" +import { User, Globe, LogIn, UserPlus, Sparkles, Shield } from "lucide-react" import { memo, useState, useEffect } from "react"; import { NavLink } from "react-router"; import { Button, Dropdown, message } from "antd" import type { MenuProps } from 'antd' import { LoginDialog } from "./LoginDialog" import { SignupDialog } from "./SignupDialog" -import { post} from "@/utils/request.ts"; +import { post, get } from "@/utils/request.ts"; import { useTranslation } from "react-i18next"; import i18n from "@/i18n"; +interface UserResponse { + username: string; + email?: string; + groupId?: string; + authenticated: boolean; + authMode: 'SSO' | 'JWT' | 'NONE'; +} + function loginUsingPost(data: any) { return post("/api/user/login", data); } @@ -17,11 +25,22 @@ function signupUsingPost(data: any) { return post("/api/user/signup", data); } +function getCurrentUser() { + return get("/api/user/me"); +} + +// ME 登录 URL(根据实际环境修改) +const ME_LOGIN_URL = process.env.VITE_ME_LOGIN_URL || 'https://modelengine.com/login'; +const OMS_LOGOUT_URL = process.env.VITE_OMS_LOGOUT_URL || 'https://oms-service/logout'; + export function Header() { const { t } = useTranslation(); const [loginOpen, setLoginOpen] = useState(false) const [signupOpen, setSignupOpen] = useState(false) const [loading, setLoading] = useState(false); + const [currentUser, setCurrentUser] = useState(null); + const [authMode, setAuthMode] = useState<'SSO' | 'JWT' | 'NONE'>('NONE'); + const [userLoading, setUserLoading] = useState(true); const handleLogin = async (values: { username: string; password: string }) => { try { @@ -69,10 +88,29 @@ export function Header() { }; const handleLogout = () => { - localStorage.removeItem('session'); - message.success(t('user.messages.logoutSuccess')); - // Refresh the page after logout - window.location.reload(); + if (authMode === 'SSO') { + // SSO 模式:检查是否配置了有效的登出 URL + const logoutUrl = OMS_LOGOUT_URL; + + // 如果配置的是默认值(内部 service 名称),只清除本地状态 + if (logoutUrl.includes('oms-service') || logoutUrl.includes('localhost')) { + console.warn('OMS logout URL not configured or using internal address, skipping redirect'); + setCurrentUser(null); + setAuthMode('NONE'); + message.success(t('user.messages.logoutSuccess')); + window.location.reload(); + } else { + // 使用配置的登出 URL + window.location.href = `${logoutUrl}?redirect=${encodeURIComponent(window.location.href)}`; + } + } else { + // JWT 模式:清除本地 session + localStorage.removeItem('session'); + setCurrentUser(null); + setAuthMode('NONE'); + message.success(t('user.messages.logoutSuccess')); + window.location.reload(); + } }; const openLoginDialog = () => { @@ -83,12 +121,47 @@ export function Header() { setSignupOpen(true); }; + // 检测是否在 ME 环境 + const isSSOAvailable = () => { + const hostname = window.location.hostname; + // 通过域名或注入的全局变量判断 + return hostname.includes('modelengine') || + hostname.includes('me-platform') || + (window as any).__ME_ENV__ === true; + }; + + // 获取当前用户信息(支持双模式) useEffect(() => { - window.addEventListener('show-login', openLoginDialog); + const fetchCurrentUser = async () => { + try { + const response = await getCurrentUser(); + if (response.data) { + setCurrentUser(response.data); + setAuthMode(response.data.authMode); - return () => { - window.removeEventListener('show-login', openLoginDialog); + // 如果未登录,根据模式处理 + if (!response.data.authenticated) { + if (isSSOAvailable()) { + // SSO 模式:自动跳转到 ME 登录 + console.log('SSO mode detected, redirecting to ME login...'); + // 不自动跳转,等待用户点击登录按钮 + } else { + // JWT 模式:保持未登录状态 + console.log('JWT mode, waiting for user to login'); + } + } else { + console.log(`User authenticated via ${response.data.authMode}:`, response.data.username); + } + } + } catch (error) { + console.error('Failed to fetch current user:', error); + // 请求失败时,保持未登录状态 + } finally { + setUserLoading(false); + } }; + + fetchCurrentUser(); }, []); const languageMenuItems: MenuProps['items'] = [ @@ -110,28 +183,68 @@ export function Header() { } ] - const userDropdownItems: MenuProps['items'] = localStorage.getItem("session") + const userDropdownItems: MenuProps['items'] = currentUser?.authenticated ? [ - { - key: 'profile', - label: JSON.parse(localStorage.getItem("session") as string).email, - }, - { - type: 'divider', - }, - { - key: 'logout', - label: t('user.actions.logout'), - icon: , - onClick: handleLogout, - }, - ] + { + key: 'profile', + label: ( +
+ {currentUser.username} + + {authMode === 'SSO' ? ( + + + {t('user.authMode.sso')} + + ) : ( + + + {t('user.authMode.jwt')} + + )} + +
+ ), + }, + currentUser.groupId && { + key: 'groupId', + label: `${t('user.group')}: ${currentUser.groupId}`, + disabled: true, + }, + currentUser.email && { + key: 'email', + label: currentUser.email, + disabled: true, + }, + // 只有 JWT 模式才显示退出登录按钮 + ...(authMode !== 'SSO' ? [ + { + type: 'divider', + }, + { + key: 'logout', + label: t('user.actions.logout'), + icon: , + onClick: handleLogout, + }, + ] : []), + ] : [ { key: 'login', - label: t('user.actions.login'), + label: authMode === 'SSO' || isSSOAvailable() + ? t('user.actions.gotoLogin') + : t('user.actions.login'), icon: , - onClick: () => setLoginOpen(true), + onClick: () => { + if (authMode === 'SSO' || isSSOAvailable()) { + // SSO 模式:跳转到 ME 登录 + window.location.href = `${ME_LOGIN_URL}?redirect=${encodeURIComponent(window.location.href)}`; + } else { + // JWT 模式:显示登录对话框 + setLoginOpen(true); + } + }, }, { type: 'divider', @@ -140,7 +253,13 @@ export function Header() { key: 'register', label: t('user.actions.register'), icon: , - onClick: () => setSignupOpen(true), + onClick: () => { + if (authMode === 'SSO' || isSSOAvailable()) { + message.info(t('user.messages.useSSO')); + } else { + setSignupOpen(true); + } + }, }, ]; @@ -175,10 +294,10 @@ export function Header() { placement="bottomRight" overlayClassName="w-40" > - diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index ef92fef93..3de3b4735 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -280,6 +280,8 @@ class Request { const config = { method: "GET", + credentials: "include", + mode: "cors", headers: { ...this.defaultHeaders, ...options.headers, @@ -299,6 +301,8 @@ class Request { async post(url, data = {}, options = {}) { let config = { method: "POST", + credentials: "include", + mode: "cors", headers: { ...this.defaultHeaders, ...options.headers, @@ -311,6 +315,8 @@ class Request { if (isFormData) { config = { method: "POST", + credentials: "include", + mode: "cors", headers: { ...options.headers, // FormData不需要Content-Type }, @@ -330,6 +336,8 @@ class Request { async put(url, data = null, options = {}) { const config = { method: "PUT", + credentials: "include", + mode: "cors", headers: { ...this.defaultHeaders, ...options.headers,