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
50 changes: 50 additions & 0 deletions backend/api-gateway/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,31 @@
<properties>
<spring-boot.version>3.5.6</spring-boot.version>
<spring-cloud.version>2025.0.0</spring-cloud.version>
<jjwt.version>0.11.5</jjwt.version>
</properties>

<dependencies>
<dependency>
<groupId>com.datamate</groupId>
<artifactId>domain-common</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-web</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!-- 响应式安全 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Log4j2 API -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand All @@ -36,6 +54,38 @@
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>

<!-- 使用新版本的 JJWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- 或 jjwt-gson -->
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package com.datamate.gateway;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;

/**
* API Gateway & Auth Service Application
* 统一的API网关和认证授权微服务
* 提供路由、鉴权、限流等功能
*/
@SpringBootApplication
@ComponentScan(basePackages = {"com.datamate"})
@MapperScan(basePackages = {"com.datamate.**.mapper"})
public class ApiGatewayApplication {

public static void main(String[] args) {
Expand Down Expand Up @@ -47,6 +51,10 @@ public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
.filters(f -> f.stripPrefix(1).prefixPath("/api"))
.uri("http://deer-flow-backend:8000"))

// 网关服务(用户)
.route("gateway", r -> r.path("/api/user/**")
.uri("http://localhost:8080"))

// 其他后端服务
.route("default", r -> r.path("/api/**")
.uri("http://datamate-backend:8080"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.datamate.gateway.application;

import com.datamate.gateway.domain.entity.User;
import com.datamate.gateway.domain.service.UserService;
import com.datamate.gateway.interfaces.dto.LoginRequest;
import com.datamate.gateway.interfaces.dto.LoginResponse;
import com.datamate.gateway.interfaces.dto.RegisterRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

/**
* UserApplicationServices
*
* @since 2026/1/14
*/
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class UserApplicationService {
private final UserService userService;

public Optional<LoginResponse> login(LoginRequest loginRequest) {
User user = new User();
user.setUsername(loginRequest.getUsername());
user.setPassword(loginRequest.getPassword());

Optional<User> authenticatedUser = userService.authenticate(user);
if (authenticatedUser.isPresent()) {
User userEntity = authenticatedUser.get();
return Optional.of(convertToLoginResponse(userEntity));
}
return Optional.empty();
}

/**
* Register a new user
*
* @param registerRequest registration request
* @return LoginResponse with user details and token if registration successful, empty otherwise
*/
public Optional<LoginResponse> register(RegisterRequest registerRequest) {
return userService.register(registerRequest)
.map(this::convertToLoginResponse);
}

private LoginResponse convertToLoginResponse(User user) {
return LoginResponse.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.token(user.getToken())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.datamate.gateway.common.config;

import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* JwtConfig
*
* @since 2026/1/14
*/
@Getter
@Setter
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "datamate.jwt")
public class JwtConfig {
private String secret;

@PostConstruct
public void validate() {
if (secret == null || secret.trim().isEmpty()) {
throw new IllegalStateException(
"""
JWT secret is required. Please configure datamate.jwt.secret
Options:
1. Add to application.yml:
datamate:
jwt:
secret: your-strong-secret-key-here
2. Set environment variable:
export JWT_SECRET=your-strong-secret-key-here
3. Run with system property:
-Ddatamate.jwt.secret=your-strong-secret-key-here"""
);
}

// 额外验证
if (secret.length() < 32) {
log.warn("\n⚠️ JWT secret is only {} characters. For security, use at least 32 characters.\n", secret.length());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.datamate.gateway.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

/**
* 安全配置 - 暂时禁用所有认证
*/
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
http.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchange ->
exchange.pathMatchers("**").permitAll() // 允许所有请求无需认证
);

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.datamate.gateway.common.filter;

import com.datamate.common.infrastructure.common.Response;
import com.datamate.common.infrastructure.exception.CommonErrorCode;
import com.datamate.gateway.domain.service.UserService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
* 用户信息过滤器
*
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class UserContextFilter implements GlobalFilter {
private static final String AUTH_HEADER = "Authorization";

private static final String TOKEN_PREFIX = "Bearer ";

private final UserService userService;

@Value("${datamate.jwt.enable:false}")
private Boolean jwtEnable;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (path.equals("/api/user/login") || path.equals("/api/user/signup")) {
return chain.filter(exchange);
}
try {
if (!jwtEnable) {
return chain.filter(exchange);
}
// Get token from Authorization header
String authHeader = request.getHeaders().getFirst(AUTH_HEADER);
if (authHeader == null || !authHeader.startsWith(TOKEN_PREFIX)) {
return sendUnauthorizedResponse(exchange);
}
String token = authHeader.substring(TOKEN_PREFIX.length());
if (!userService.validateToken(token)) {
return sendUnauthorizedResponse(exchange);
}
return chain.filter(exchange);
} catch (Exception e) {
log.error("get current user info error", e);
return sendUnauthorizedResponse(exchange);
}
}

private Mono<Void> sendUnauthorizedResponse(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes;
try {
bytes = objectMapper.writeValueAsString(Response.error(CommonErrorCode.UNAUTHORIZED)).getBytes(StandardCharsets.UTF_8);
} catch (JsonProcessingException e) {
String responseBody = "{\"code\":401,\"message\":\"登录失败:用户名或密码错误\",\"data\":null}";
bytes = responseBody.getBytes(StandardCharsets.UTF_8);
}
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.datamate.gateway.domain.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

/**
* 用户
*
* @since 2026/1/12
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "users", autoResultMap = true)
public class User {
private Long id;
private String username;
private String email;
private String passwordHash;
private String fullName;
private String role;
private boolean enabled;
private LocalDateTime lastLoginAt;

@TableField(exist = false)
private String password;

@TableField(exist = false)
private String token;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.datamate.gateway.domain.repository;

import com.baomidou.mybatisplus.extension.repository.IRepository;
import com.datamate.gateway.domain.entity.User;

/**
* UserRepository
*
* @since 2026/1/12
*/
public interface UserRepository extends IRepository<User> {
}
Loading
Loading