Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions backend/api-gateway/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
<artifactId>spring-boot-starter-web</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<groupId>org.springframework.ai</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
Expand All @@ -57,6 +61,12 @@
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
Expand Down Expand Up @@ -109,6 +119,10 @@
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@
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
* 统一的API网关和认证授权微服务
* 提供路由、鉴权、限流等功能
*/
@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 {

Expand Down Expand Up @@ -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/**")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ";
Expand Down Expand Up @@ -95,4 +96,14 @@ private Mono<Void> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -133,4 +134,14 @@ private String getToken(MultiValueMap<String, HttpCookie> cookies, String tokenK
}
return "";
}

/**
* SSO 认证优先级最高
*
* @return order value (1 = highest priority for auth filters)
*/
@Override
public int getOrder() {
return 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

/**
* OmsServiceImpl is a service that interacts with the OMS service.
*
*
* @author songyongtan
* @date 2026-03-16
*/
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, HttpCookie> 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
Expand All @@ -48,4 +96,84 @@ public ResponseEntity<Response<LoginResponse>> 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<UserResponse> getCurrentUser(ServerHttpRequest request) {
log.info("=== /api/user/me called ===");

// 优先检查 SSO 模式(从 cookies 读取 OMS token)
MultiValueMap<String, HttpCookie> 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());
}
}
1 change: 1 addition & 0 deletions backend/api-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
spring:
main:
allow-circular-references: true
allow-bean-definition-overriding: true
application:
name: datamate-gateway # 必须设置应用名

Expand Down
2 changes: 2 additions & 0 deletions deployment/helm/datamate/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading