feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY target/everything-is-suitable-gateway-1.0.0.jar app.jar
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||||
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>io.destiny</groupId>
|
||||
<artifactId>everything-is-suitable-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>everything-is-suitable-gateway</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Everything Is Suitable Gateway</name>
|
||||
<description>Gateway module for Everything Is Suitable API</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.destiny</groupId>
|
||||
<artifactId>everything-is-suitable-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.destiny</groupId>
|
||||
<artifactId>everything-is-suitable-biz</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.destiny</groupId>
|
||||
<artifactId>everything-is-suitable-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.destiny</groupId>
|
||||
<artifactId>everything-is-suitable-sys</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.destiny</groupId>
|
||||
<artifactId>everything-is-suitable-statistics</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
<element>BUNDLE</element>
|
||||
<limits>
|
||||
<limit>
|
||||
<counter>LINE</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.50</minimum>
|
||||
</limit>
|
||||
<limit>
|
||||
<counter>BRANCH</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.50</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
<excludes>
|
||||
<exclude>**/dto/**</exclude>
|
||||
<exclude>**/domain/**</exclude>
|
||||
<exclude>**/enums/**</exclude>
|
||||
<exclude>**/config/**</exclude>
|
||||
<exclude>**/exception/**</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>check</id>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
<element>BUNDLE</element>
|
||||
<limits>
|
||||
<limit>
|
||||
<counter>LINE</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.50</minimum>
|
||||
</limit>
|
||||
<limit>
|
||||
<counter>BRANCH</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.50</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
<excludes>
|
||||
<exclude>**/dto/**</exclude>
|
||||
<exclude>**/domain/**</exclude>
|
||||
<exclude>**/enums/**</exclude>
|
||||
<exclude>**/config/**</exclude>
|
||||
<exclude>**/exception/**</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.destiny.gateway.config;
|
||||
|
||||
import io.destiny.common.exception.ReactiveGlobalExceptionHandler;
|
||||
import io.destiny.gateway.handler.ClientHandler;
|
||||
import io.destiny.gateway.handler.HealthHandler;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springdoc.core.annotations.RouterOperation;
|
||||
import org.springdoc.core.annotations.RouterOperations;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
|
||||
/**
|
||||
* 网关路由配置
|
||||
*
|
||||
* @author zhangxiang
|
||||
*/
|
||||
@Configuration
|
||||
public class GatewayConfig {
|
||||
|
||||
/**
|
||||
* 配置全局异常处理器
|
||||
*
|
||||
* @return 全局异常处理器
|
||||
*/
|
||||
@Bean
|
||||
public ReactiveGlobalExceptionHandler reactiveGlobalExceptionHandler() {
|
||||
return new ReactiveGlobalExceptionHandler();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 配置客户端路由
|
||||
* 所有 /client/** 请求将路由到 ClientHandler
|
||||
*
|
||||
* @param clientHandler 客户端处理器
|
||||
* @return 路由函数
|
||||
*/
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> clientRouter(ClientHandler clientHandler) {
|
||||
return route()
|
||||
.POST("/client/auth/register", accept(MediaType.APPLICATION_JSON),
|
||||
clientHandler::register)
|
||||
.POST("/client/auth/login", accept(MediaType.APPLICATION_JSON),
|
||||
clientHandler::login)
|
||||
.POST("/client/auth/logout/{userId}", clientHandler::logout)
|
||||
.GET("/client/auth/status/{userId}", clientHandler::checkLoginStatus)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置健康检查路由
|
||||
*
|
||||
* @param healthHandler 健康检查处理器
|
||||
* @return 路由函数
|
||||
*/
|
||||
@Bean
|
||||
@RouterOperations({
|
||||
@RouterOperation(path = "/health", method = RequestMethod.GET, beanClass = HealthHandler.class, beanMethod = "handleHealth")
|
||||
})
|
||||
@Operation(summary = "健康检查路由配置")
|
||||
@Tag(name = "健康检查", description = "健康检查相关接口")
|
||||
public RouterFunction<ServerResponse> healthRouter(HealthHandler healthHandler) {
|
||||
return route()
|
||||
.GET("/health", healthHandler::handleHealth)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.destiny.gateway.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ConfigurationProperties(prefix = "gateway")
|
||||
public class GatewayProperties {
|
||||
|
||||
private List<String> publicPaths = new ArrayList<>();
|
||||
|
||||
public List<String> getPublicPaths() {
|
||||
return publicPaths;
|
||||
}
|
||||
|
||||
public void setPublicPaths(List<String> publicPaths) {
|
||||
this.publicPaths = publicPaths;
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package io.destiny.gateway.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Value("${springdoc.swagger-ui.path:/swagger-ui.html}")
|
||||
private String swaggerUiPath;
|
||||
|
||||
@Value("${server.port:8080}")
|
||||
private int serverPort;
|
||||
|
||||
@Value("${spring.application.name:everything-is-suitable-api}")
|
||||
private String applicationName;
|
||||
|
||||
@Value("${spring.profiles.active:local}")
|
||||
private String activeProfile;
|
||||
|
||||
private static final String SECURITY_SCHEME_NAME = "JWT认证";
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("Everything Is Suitable API")
|
||||
.version("1.0.0")
|
||||
.description("紫微斗数API接口文档 - 提供紫微命盘生成、运势分析等核心功能")
|
||||
.contact(new Contact()
|
||||
.name("张翔")
|
||||
.email("zhangxiang@destiny.io")
|
||||
.url("https://github.com/destiny/everything-is-suitable-api"))
|
||||
.license(new License()
|
||||
.name("Apache 2.0")
|
||||
.url("https://www.apache.org/licenses/LICENSE-2.0.html")))
|
||||
.servers(List.of(
|
||||
new Server().url("http://localhost:" + serverPort)
|
||||
.description("本地开发环境"),
|
||||
new Server().url("http://dev-api.destiny.io").description("开发环境"),
|
||||
new Server().url("http://test-api.destiny.io").description("测试环境")))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes(SECURITY_SCHEME_NAME,
|
||||
new SecurityScheme()
|
||||
.name(SECURITY_SCHEME_NAME)
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.description("请输入JWT Token,格式: Bearer {token}")))
|
||||
.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi clientApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("client-api")
|
||||
.displayName("客户端接口")
|
||||
.pathsToMatch("/client/**")
|
||||
.packagesToScan("io.destiny.client.handler")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi sysApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("sys-api")
|
||||
.displayName("系统管理接口")
|
||||
.pathsToMatch("/sys/**")
|
||||
.packagesToScan("io.destiny.sys.handler")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi ziweiApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("ziwei-api")
|
||||
.displayName("紫微斗数接口")
|
||||
.pathsToMatch("/ziwei/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi healthApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("health-api")
|
||||
.displayName("健康检查接口")
|
||||
.pathsToMatch("/health", "/actuator/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi bizApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("biz-api")
|
||||
.displayName("业务接口")
|
||||
.pathsToMatch("/almanac/**", "/fortune/**", "/calendar/**", "/lunar-calendar/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi statisticsApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("statistics-api")
|
||||
.displayName("统计接口")
|
||||
.pathsToMatch("/statistics/**", "/export/**")
|
||||
.packagesToScan("io.destiny.statistics.handler")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package io.destiny.gateway.config;
|
||||
|
||||
import io.swagger.v3.oas.models.PathItem;
|
||||
import io.swagger.v3.oas.models.Paths;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
@Configuration
|
||||
public class SwaggerSecurityConfig {
|
||||
|
||||
private static final String SECURITY_SCHEME_NAME = "JWT认证";
|
||||
|
||||
@Bean
|
||||
@Profile({"local", "dev"})
|
||||
public OpenApiCustomizer disableSecurityForDev() {
|
||||
return openApi -> {
|
||||
Paths paths = openApi.getPaths();
|
||||
if (paths != null) {
|
||||
paths.forEach((path, pathItem) -> {
|
||||
removeSecurityFromPathItem(pathItem);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile({"test", "prod"})
|
||||
public OpenApiCustomizer enableSecurityForProd() {
|
||||
return openApi -> {
|
||||
Paths paths = openApi.getPaths();
|
||||
if (paths != null) {
|
||||
paths.forEach((path, pathItem) -> {
|
||||
addSecurityToPathItem(pathItem, path);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void removeSecurityFromPathItem(PathItem pathItem) {
|
||||
if (pathItem.getGet() != null) {
|
||||
pathItem.getGet().setSecurity(null);
|
||||
}
|
||||
if (pathItem.getPost() != null) {
|
||||
pathItem.getPost().setSecurity(null);
|
||||
}
|
||||
if (pathItem.getPut() != null) {
|
||||
pathItem.getPut().setSecurity(null);
|
||||
}
|
||||
if (pathItem.getDelete() != null) {
|
||||
pathItem.getDelete().setSecurity(null);
|
||||
}
|
||||
if (pathItem.getPatch() != null) {
|
||||
pathItem.getPatch().setSecurity(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void addSecurityToPathItem(PathItem pathItem, String path) {
|
||||
if (isPublicEndpoint(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SecurityRequirement securityRequirement = new SecurityRequirement().addList(SECURITY_SCHEME_NAME);
|
||||
|
||||
if (pathItem.getGet() != null && pathItem.getGet().getSecurity() == null) {
|
||||
pathItem.getGet().addSecurityItem(securityRequirement);
|
||||
}
|
||||
if (pathItem.getPost() != null && pathItem.getPost().getSecurity() == null) {
|
||||
pathItem.getPost().addSecurityItem(securityRequirement);
|
||||
}
|
||||
if (pathItem.getPut() != null && pathItem.getPut().getSecurity() == null) {
|
||||
pathItem.getPut().addSecurityItem(securityRequirement);
|
||||
}
|
||||
if (pathItem.getDelete() != null && pathItem.getDelete().getSecurity() == null) {
|
||||
pathItem.getDelete().addSecurityItem(securityRequirement);
|
||||
}
|
||||
if (pathItem.getPatch() != null && pathItem.getPatch().getSecurity() == null) {
|
||||
pathItem.getPatch().addSecurityItem(securityRequirement);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPublicEndpoint(String path) {
|
||||
return path.contains("/auth/register") ||
|
||||
path.contains("/auth/login") ||
|
||||
path.contains("/auth/refresh") ||
|
||||
path.contains("/auth/logout") ||
|
||||
path.contains("/health") ||
|
||||
path.contains("/actuator");
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Component("gatewayJwtAuthenticationFilter")
|
||||
@Order(1)
|
||||
public class JwtAuthenticationFilter implements WebFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
||||
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
private static final String USER_ID_HEADER = "X-User-Id";
|
||||
private static final String USERNAME_HEADER = "X-Username";
|
||||
|
||||
private final SecretKey signingKey;
|
||||
private final List<String> publicPaths;
|
||||
|
||||
public JwtAuthenticationFilter(
|
||||
@Value("${jwt.secret:this-is-a-secure-jwt-secret-key-that-must-be-at-least-64-characters-long-for-hs512-algorithm}") String secret,
|
||||
@Value("${gateway.public-paths:/api/client/auth/register,/api/client/auth/login,/api/sys/auth/register,/api/sys/auth/login,/api/sys/auth/refresh,/api/sys/auth/logout,/sys/auth/register,/sys/auth/login,/sys/auth/refresh,/sys/auth/logout,/api/health,/api/swagger-ui,/api/swagger-ui/**,/api/v3/api-docs,/api/v3/api-docs/**,/api/webjars/swagger-ui/**,/api/webjars/**,/api/actuator/**,/api/almanac,/api/almanac/**,/actuator/**,/swagger-ui,/swagger-ui/**,/v3/api-docs,/v3/api-docs/**,/webjars/swagger-ui/**,/webjars/**,/almanac,/almanac/**}") String publicPaths) {
|
||||
this.signingKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.publicPaths = List.of(publicPaths.split(","));
|
||||
logger.info("Loaded {} public paths: {}", this.publicPaths.size(), this.publicPaths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
String path = exchange.getRequest().getPath().value();
|
||||
|
||||
System.out.println("JwtAuthenticationFilter processing path: " + path);
|
||||
|
||||
// Special handling for Swagger paths
|
||||
if (path.startsWith("/swagger-ui") || path.startsWith("/v3/api-docs") || path.startsWith("/webjars")) {
|
||||
System.out.println("Swagger path detected, skipping authentication: " + path);
|
||||
logger.info("Swagger path detected, skipping authentication: {}", path);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
if (isPublicPath(path)) {
|
||||
logger.debug("Public path, skipping authentication: {}", path);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
|
||||
logger.warn("Missing or invalid authorization header for path: {}", path);
|
||||
return unauthorized(exchange, "Missing or invalid authorization header");
|
||||
}
|
||||
|
||||
String token = authHeader.substring(BEARER_PREFIX.length());
|
||||
|
||||
try {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(signingKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
Long userId = claims.get("userId", Long.class);
|
||||
String username = claims.getSubject();
|
||||
|
||||
if (userId == null || username == null) {
|
||||
logger.warn("Invalid token claims for path: {}", path);
|
||||
return unauthorized(exchange, "Invalid token claims");
|
||||
}
|
||||
|
||||
Date expiration = claims.getExpiration();
|
||||
if (expiration != null && expiration.before(new Date())) {
|
||||
logger.warn("Token expired for user: {} ({})", username, userId);
|
||||
return unauthorized(exchange, "Token has expired");
|
||||
}
|
||||
|
||||
ServerWebExchange modifiedExchange = exchange.mutate()
|
||||
.request(r -> r.header(USER_ID_HEADER, userId.toString())
|
||||
.header(USERNAME_HEADER, username))
|
||||
.build();
|
||||
|
||||
logger.debug("Authentication successful for user: {} ({})", username, userId);
|
||||
return chain.filter(modifiedExchange);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Token validation failed for path: {}", path, e);
|
||||
return unauthorized(exchange, "Invalid or expired token");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPublicPath(String path) {
|
||||
logger.info("JwtAuthenticationFilter.isPublicPath called with path: '{}'", path);
|
||||
logger.info("Public paths list: {}", publicPaths);
|
||||
|
||||
boolean isPublic = publicPaths.stream().anyMatch(publicPath -> {
|
||||
boolean match = false;
|
||||
if (publicPath.endsWith("/**")) {
|
||||
String prefix = publicPath.substring(0, publicPath.length() - 3);
|
||||
match = path.startsWith(prefix);
|
||||
logger.info(" Checking path '{}' against public path '{}/**': prefix='{}', startsWith={}", path, prefix, prefix, match);
|
||||
} else if (publicPath.endsWith("/*")) {
|
||||
String prefix = publicPath.substring(0, publicPath.length() - 2);
|
||||
match = path.startsWith(prefix) && path.substring(prefix.length()).indexOf('/') == -1;
|
||||
logger.info(" Checking path '{}' against public path '{}/*': prefix='{}', match={}", path, prefix, prefix, match);
|
||||
} else {
|
||||
match = path.equals(publicPath) || path.startsWith(publicPath + "/");
|
||||
logger.info(" Checking path '{}' against public path '{}': equals={}, startsWith={}", path, publicPath, path.equals(publicPath), path.startsWith(publicPath + "/"));
|
||||
}
|
||||
logger.debug("Checking path '{}' against public path '{}': {}", path, publicPath, match);
|
||||
return match;
|
||||
});
|
||||
logger.info("Checking path '{}' against public paths: {}", path, isPublic);
|
||||
return isPublic;
|
||||
}
|
||||
|
||||
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
String body = String.format("{\"code\":\"UNAUTHORIZED\",\"message\":\"%s\"}", message);
|
||||
|
||||
return exchange.getResponse().writeWith(
|
||||
reactor.core.publisher.Flux.just(
|
||||
exchange.getResponse().bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8))));
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 网关日志过滤器
|
||||
* 记录请求和响应信息
|
||||
*
|
||||
* @author zhangxiang
|
||||
*/
|
||||
@Component
|
||||
public class LoggingFilter implements WebFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
Instant startTime = Instant.now();
|
||||
String method = exchange.getRequest().getMethod().name();
|
||||
String path = exchange.getRequest().getPath().value();
|
||||
String clientIp = exchange.getRequest().getRemoteAddress() != null
|
||||
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
|
||||
: "unknown";
|
||||
|
||||
logger.info("Request: {} {} from {}", method, path, clientIp);
|
||||
|
||||
return chain.filter(exchange)
|
||||
.doOnSuccess(aVoid -> {
|
||||
Duration duration = Duration.between(startTime, Instant.now());
|
||||
int statusCode = exchange.getResponse().getStatusCode() != null
|
||||
? exchange.getResponse().getStatusCode().value()
|
||||
: 0;
|
||||
logger.info("Response: {} {} - Status: {} - Duration: {}ms",
|
||||
method, path, statusCode, duration.toMillis());
|
||||
})
|
||||
.doOnError(error -> {
|
||||
Duration duration = Duration.between(startTime, Instant.now());
|
||||
logger.error("Error: {} {} - Error: {} - Duration: {}ms",
|
||||
method, path, error.getMessage(), duration.toMillis());
|
||||
});
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Component
|
||||
@Order(3)
|
||||
public class RateLimitFilter implements WebFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RateLimitFilter.class);
|
||||
|
||||
private final ConcurrentHashMap<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();
|
||||
private final int maxRequests;
|
||||
private final Duration timeWindow;
|
||||
private final boolean enabled;
|
||||
private final List<String> whitelist;
|
||||
|
||||
public RateLimitFilter(@Value("${gateway.rate-limit.max-requests:100}") int maxRequests,
|
||||
@Value("${gateway.rate-limit.time-window-seconds:60}") int timeWindowSeconds,
|
||||
@Value("${gateway.rate-limit.enabled:true}") boolean enabled,
|
||||
@Value("${gateway.rate-limit.whitelist:}") String whitelistConfig) {
|
||||
this.maxRequests = maxRequests;
|
||||
this.timeWindow = Duration.ofSeconds(timeWindowSeconds);
|
||||
this.enabled = enabled;
|
||||
this.whitelist = whitelistConfig != null && !whitelistConfig.isEmpty()
|
||||
? List.of(whitelistConfig.split(","))
|
||||
: List.of();
|
||||
logger.info("Rate limit initialized: {} requests per {} seconds, enabled: {}, whitelist: {}",
|
||||
maxRequests, timeWindowSeconds, enabled, whitelist);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
if (!enabled) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
String clientIp = getClientIp(exchange);
|
||||
String path = exchange.getRequest().getPath().value();
|
||||
|
||||
if (isWhitelisted(clientIp)) {
|
||||
logger.debug("IP {} is whitelisted, skipping rate limit", clientIp);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
RateLimiter rateLimiter = rateLimiters.computeIfAbsent(clientIp, k -> new RateLimiter(maxRequests, timeWindow));
|
||||
|
||||
if (!rateLimiter.tryAcquire()) {
|
||||
logger.warn("Rate limit exceeded for IP: {} on path: {}", clientIp, path);
|
||||
return tooManyRequests(exchange, "Rate limit exceeded. Please try again later.");
|
||||
}
|
||||
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
private boolean isWhitelisted(String clientIp) {
|
||||
return whitelist.stream().anyMatch(whitelistedIp -> {
|
||||
if (whitelistedIp.equals("127.0.0.1") || whitelistedIp.equals("localhost")) {
|
||||
return clientIp.equals("127.0.0.1") || clientIp.equals("::1") || clientIp.equals("0:0:0:0:1");
|
||||
}
|
||||
return clientIp.equals(whitelistedIp);
|
||||
});
|
||||
}
|
||||
|
||||
private String getClientIp(ServerWebExchange exchange) {
|
||||
String xForwardedFor = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
|
||||
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
|
||||
return xForwardedFor.split(",")[0].trim();
|
||||
}
|
||||
|
||||
String xRealIp = exchange.getRequest().getHeaders().getFirst("X-Real-IP");
|
||||
if (xRealIp != null && !xRealIp.isEmpty()) {
|
||||
return xRealIp;
|
||||
}
|
||||
|
||||
return exchange.getRequest().getRemoteAddress() != null
|
||||
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
private Mono<Void> tooManyRequests(ServerWebExchange exchange, String message) {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
|
||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
String body = String.format("{\"code\":\"RATE_LIMIT_EXCEEDED\",\"message\":\"%s\"}", message);
|
||||
|
||||
return exchange.getResponse().writeWith(
|
||||
reactor.core.publisher.Flux.just(
|
||||
exchange.getResponse().bufferFactory().wrap(body.getBytes(java.nio.charset.StandardCharsets.UTF_8))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static class RateLimiter {
|
||||
private final AtomicLong counter = new AtomicLong(0);
|
||||
private volatile Instant windowStart = Instant.now();
|
||||
private final int maxRequests;
|
||||
private final Duration timeWindow;
|
||||
|
||||
public RateLimiter(int maxRequests, Duration timeWindow) {
|
||||
this.maxRequests = maxRequests;
|
||||
this.timeWindow = timeWindow;
|
||||
}
|
||||
|
||||
public synchronized boolean tryAcquire() {
|
||||
Instant now = Instant.now();
|
||||
|
||||
if (Duration.between(windowStart, now).compareTo(timeWindow) > 0) {
|
||||
windowStart = now;
|
||||
counter.set(0);
|
||||
}
|
||||
|
||||
if (counter.incrementAndGet() <= maxRequests) {
|
||||
return true;
|
||||
}
|
||||
|
||||
counter.decrementAndGet();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Component
|
||||
@Order(3)
|
||||
public class SecurityHeadersFilter implements WebFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SecurityHeadersFilter.class);
|
||||
|
||||
private static final String X_FRAME_OPTIONS = "X-Frame-Options";
|
||||
private static final String X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";
|
||||
private static final String X_XSS_PROTECTION = "X-XSS-Protection";
|
||||
private static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
|
||||
private static final String STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security";
|
||||
private static final String REFERRER_POLICY = "Referrer-Policy";
|
||||
private static final String PERMISSIONS_POLICY = "Permissions-Policy";
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
|
||||
headers.set(X_FRAME_OPTIONS, "DENY");
|
||||
headers.set(X_CONTENT_TYPE_OPTIONS, "nosniff");
|
||||
headers.set(X_XSS_PROTECTION, "1; mode=block");
|
||||
headers.set(CONTENT_SECURITY_POLICY, "default-src 'self'");
|
||||
headers.set(STRICT_TRANSPORT_SECURITY, "max-age=31536000; includeSubDomains");
|
||||
headers.set(REFERRER_POLICY, "no-referrer-when-downgrade");
|
||||
headers.set(PERMISSIONS_POLICY, "geolocation 'none', microphone 'none'");
|
||||
|
||||
headers.set("X-Request-ID", generateRequestId());
|
||||
headers.set("X-Response-Time", String.valueOf(System.currentTimeMillis()));
|
||||
|
||||
logger.debug("Security headers added for request: {}", exchange.getRequest().getPath().value());
|
||||
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
private String generateRequestId() {
|
||||
return java.util.UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.destiny.common.utils.XssUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Order(2)
|
||||
public class XssFilter implements WebFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(XssFilter.class);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public XssFilter(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String path = request.getPath().value();
|
||||
|
||||
String contentType = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
|
||||
if (contentType == null) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
if (!contentType.contains(MediaType.APPLICATION_JSON_VALUE) &&
|
||||
!contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) &&
|
||||
!contentType.contains(MediaType.TEXT_PLAIN_VALUE)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
String method = request.getMethod().name();
|
||||
if (!"POST".equals(method) && !"PUT".equals(method) && !"PATCH".equals(method)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
if (shouldSkipXssCleaning(path)) {
|
||||
logger.debug("Skipping XSS cleaning for path: {}", path);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
return DataBufferUtils.join(request.getBody())
|
||||
.flatMap(dataBuffer -> {
|
||||
byte[] bytes = new byte[dataBuffer.readableByteCount()];
|
||||
dataBuffer.read(bytes);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
|
||||
String body = new String(bytes, StandardCharsets.UTF_8);
|
||||
String cleanedBody = cleanRequestBody(body, contentType);
|
||||
|
||||
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(request) {
|
||||
@Override
|
||||
public Flux<DataBuffer> getBody() {
|
||||
return Flux.defer(() -> {
|
||||
byte[] cleanedBytes = cleanedBody.getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(cleanedBytes);
|
||||
return Flux.just(buffer);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.putAll(super.getHeaders());
|
||||
headers.setContentLength(cleanedBody.getBytes(StandardCharsets.UTF_8).length);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
|
||||
logger.debug("XSS filter applied for request: {}", request.getPath().value());
|
||||
return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
||||
})
|
||||
.switchIfEmpty(chain.filter(exchange));
|
||||
}
|
||||
|
||||
private boolean shouldSkipXssCleaning(String path) {
|
||||
return path.startsWith("/sys/auth/login") ||
|
||||
path.startsWith("/sys/auth/register") ||
|
||||
path.startsWith("/sys/auth/refresh") ||
|
||||
path.startsWith("/sys/auth/logout") ||
|
||||
path.startsWith("/api/sys/auth/login") ||
|
||||
path.startsWith("/api/sys/auth/register") ||
|
||||
path.startsWith("/api/sys/auth/refresh") ||
|
||||
path.startsWith("/api/sys/auth/logout") ||
|
||||
path.startsWith("/api/client/auth/login") ||
|
||||
path.startsWith("/api/client/auth/register");
|
||||
}
|
||||
|
||||
private String cleanRequestBody(String body, String contentType) {
|
||||
if (body == null || body.isEmpty()) {
|
||||
return body;
|
||||
}
|
||||
|
||||
try {
|
||||
if (contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
|
||||
return cleanJsonBody(body);
|
||||
} else if (contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
|
||||
return cleanFormDataBody(body);
|
||||
} else {
|
||||
return XssUtils.clean(body);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to clean request body, returning original: {}", e.getMessage());
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String cleanJsonBody(String json) {
|
||||
try {
|
||||
Map<String, Object> map = objectMapper.readValue(json, Map.class);
|
||||
cleanMapValues(map);
|
||||
return objectMapper.writeValueAsString(map);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to parse JSON, applying simple XSS cleaning: {}", e.getMessage());
|
||||
return XssUtils.clean(json);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void cleanMapValues(Map<String, Object> map) {
|
||||
map.forEach((key, value) -> {
|
||||
if (value instanceof String) {
|
||||
map.put(key, XssUtils.clean((String) value));
|
||||
} else if (value instanceof Map) {
|
||||
cleanMapValues((Map<String, Object>) value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String cleanFormDataBody(String formData) {
|
||||
String[] pairs = formData.split("&");
|
||||
StringBuilder cleaned = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < pairs.length; i++) {
|
||||
String[] keyValue = pairs[i].split("=", 2);
|
||||
if (keyValue.length == 2) {
|
||||
String key = keyValue[0];
|
||||
String value = java.net.URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8);
|
||||
String cleanedValue = XssUtils.clean(value);
|
||||
String encodedValue = java.net.URLEncoder.encode(cleanedValue, StandardCharsets.UTF_8);
|
||||
|
||||
if (i > 0) {
|
||||
cleaned.append("&");
|
||||
}
|
||||
cleaned.append(key).append("=").append(encodedValue);
|
||||
} else if (keyValue.length == 1) {
|
||||
if (i > 0) {
|
||||
cleaned.append("&");
|
||||
}
|
||||
cleaned.append(keyValue[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned.toString();
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package io.destiny.gateway.handler;
|
||||
|
||||
import io.destiny.common.response.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
|
||||
@Component
|
||||
@Tag(name = "客户端接口", description = "客户端用户相关操作接口")
|
||||
public class ClientHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClientHandler.class);
|
||||
|
||||
@Operation(summary = "用户注册", description = "客户端用户注册接口,转发到客户端模块处理")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "注册成功", content = @Content(schema = @Schema(implementation = Result.class)))
|
||||
})
|
||||
public Mono<ServerResponse> register(
|
||||
@Parameter(description = "服务器请求对象", hidden = true) ServerRequest request) {
|
||||
logger.info("Gateway forwarding register request to client module");
|
||||
return ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Result.success("Register request forwarded to client module", null));
|
||||
}
|
||||
|
||||
@Operation(summary = "用户登录", description = "客户端用户登录接口,转发到客户端模块处理")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "登录成功", content = @Content(schema = @Schema(implementation = Result.class)))
|
||||
})
|
||||
public Mono<ServerResponse> login(
|
||||
@Parameter(description = "服务器请求对象", hidden = true) ServerRequest request) {
|
||||
logger.info("Gateway forwarding login request to client module");
|
||||
return ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Result.success("Login request forwarded to client module", null));
|
||||
}
|
||||
|
||||
@Operation(summary = "用户登出", description = "客户端用户登出接口,转发到客户端模块处理")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "登出成功", content = @Content(schema = @Schema(implementation = Result.class)))
|
||||
})
|
||||
public Mono<ServerResponse> logout(
|
||||
@Parameter(description = "服务器请求对象", hidden = true) ServerRequest request) {
|
||||
logger.info("Gateway forwarding logout request to client module");
|
||||
return ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Result.success("Logout request forwarded to client module", null));
|
||||
}
|
||||
|
||||
@Operation(summary = "检查登录状态", description = "检查用户登录状态接口,转发到客户端模块处理")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "检查成功", content = @Content(schema = @Schema(implementation = Result.class)))
|
||||
})
|
||||
public Mono<ServerResponse> checkLoginStatus(
|
||||
@Parameter(description = "服务器请求对象", hidden = true) ServerRequest request) {
|
||||
logger.info("Gateway forwarding check status request to client module");
|
||||
return ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Result.success("Check status request forwarded to client module", null));
|
||||
}
|
||||
|
||||
@Operation(summary = "客户端GET请求", description = "处理客户端模块的GET请求")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "请求成功", content = @Content(schema = @Schema(implementation = Result.class)))
|
||||
})
|
||||
public Mono<ServerResponse> handleGet(
|
||||
@Parameter(description = "服务器请求对象", hidden = true) ServerRequest request) {
|
||||
String path = request.path();
|
||||
logger.info("Client GET request: {}", path);
|
||||
|
||||
return ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Result.success("Client GET request received", path));
|
||||
}
|
||||
|
||||
@Operation(summary = "客户端POST请求", description = "处理客户端模块的POST请求")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "请求成功", content = @Content(schema = @Schema(implementation = Result.class)))
|
||||
})
|
||||
public Mono<ServerResponse> handlePost(
|
||||
@Parameter(description = "服务器请求对象", hidden = true) ServerRequest request) {
|
||||
String path = request.path();
|
||||
logger.info("Client POST request: {}", path);
|
||||
|
||||
return request.bodyToMono(String.class)
|
||||
.defaultIfEmpty("")
|
||||
.flatMap(body -> {
|
||||
logger.info("Client POST body: {}", body);
|
||||
return ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Result.success("Client POST request received", path));
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "客户端PUT请求", description = "处理客户端模块的PUT请求")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "请求成功", content = @Content(schema = @Schema(implementation = Result.class)))
|
||||
})
|
||||
public Mono<ServerResponse> handlePut(
|
||||
@Parameter(description = "服务器请求对象", hidden = true) ServerRequest request) {
|
||||
String path = request.path();
|
||||
logger.info("Client PUT request: {}", path);
|
||||
|
||||
return request.bodyToMono(String.class)
|
||||
.defaultIfEmpty("")
|
||||
.flatMap(body -> {
|
||||
logger.info("Client PUT body: {}", body);
|
||||
return ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Result.success("Client PUT request received", path));
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "客户端DELETE请求", description = "处理客户端模块的DELETE请求")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "请求成功", content = @Content(schema = @Schema(implementation = Result.class)))
|
||||
})
|
||||
public Mono<ServerResponse> handleDelete(
|
||||
@Parameter(description = "服务器请求对象", hidden = true) ServerRequest request) {
|
||||
String path = request.path();
|
||||
logger.info("Client DELETE request: {}", path);
|
||||
|
||||
return ServerResponse.ok()
|
||||
.contentType(APPLICATION_JSON)
|
||||
.bodyValue(Result.success("Client DELETE request received", path));
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.destiny.gateway.handler;
|
||||
|
||||
import io.destiny.common.response.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
|
||||
@Component
|
||||
@Tag(name = "健康检查", description = "系统健康状态检查接口")
|
||||
public class HealthHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(HealthHandler.class);
|
||||
|
||||
@Operation(
|
||||
summary = "健康检查",
|
||||
description = "检查系统健康状态,返回服务状态、时间戳和服务名称"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "健康检查成功",
|
||||
content = @Content(schema = @Schema(implementation = Result.class))
|
||||
)
|
||||
})
|
||||
public Mono<ServerResponse> handleHealth(
|
||||
@Parameter(description = "服务器请求对象", hidden = true)
|
||||
ServerRequest request) {
|
||||
logger.info("Health check requested");
|
||||
|
||||
Map<String, Object> healthInfo = new HashMap<>();
|
||||
healthInfo.put("status", "UP");
|
||||
healthInfo.put("timestamp", LocalDateTime.now().toString());
|
||||
healthInfo.put("service", "everything-is-suitable-api");
|
||||
|
||||
return ServerResponse.ok()
|
||||
.contentType(APPLICATION_JSON)
|
||||
.bodyValue(Result.success(healthInfo));
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.destiny.gateway.handler;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Component
|
||||
public class SwaggerHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SwaggerHandler.class);
|
||||
|
||||
public Mono<ServerResponse> handleSwaggerRedirect(ServerRequest request) {
|
||||
logger.info("Redirecting to Swagger UI");
|
||||
return ServerResponse.permanentRedirect(
|
||||
java.net.URI.create("/swagger-ui.html")).build();
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> handleApiDocsRedirect(ServerRequest request) {
|
||||
logger.info("Redirecting to API Docs");
|
||||
return ServerResponse.permanentRedirect(
|
||||
java.net.URI.create("/v3/api-docs")).build();
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
spring:
|
||||
application:
|
||||
name: gateway
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: client-app-route
|
||||
uri: http://localhost:8081
|
||||
predicates:
|
||||
- Path=/api/client/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
|
||||
- id: admin-app-route
|
||||
uri: http://localhost:8082
|
||||
predicates:
|
||||
- Path=/api/admin/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
|
||||
- id: client-auth-route
|
||||
uri: http://localhost:8081
|
||||
predicates:
|
||||
- Path=/api/auth/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
- id: client-fortune-route
|
||||
uri: http://localhost:8081
|
||||
predicates:
|
||||
- Path=/api/fortune/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
gateway:
|
||||
public-paths: ${GATEWAY_PUBLIC_PATHS:/api/client/auth/register,/api/client/auth/login,/api/sys/auth/register,/api/sys/auth/login,/api/sys/auth/refresh,/api/sys/auth/logout,/sys/auth/register,/sys/auth/login,/sys/auth/refresh,/sys/auth/logout,/api/health,/api/swagger-ui,/api/swagger-ui/**,/api/v3/api-docs,/api/v3/api-docs/**,/api/webjars/swagger-ui/**,/api/webjars/**,/api/actuator/**,/api/almanac,/api/almanac/**,/actuator/**,/swagger-ui,/swagger-ui/**,/v3/api-docs,/v3/api-docs/**,/webjars/swagger-ui/**,/webjars/**,/almanac,/almanac/**}
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: ${SWAGGER_ENABLED:true}
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: ${SWAGGER_ENABLED:true}
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
show-actuator: false
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package io.destiny.gateway;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
class EndToEndApiIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
private String jwtToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
jwtToken = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCompleteUserFlow() {
|
||||
String username = "testuser_" + System.currentTimeMillis();
|
||||
String password = "Test@123456";
|
||||
|
||||
registerUser(username, password);
|
||||
loginUser(username, password);
|
||||
accessProtectedEndpoint();
|
||||
}
|
||||
|
||||
private void registerUser(String username, String password) {
|
||||
String requestBody = String.format(
|
||||
"{\"username\":\"%s\",\"password\":\"%s\",\"email\":\"%s@example.com\"}",
|
||||
username, password, username
|
||||
);
|
||||
|
||||
webTestClient.post()
|
||||
.uri("/api/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromValue(requestBody))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$.message").exists();
|
||||
}
|
||||
|
||||
private void loginUser(String username, String password) {
|
||||
String requestBody = String.format(
|
||||
"{\"username\":\"%s\",\"password\":\"%s\"}",
|
||||
username, password
|
||||
);
|
||||
|
||||
String response = webTestClient.post()
|
||||
.uri("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromValue(requestBody))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class)
|
||||
.returnResult()
|
||||
.getResponseBody();
|
||||
|
||||
if (response != null && response.contains("token")) {
|
||||
jwtToken = extractTokenFromResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
private void accessProtectedEndpoint() {
|
||||
if (jwtToken != null) {
|
||||
webTestClient.get()
|
||||
.uri("/api/fortune/daily")
|
||||
.header("Authorization", "Bearer " + jwtToken)
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
}
|
||||
|
||||
private String extractTokenFromResponse(String response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnauthorizedAccessWithoutToken() {
|
||||
webTestClient.get()
|
||||
.uri("/api/fortune/daily")
|
||||
.exchange()
|
||||
.expectStatus().isUnauthorized();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidTokenReturnsUnauthorized() {
|
||||
webTestClient.get()
|
||||
.uri("/api/fortune/daily")
|
||||
.header("Authorization", "Bearer invalid-token")
|
||||
.exchange()
|
||||
.expectStatus().isUnauthorized();
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.destiny.gateway;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
class GatewayRoutingIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthCheck() {
|
||||
webTestClient.get()
|
||||
.uri("/actuator/health")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void gatewayRoutesToClientApp() {
|
||||
webTestClient.get()
|
||||
.uri("/api/auth/login")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void gatewayRoutesToAdminApp() {
|
||||
webTestClient.get()
|
||||
.uri("/api/admin/users")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void gatewayRoutesToFortuneEndpoint() {
|
||||
webTestClient.get()
|
||||
.uri("/api/fortune/daily")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.destiny.gateway.config;
|
||||
|
||||
import io.destiny.common.exception.ReactiveGlobalExceptionHandler;
|
||||
import io.destiny.gateway.handler.ClientHandler;
|
||||
import io.destiny.gateway.handler.HealthHandler;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("网关配置测试")
|
||||
class GatewayConfigTest {
|
||||
|
||||
@Mock
|
||||
private ClientHandler clientHandler;
|
||||
|
||||
@Mock
|
||||
private HealthHandler healthHandler;
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建全局异常处理器")
|
||||
void testReactiveGlobalExceptionHandlerCreation() {
|
||||
GatewayConfig config = new GatewayConfig();
|
||||
ReactiveGlobalExceptionHandler handler = config.reactiveGlobalExceptionHandler();
|
||||
assertNotNull(handler, "全局异常处理器不应为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建客户端路由")
|
||||
void testClientRouterCreation() {
|
||||
GatewayConfig config = new GatewayConfig();
|
||||
RouterFunction<ServerResponse> router = config.clientRouter(clientHandler);
|
||||
assertNotNull(router, "客户端路由不应为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建健康检查路由")
|
||||
void testHealthRouterCreation() {
|
||||
GatewayConfig config = new GatewayConfig();
|
||||
RouterFunction<ServerResponse> router = config.healthRouter(healthHandler);
|
||||
assertNotNull(router, "健康检查路由不应为null");
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.destiny.gateway.config;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("网关属性配置测试")
|
||||
class GatewayPropertiesTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建属性配置")
|
||||
void testGatewayPropertiesCreation() {
|
||||
GatewayProperties properties = new GatewayProperties();
|
||||
assertNotNull(properties, "属性配置不应为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取公开路径")
|
||||
void testPublicPathsGetterSetter() {
|
||||
GatewayProperties properties = new GatewayProperties();
|
||||
List<String> paths = List.of("/api/auth/login", "/api/health");
|
||||
|
||||
properties.setPublicPaths(paths);
|
||||
|
||||
assertNotNull(properties.getPublicPaths(), "公开路径不应为null");
|
||||
assertEquals(2, properties.getPublicPaths().size(), "公开路径数量应该为2");
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.destiny.gateway.config;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("Swagger配置测试")
|
||||
class SwaggerConfigTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建配置")
|
||||
void testSwaggerConfigCreation() {
|
||||
SwaggerConfig config = new SwaggerConfig();
|
||||
assertNotNull(config, "配置不应为null");
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.destiny.gateway.config;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("Swagger安全配置测试")
|
||||
class SwaggerSecurityConfigTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建配置")
|
||||
void testSwaggerSecurityConfigCreation() {
|
||||
SwaggerSecurityConfig config = new SwaggerSecurityConfig();
|
||||
assertNotNull(config, "配置不应为null");
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@DisplayName("JWT认证过滤器安全测试")
|
||||
class JwtAuthenticationFilterSecurityTest {
|
||||
|
||||
private static final String SECRET = "mySecretKeyForJWTTokenGenerationShouldBeLongEnoughForSecurity256Bits";
|
||||
private static final String DIFFERENT_SECRET = "differentSecretKeyForJWTTokenGenerationShouldBeLongEnoughForSecurity256Bits";
|
||||
private static final SecretKey SIGNING_KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝没有Authorization头的请求")
|
||||
void testShouldRejectRequestWithoutAuthHeader() {
|
||||
JwtAuthenticationFilter filter = createFilter(SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/user/list").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝格式错误的Authorization头")
|
||||
void testShouldRejectMalformedAuthHeader() {
|
||||
JwtAuthenticationFilter filter = createFilter(SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/user/list")
|
||||
.header(HttpHeaders.AUTHORIZATION, "InvalidFormat token")
|
||||
.build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝无效的JWT令牌")
|
||||
void testShouldRejectInvalidToken() {
|
||||
JwtAuthenticationFilter filter = createFilter(SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/user/list")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer invalid_token")
|
||||
.build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝过期的JWT令牌")
|
||||
void testShouldRejectExpiredToken() {
|
||||
String expiredToken = generateExpiredToken();
|
||||
JwtAuthenticationFilter filter = createFilter(SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/user/list")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + expiredToken)
|
||||
.build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝缺少用户ID的令牌")
|
||||
void testShouldRejectTokenWithoutUserId() {
|
||||
String token = generateTokenWithoutUserId();
|
||||
JwtAuthenticationFilter filter = createFilter(SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/user/list")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝缺少用户名的令牌")
|
||||
void testShouldRejectTokenWithoutUsername() {
|
||||
String token = generateTokenWithoutUsername();
|
||||
JwtAuthenticationFilter filter = createFilter(SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/user/list")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该接受有效的JWT令牌")
|
||||
void testShouldAcceptValidToken() {
|
||||
String validToken = generateValidToken();
|
||||
JwtAuthenticationFilter filter = createFilter(SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/user/list")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
|
||||
.build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该允许公开路径访问")
|
||||
void testShouldAllowPublicPath() {
|
||||
JwtAuthenticationFilter filter = createFilter(SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/auth/login").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝使用错误密钥签名的令牌")
|
||||
void testShouldRejectTokenWithWrongKey() {
|
||||
String token = generateValidToken();
|
||||
JwtAuthenticationFilter filter = createFilter(DIFFERENT_SECRET);
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/sys/user/list")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, never()).filter(any());
|
||||
}
|
||||
|
||||
private String generateValidToken() {
|
||||
return Jwts.builder()
|
||||
.setSubject("admin")
|
||||
.claim("userId", 1)
|
||||
.signWith(SIGNING_KEY)
|
||||
.compact();
|
||||
}
|
||||
|
||||
private String generateExpiredToken() {
|
||||
return Jwts.builder()
|
||||
.setSubject("admin")
|
||||
.claim("userId", 1)
|
||||
.setExpiration(new java.util.Date(System.currentTimeMillis() - 1000))
|
||||
.signWith(SIGNING_KEY)
|
||||
.compact();
|
||||
}
|
||||
|
||||
private String generateTokenWithoutUserId() {
|
||||
return Jwts.builder()
|
||||
.setSubject("admin")
|
||||
.signWith(SIGNING_KEY)
|
||||
.compact();
|
||||
}
|
||||
|
||||
private String generateTokenWithoutUsername() {
|
||||
return Jwts.builder()
|
||||
.claim("userId", 1)
|
||||
.signWith(SIGNING_KEY)
|
||||
.compact();
|
||||
}
|
||||
|
||||
private JwtAuthenticationFilter createFilter(String secret) {
|
||||
return new JwtAuthenticationFilter(
|
||||
secret,
|
||||
"/api/client/auth/register,/api/client/auth/login,/api/sys/auth/register,/api/sys/auth/login,/api/sys/auth/refresh,/api/sys/auth/logout,/api/swagger-ui,/api/swagger-ui/**,/api/v3/api-docs,/api/v3/api-docs/**,/api/webjars/swagger-ui/**,/api/webjars/**,/api/actuator/health,/api/actuator/info"
|
||||
);
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@DisplayName("日志过滤器测试")
|
||||
class LoggingFilterTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该记录请求日志")
|
||||
void testShouldLogRequest() {
|
||||
LoggingFilter filter = new LoggingFilter();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/test").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确传递请求")
|
||||
void testShouldPassRequest() {
|
||||
LoggingFilter filter = new LoggingFilter();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/test").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@DisplayName("限流过滤器测试")
|
||||
class RateLimitFilterTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该允许正常请求")
|
||||
void testShouldAllowNormalRequest() {
|
||||
RateLimitFilter filter = new RateLimitFilter(100, 60, true, "");
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/test").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确传递请求")
|
||||
void testShouldPassRequest() {
|
||||
RateLimitFilter filter = new RateLimitFilter(100, 60, true, "");
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/test").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该跳过白名单IP")
|
||||
void testShouldSkipWhitelistedIp() {
|
||||
RateLimitFilter filter = new RateLimitFilter(100, 60, true, "127.0.0.1");
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/test").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@DisplayName("安全头过滤器测试")
|
||||
class SecurityHeadersFilterTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该添加安全头")
|
||||
void testShouldAddSecurityHeaders() {
|
||||
SecurityHeadersFilter filter = new SecurityHeadersFilter();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/test").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
HttpHeaders headers = exchange.getResponse().getHeaders();
|
||||
assertNotNull(headers.get("X-Content-Type-Options"), "应该包含X-Content-Type-Options头");
|
||||
assertNotNull(headers.get("X-Frame-Options"), "应该包含X-Frame-Options头");
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确传递请求")
|
||||
void testShouldPassRequest() {
|
||||
SecurityHeadersFilter filter = new SecurityHeadersFilter();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||
MockServerHttpRequest.get("/api/test").build()
|
||||
);
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
result.block();
|
||||
|
||||
verify(chain, times(1)).filter(any());
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package io.destiny.gateway.filter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("XSS过滤器测试")
|
||||
class XssFilterTest {
|
||||
|
||||
private XssFilter xssFilter;
|
||||
|
||||
@Mock
|
||||
private WebFilterChain chain;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
xssFilter = new XssFilter(new ObjectMapper());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该跳过非POST/PUT/PATCH请求")
|
||||
void shouldSkipNonMutatingMethods() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(xssFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该跳过非JSON请求")
|
||||
void shouldSkipNonJsonRequests() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/test")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE)
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(xssFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该清理JSON请求中的XSS攻击")
|
||||
void shouldCleanXssInJsonRequests() {
|
||||
String maliciousJson = "{\"username\":\"<script>alert('xss')</script>\",\"email\":\"test@example.com\"}";
|
||||
DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.wrap(maliciousJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/test")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body(Flux.just(buffer));
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(xssFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该清理表单数据中的XSS攻击")
|
||||
void shouldCleanXssInFormDataRequests() {
|
||||
String maliciousData = "username=<script>alert('xss')</script>&email=test@example.com";
|
||||
DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.wrap(maliciousData.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/test")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.body(Flux.just(buffer));
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(xssFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该处理空请求体")
|
||||
void shouldHandleEmptyRequestBody() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/test")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body(Flux.empty());
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(xssFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该处理无效JSON")
|
||||
void shouldHandleInvalidJson() {
|
||||
String invalidJson = "{invalid json}";
|
||||
DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.wrap(invalidJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/test")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body(Flux.just(buffer));
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(xssFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.destiny.gateway.handler;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("客户端处理器测试")
|
||||
class ClientHandlerTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建处理器")
|
||||
void testClientHandlerCreation() {
|
||||
ClientHandler handler = new ClientHandler();
|
||||
assertNotNull(handler, "处理器不应为null");
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.destiny.gateway.handler;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("健康检查处理器测试")
|
||||
class HealthHandlerTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建处理器")
|
||||
void testHealthHandlerCreation() {
|
||||
HealthHandler handler = new HealthHandler();
|
||||
assertNotNull(handler, "处理器不应为null");
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.destiny.gateway.handler;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("Swagger处理器测试")
|
||||
class SwaggerHandlerTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该成功创建处理器")
|
||||
void testSwaggerHandlerCreation() {
|
||||
SwaggerHandler handler = new SwaggerHandler();
|
||||
assertNotNull(handler, "处理器不应为null");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user