fix: 确保项目可以启动
This commit is contained in:
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<>();
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user