fix: 确保项目可以启动

This commit is contained in:
2025-12-12 23:50:55 +08:00
parent c61ee69561
commit 4912e48922
21 changed files with 623 additions and 145 deletions

View File

@@ -18,11 +18,6 @@
<spring-cloud-gateway.version>4.3.2</spring-cloud-gateway.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
@@ -66,5 +61,27 @@
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<!-- 🚫 必须排除Spring MVC 核心 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
<!-- 🚫 必须排除Servlet API (Gateway用不到) -->
<exclusion>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</exclusion>
<!-- 🚫 必须排除Spring WebMVC -->
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,23 @@
package cn.meowrain.aioj.backend.gateway.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
/**
* 网关配置类
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GatewayPropertiesConfiguration.class)
public class GatewayConfiguration {
/**
* WebClient Bean用于服务间调用
*/
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}

View File

@@ -2,7 +2,12 @@ package cn.meowrain.aioj.backend.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@ConfigurationProperties(prefix = GatewayPropertiesConfiguration.PREFIX)
@Data
public class GatewayPropertiesConfiguration {
@@ -10,8 +15,9 @@ public class GatewayPropertiesConfiguration {
public static final String PREFIX = "aioj-backend-gateway";
/*
* 白名单放行
* 白名单放行路径
* 支持Ant风格路径匹配如 /api/v1/question/**
*/
private String[] whiteList;
private List<String> whiteList = new ArrayList<>();
}

View File

@@ -1,28 +1,169 @@
package cn.meowrain.aioj.backend.gateway.filter;
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.core.exception.RemoteException;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.gateway.config.GatewayPropertiesConfiguration;
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.Autowired;
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.HttpHeaders;
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.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final WebClient.Builder webClientBuilder;
@Autowired
private GatewayPropertiesConfiguration gatewayPropertiesConfiguration;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 不需要认证的路径
*/
private static final String[] DEFAULT_WHITE_LIST = {
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/refresh",
"/api/v1/user/info"
};
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return null;
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
log.info("Auth filter processing request: {}", path);
// 检查是否在白名单中
if (isWhiteListPath(path)) {
log.info("Path {} is in whitelist, skip authentication", path);
return chain.filter(exchange);
}
// 获取Authorization头
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
log.warn("No valid authorization header found for path: {}", path);
return handleUnauthorized(exchange);
}
String token = authHeader.substring(7);
// 调用auth服务验证token
return validateToken(token)
.flatMap(isValid -> {
if (isValid) {
log.info("Token validation successful for path: {}", path);
return chain.filter(exchange);
} else {
log.warn("Token validation failed for path: {}", path);
return handleUnauthorized(exchange);
}
})
.onErrorResume(throwable -> {
log.error("Token validation error for path: {}", path, throwable);
return handleUnauthorized(exchange);
});
}
/**
* 检查路径是否在白名单中
*/
private boolean isWhiteListPath(String path) {
// 先检查默认白名单
for (String whitePath : DEFAULT_WHITE_LIST) {
if (antPathMatcher.match(whitePath, path)) {
return true;
}
}
// 检查配置文件中的白名单
if (gatewayPropertiesConfiguration.getWhiteList() != null && !gatewayPropertiesConfiguration.getWhiteList().isEmpty()) {
for (String whitePath : gatewayPropertiesConfiguration.getWhiteList()) {
if (antPathMatcher.match(whitePath, path)) {
return true;
}
}
}
return false;
}
/**
* 调用auth服务验证token
*/
private Mono<Boolean> validateToken(String token) {
return webClientBuilder.build()
.post()
.uri("lb://auth-service/api/v1/auth/validate")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.map(response -> {
try {
// 解析响应,判断是否有效
Result result = objectMapper.readValue(response, Result.class);
return Objects.equals(result.getCode(), Result.SUCCESS_CODE);
} catch (JsonProcessingException e) {
log.error("Failed to parse auth response", e);
return false;
}
})
.defaultIfEmpty(false);
}
/**
* 处理未授权的请求
*/
private Mono<Void> handleUnauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result = Results.failure(new RemoteException(ErrorCode.NO_AUTH_ERROR));
String responseBody;
try {
responseBody = objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
responseBody = "{\"code\":401,\"message\":\"Unauthorized\",\"data\":null}";
}
DataBuffer buffer = response.bufferFactory().wrap(responseBody.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return 0;
// 设置较高的优先级,确保在其他过滤器之前执行
return -100;
}
}

View File

@@ -7,13 +7,28 @@ spring:
webflux:
routes:
- id: auth-service
uri: lb://auth-service
uri: lb://auth-service/api
predicates:
- Path=/api/v1/auth/**
- id: user-service
uri: lb://user-service
uri: lb://user-service/api
predicates:
- Path=/api/v1/user/**
aioj-backend-gateway:
# 白名单配置
white-list:
- /api/v1/auth/login
- /api/v1/auth/register
- /api/v1/auth/refresh
- /api/v1/user/info
- /api/v1/question/list
- /api/v1/question/detail/**
- /actuator/health
- /swagger-ui/**
- /v3/api-docs/**
- /swagger-resources/**
aioj:
log:
enabled: true