1
0
This commit is contained in:
lingxiao865
2026-02-10 08:54:10 +08:00
parent b230d23173
commit e394f2b51f
27 changed files with 345 additions and 146 deletions

View File

@@ -0,0 +1,13 @@
package com.example.geteway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

View File

@@ -0,0 +1,37 @@
package com.example.geteway.config;
import org.jspecify.annotations.NonNull;
import org.springframework.http.HttpCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class CookieBearerTokenResolver extends ServerBearerTokenAuthenticationConverter {
private final String cookieName;
public CookieBearerTokenResolver(String cookieName) {
this.cookieName = cookieName;
}
@Override
public @NonNull Mono<Authentication> convert(ServerWebExchange exchange) {
HttpCookie cookie = exchange.getRequest().getCookies().getFirst(cookieName);
if (cookie == null || !StringUtils.hasText(cookie.getValue())) {
// No token → return empty → triggers 401 Unauthorized
return Mono.empty();
}
String token = cookie.getValue().trim();
if (token.isEmpty()) {
return Mono.empty();
}
// Create authentication token with the extracted JWT
return Mono.just(new BearerTokenAuthenticationToken(token));
}
}

View File

@@ -0,0 +1,22 @@
package com.example.geteway.config;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
class InstallOpenTelemetryAppender implements InitializingBean {
private final OpenTelemetry openTelemetry;
InstallOpenTelemetryAppender(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@Override
public void afterPropertiesSet() {
OpenTelemetryAppender.install(this.openTelemetry);
}
}

View File

@@ -0,0 +1,54 @@
package com.example.geteway.config;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmClassLoadingMeterConventions;
import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmCpuMeterConventions;
import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmMemoryMeterConventions;
import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmThreadMeterConventions;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.observation.OpenTelemetryServerRequestObservationConvention;
import java.util.List;
/**
* 指标配置,选择需要的指标
*/
@Configuration(proxyBeanMethods = false)
public class OpenTelemetryConfiguration {
@Bean
OpenTelemetryServerRequestObservationConvention openTelemetryServerRequestObservationConvention() {
return new OpenTelemetryServerRequestObservationConvention();
}
@Bean
OpenTelemetryJvmCpuMeterConventions openTelemetryJvmCpuMeterConventions() {
return new OpenTelemetryJvmCpuMeterConventions(Tags.empty());
}
@Bean
ProcessorMetrics processorMetrics() {
return new ProcessorMetrics(List.of(), new OpenTelemetryJvmCpuMeterConventions(Tags.empty()));
}
@Bean
JvmMemoryMetrics jvmMemoryMetrics() {
return new JvmMemoryMetrics(List.of(), new OpenTelemetryJvmMemoryMeterConventions(Tags.empty()));
}
@Bean
JvmThreadMetrics jvmThreadMetrics() {
return new JvmThreadMetrics(List.of(), new OpenTelemetryJvmThreadMeterConventions(Tags.empty()));
}
@Bean
ClassLoaderMetrics classLoaderMetrics() {
return new ClassLoaderMetrics(new OpenTelemetryJvmClassLoadingMeterConventions());
}
}

View File

@@ -0,0 +1,73 @@
package com.example.geteway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import java.util.List;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig implements WebFluxConfigurer {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges ->
exchanges
.pathMatchers("/api/public/**", "/api/auth/**").permitAll() // 公共API路径
.pathMatchers("/api/**").authenticated() // 保护你的 API 路由
.anyExchange().permitAll()
)
.oauth2ResourceServer(oauth2 ->
oauth2.bearerTokenConverter(new CookieBearerTokenResolver("access_token"))
.jwt(Customizer.withDefaults())
)
.cors(cors -> cors.configurationSource(corsConfigurationSource())); // 使用自定义CORS配置
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowedMethods(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
CorsWebFilter corsWebFilter() {
return new CorsWebFilter(corsConfigurationSource());
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("*")
.allowCredentials(true);
}
}

View File

@@ -0,0 +1,38 @@
package com.example.geteway.config;
import io.micrometer.tracing.TraceContext;
import io.micrometer.tracing.Tracer;
import jakarta.annotation.Nullable;
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
class TraceIdFilter implements WebFilter {
private final Tracer tracer;
TraceIdFilter(Tracer tracer) {
this.tracer = tracer;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String traceId = getTraceId();
if (traceId != null) {
exchange.getRequest().mutate()
.header("X-Trace-Id", traceId)
.build();
}
return chain.filter(exchange);
}
private @Nullable String getTraceId() {
TraceContext context = this.tracer.currentTraceContext().context();
return context != null ? context.traceId() : null;
}
}

View File

@@ -0,0 +1,41 @@
package com.example.geteway.config;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Order(1000)
public class UserIdMappingFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println(exchange.getRequest().getHeaders());
return ReactiveSecurityContextHolder.getContext()
.mapNotNull(SecurityContext::getAuthentication)
.cast(JwtAuthenticationToken.class) // ✅ 先转为 JwtAuthenticationToken
.map(AbstractOAuth2TokenAuthenticationToken::getToken) // ✅ 再获取 Jwt
.map(jwt -> {
String userId = jwt.getSubject(); // ✅ 现在可以安全调用
if (userId == null || userId.isBlank()) {
throw new IllegalArgumentException("JWT missing 'sub' claim");
}
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.header("X-User-Id", userId)
.build();
return exchange.mutate().request(modifiedRequest).build();
})
.switchIfEmpty(Mono.just(exchange))
.flatMap(chain::filter);
}
}

View File

@@ -0,0 +1,70 @@
server:
port: 8083
#spring:
# application:
# name: api-gateway
# cloud:
# gateway:
# server:
# webflux:
# default-filters:
# - RewritePath=/api/(?<service>.*)/(?<path>.*), /$\{path}
# enabled: true
# routes:
# - id: user-service
# uri: http://a-service:8091
# predicates:
# - Path=/api/auth/**
# security:
# oauth2:
# resourceserver:
# jwt:
# jwk-set-uri: http://auth-service:9000/oauth2/jwks
# client:
# provider:
# spring:
# issuer-uri: http://auth-service:9000
spring:
cloud:
gateway:
server:
webflux:
default-filters:
- RewritePath=/api/(?<service>.*)/(?<path>.*), /$\{path}
enabled: true
routes:
- id: user-service
uri: http://localhost:8091
predicates:
- Path=/api/auth/**
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:9000/oauth2/jwks
client:
provider:
spring:
issuer-uri: http://localhost:9000
management:
otlp:
metrics:
export:
url: http://192.168.1.14:9090/api/v1/otlp/v1/metrics #Prometheus otlp协议 http地址
step: 30s
opentelemetry:
tracing:
export:
otlp:
endpoint: http://192.168.1.14:4317/v1/traces #Jaeger otlp协议 grpc地址
transport: grpc
# endpoint: http://localhost:4318/v1/traces
# transport: http
logging:
export:
otlp:
endpoint: http://192.168.1.14:32664/otlp/v1/logs #Loki otlp协议 grpc地址
# transport: grpc

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="OTEL"/>
</root>
</configuration>

View File

@@ -0,0 +1,13 @@
package com.example.geteway;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class GatewayApplicationTests {
@Test
void contextLoads() {
}
}