Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions backend-services/api-gateway/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,45 @@
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version> <!-- 使用一个较新的稳定版本 -->
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<!-- 版本号会从你的 dependencyManagement 中继承 -->
</dependency>
</dependencies>

<!-- 3. 添加构建插件,用于打包成可执行jar -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ai.qa.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ai.qa.gateway.api.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/test")
public class TestConfigController {


@Value("${jwt.secret}")
private String jwtSecret;

@GetMapping("/config")
public String login() {
System.out.println("测试config");
return "测试JWT:"+jwtSecret;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.ai.qa.gateway.api.web.filter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

//@Component
//@RefreshScope // 为了动态刷新JWT密钥
public class AuthenticationFilter implements GlobalFilter, Ordered {

@Value("${jwt.secret}")
private String jwtSecret;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();

// 定义白名单路径,这些路径不需要JWT验证
List<String> whiteList = List.of("/api/user/register", "/api/user/login");
if (whiteList.contains(request.getURI().getPath())) {
return chain.filter(exchange); // 放行
}

String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}

String token = authHeader.substring(7);
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtSecret.getBytes())
.build()
.parseClaimsJws(token)
.getBody();

// 验证通过,可以将用户信息放入请求头,传递给下游服务
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Name", claims.get("username", String.class))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}

@Override
public int getOrder() {
// 鉴权过滤器应在日志过滤器之后,在路由之前,优先级要高
return -100;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.ai.qa.gateway.infrastructure.config;

import com.google.common.util.concurrent.RateLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Configuration
public class InMemoryRateLimiterConfig {
// 定义默认的限流速率
@Value("${default-limiter.defaultReplenishRate}")
private double defaultReplenishRate; // 每秒生成的令牌数
@Value("${default-limiter.defaultBurstCapacity}")
private int defaultBurstCapacity; // 令牌桶总容量

private static final Logger log = LoggerFactory.getLogger(InMemoryRateLimiterConfig.class);
// ipKeyResolver Bean 保持不变
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}

/**
* 自定义的内存限流器 Bean (完整版)
*/
@Bean
@Primary
public org.springframework.cloud.gateway.filter.ratelimit.RateLimiter<InMemoryRateLimiterConfig.RateLimiterConfig> inMemoryRateLimiter() {

return new org.springframework.cloud.gateway.filter.ratelimit.RateLimiter<RateLimiterConfig>() {

private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, RateLimiterConfig> configs = new ConcurrentHashMap<>();

@Override
public Mono<Response> isAllowed(String routeId, String id) {
// routeId 是当前请求匹配的路由ID
// id 是 KeyResolver 解析出的 key (IP地址)
// 获取当前路由的配置,如果不存在则使用默认配置
RateLimiterConfig config = configs.getOrDefault(routeId, new RateLimiterConfig(defaultReplenishRate, defaultBurstCapacity));

// 根据 IP 地址获取或创建 Guava RateLimiter
RateLimiter limiter = limiters.computeIfAbsent(id, k -> {
// 使用配置的速率来创建限流器
RateLimiter newLimiter = RateLimiter.create(config.getReplenishRate());
// 预热 Guava RateLimiter (可选,但更好),让令牌桶初始时就是满的
newLimiter.tryAcquire(config.getBurstCapacity());
return newLimiter;
});

// 尝试获取一个令牌
boolean allowed = limiter.tryAcquire();

if (allowed) {
log.info("Request ALLOWED. Route: {}, Key: {}", routeId, id);
return Mono.just(new Response(true, new HashMap<>()));
} else {
log.warn("Request DENIED (Rate Limited). Route: {}, Key: {}", routeId, id);
// 当被限流时,可以返回一些有用的头信息
HashMap<String, String> headers = new HashMap<>();
headers.put("X-RateLimit-Remaining", "0");
headers.put("X-RateLimit-Burst-Capacity", String.valueOf(config.getBurstCapacity()));
headers.put("X-RateLimit-Replenish-Rate", String.valueOf(config.getReplenishRate()));
return Mono.just(new Response(false, headers));
}
}

// =======================================================
// 补全缺失的方法
// =======================================================
@Override
public Map<String, RateLimiterConfig> getConfig() {
// 返回当前所有路由的配置信息
return this.configs;
}
// =======================================================

@Override
public Class<RateLimiterConfig> getConfigClass() {
return RateLimiterConfig.class;
}

@Override
public RateLimiterConfig newConfig() {
// 提供一个默认的空配置对象
return new RateLimiterConfig();
}
};
}

/**
* 配置类,用于存储限流参数
* 现在它不再是空的了,包含了速率和容量
*/
public static class RateLimiterConfig {
private double replenishRate;
private int burstCapacity;

public RateLimiterConfig() {
}

public RateLimiterConfig(double replenishRate, int burstCapacity) {
this.replenishRate = replenishRate;
this.burstCapacity = burstCapacity;
}

// Getters and Setters
public double getReplenishRate() {
return replenishRate;
}
public void setReplenishRate(double replenishRate) {
this.replenishRate = replenishRate;
}
public int getBurstCapacity() {
return burstCapacity;
}
public void setBurstCapacity(int burstCapacity) {
this.burstCapacity = burstCapacity;
}
}
}
45 changes: 37 additions & 8 deletions backend-services/api-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
server:
port: 8080 # 所有后端请求的入口
logging:
level:
# 将 Gateway 的核心日志级别设置为 DEBUG
org.springframework.cloud.gateway: DEBUG
# (可选) 将 Reactor Netty 的日志也设为 DEBUG,可以看到更底层的网络交互
reactor.netty.http.client: DEBUG
com.alibaba.nacos.client: DEBUG
spring:
application:
name: api-gateway
name: api-gateway-xuwei
config:
import:
# 引入共享配置
- "nacos:shared-config-xuwei.yml"
cloud:
nacos:
discovery:
server-addr: localhost:8848
server-addr: 54.219.180.170:8848
config:
# 明确告诉 Nacos Config 默认的文件扩展名是 yml
file-extension: yml
group: DEFAULT_GROUP
gateway:
discovery:
locator:
enabled: true # 开启基于服务发现的路由功能
lower-case-service-id: true
enabled: false # 开启基于服务发现的路由功能
lower-case-service-id: true # 将服务名转为小写路径,e.g., user-service -> /user-service/**
routes:
- id: user_service_route
uri: lb://user-service # lb:// 表示从Nacos负载均衡地选择一个user-service实例
uri: lb://user-service-xuwei # lb:// 表示从Nacos负载均衡地选择一个user-service实例
# uri: http://192.168.31.186:8081
predicates:
- Path=/api/user/** # 匹配所有/api/user/开头的请求
# filters:
# - StripPrefix=2 #跳过/api/user直接匹配/**
- id: niki_service_route
uri: lb://user-service-xuwei # lb:// 表示从Nacos负载均衡地选择一个user-service实例
predicates:
- Path=/api/niki/**
filters:
- StripPrefix=1 #跳过/api直接匹配/niki/**
- id: qa_service_route
uri: lb://qa-service
uri: lb://qa-service-xuwei
predicates:
- Path=/api/qa/**
- Path=/api/qa/**
default-filters:
# 配置默认的限流过滤器
- name: RequestRateLimiter
args:
key-resolver: '#{@ipKeyResolver}'

Loading