feat(gateway): Implement gateway module foundation

This commit is contained in:
2025-03-29 21:30:20 +08:00
parent c994e72960
commit 4ef46e6fa3
9 changed files with 556 additions and 5 deletions
+56
View File
@@ -24,6 +24,12 @@
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> </dependency>
<!-- Spring Cloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Sentinel --> <!-- Spring Cloud Alibaba Sentinel -->
<dependency> <dependency>
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
@@ -41,5 +47,55 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
</dependency> </dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Commons IO -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
<!-- JJWT (JSON Web Token) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>
@@ -0,0 +1,30 @@
package com.nuaa.video.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* 跨域配置
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*"); // 允许所有来源
config.addAllowedHeader("*"); // 允许所有请求头
config.addAllowedMethod("*"); // 允许所有方法
config.setAllowCredentials(true); // 允许携带凭证
config.setMaxAge(3600L); // 预检请求的有效期,单位为秒
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
@@ -0,0 +1,70 @@
package com.nuaa.video.gateway.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import jakarta.annotation.Resource;
/**
* 动态路由配置
*/
@Slf4j
@Component
public class DynamicRouteConfig implements ApplicationEventPublisherAware {
@Resource
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
* 添加路由
*/
public void addRoute(RouteDefinition definition) {
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("成功添加路由: {}", definition.getId());
} catch (Exception e) {
log.error("添加路由失败: {}", definition.getId(), e);
}
}
/**
* 更新路由
*/
public void updateRoute(RouteDefinition definition) {
try {
this.deleteRoute(definition.getId());
this.addRoute(definition);
log.info("成功更新路由: {}", definition.getId());
} catch (Exception e) {
log.error("更新路由失败: {}", definition.getId(), e);
}
}
/**
* 删除路由
*/
public void deleteRoute(String id) {
try {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("成功删除路由: {}", id);
} catch (Exception e) {
log.error("删除路由失败: {}", id, e);
}
}
}
@@ -0,0 +1,78 @@
package com.nuaa.video.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 网关降级配置
*/
@Configuration
public class GatewayFallbackConfig {
/**
* 初始化自定义限流响应和规则
*/
@PostConstruct
public void doInit() {
// 自定义限流响应
initBlockHandlers();
// 初始化QPS限流规则
initQpsLimitRule();
}
/**
* 自定义限流响应
*/
private void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = (serverWebExchange, throwable) -> {
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
response.put("message", "请求过于频繁,请稍后再试");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(response));
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
/**
* 初始化QPS限流规则
*/
private void initQpsLimitRule() {
Set<GatewayFlowRule> rules = new HashSet<>();
// 对每个API路径设置限流
// 用户服务限流
rules.add(new GatewayFlowRule("video-user")
.setCount(100)
.setIntervalSec(1));
// 内容服务限流
rules.add(new GatewayFlowRule("video-content")
.setCount(200)
.setIntervalSec(1));
// 认证服务限流
rules.add(new GatewayFlowRule("video-auth")
.setCount(50)
.setIntervalSec(1));
// 加载限流规则
GatewayRuleManager.loadRules(rules);
}
}
@@ -0,0 +1,144 @@
package com.nuaa.video.gateway.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* JWT认证过滤器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
private String secret;
@Value("${jwt.token-header:Authorization}")
private String tokenHeader;
@Value("${gateway.ignore.urls:/auth/login,/auth/register,/auth/captcha}")
private List<String> ignoreUrls;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 跳过不需要验证的路径
if (isIgnorePath(path)) {
return chain.filter(exchange);
}
// 获取token
String token = getToken(request);
if (!StringUtils.hasText(token)) {
return unauthorized(exchange, "缺少token");
}
try {
// 验证token
Claims claims = parseToken(token);
// 将用户信息传递到下游服务
ServerHttpRequest.Builder mutate = request.mutate();
mutate.header("userId", claims.getSubject());
mutate.header("username", claims.get("username", String.class));
return chain.filter(exchange.mutate().request(mutate.build()).build());
} catch (Exception e) {
log.error("Token验证失败", e);
return unauthorized(exchange, "Token验证失败");
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
/**
* 检查是否为忽略路径
*/
private boolean isIgnorePath(String path) {
return ignoreUrls.stream().anyMatch(ignore -> pathMatcher.match(ignore, path));
}
/**
* 从请求中获取token
*/
private String getToken(ServerHttpRequest request) {
String token = request.getHeaders().getFirst(tokenHeader);
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
return token.substring(7);
}
return null;
}
/**
* 解析token
*/
private Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 获取签名密钥
*/
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 返回未授权响应
*/
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.UNAUTHORIZED.value());
result.put("message", message);
try {
byte[] bytes = objectMapper.writeValueAsBytes(result);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
} catch (JsonProcessingException e) {
log.error("响应JSON转换异常", e);
return response.setComplete();
}
}
}
@@ -0,0 +1,49 @@
package com.nuaa.video.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
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.net.URI;
import java.util.List;
import java.util.Map;
/**
* 请求日志过滤器
*/
@Slf4j
@Component
public class RequestLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
String method = request.getMethod().name();
String host = request.getRemoteAddress().getHostString();
// 记录请求日志
log.info("Request: {} {} from {}", method, uri, host);
// 记录请求头信息(生产环境中可以根据需要调整日志级别)
if (log.isDebugEnabled()) {
Map<String, List<String>> headers = request.getHeaders();
headers.forEach((name, values) ->
log.debug("Header: {} = {}", name, String.join(",", values)));
}
// 执行后续过滤器
return chain.filter(exchange).then(Mono.fromRunnable(() ->
log.info("Response for: {} {}", method, uri)));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 10;
}
}
@@ -0,0 +1,77 @@
package com.nuaa.video.gateway.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; // 导入HttpStatusCode
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*/
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
// 设置响应的状态码
if (response.isCommitted()) {
return Mono.error(ex);
}
HttpStatusCode status; // 修改为HttpStatusCode类型
String message;
int statusCode; // 添加一个int类型的状态码变量
// 根据异常类型设置响应状态和消息
if (ex instanceof ResponseStatusException) {
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
status = responseStatusException.getStatusCode();
message = responseStatusException.getReason();
statusCode = status.value(); // 获取状态码的数值
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = "服务器内部错误";
statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value();
}
log.error("网关异常: {}", message, ex);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(status);
// 构建响应体
Map<String, Object> result = new HashMap<>();
result.put("code", statusCode); // 使用状态码数值
result.put("message", message);
try {
byte[] bytes = objectMapper.writeValueAsBytes(result);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
} catch (JsonProcessingException e) {
log.error("响应JSON转换异常", e);
return response.setComplete();
}
}
}
@@ -0,0 +1,15 @@
spring:
cloud:
sentinel:
log:
dir: ./logs/sentinel/
switch-pid: true
gateway:
httpclient:
connect-timeout: 60000
response-timeout: 60000
logging:
level:
org.springframework.cloud.gateway: debug
reactor.netty.http.client: debug
@@ -4,12 +4,26 @@ server:
spring: spring:
application: application:
name: video-gateway name: video-gateway
profiles:
active: dev
config:
import:
- optional:nacos:${spring.application.name}.yaml
cloud: cloud:
nacos: nacos:
discovery: discovery:
server-addr: 5.180.253.115:8848 server-addr: ${NACOS_HOST:5.180.253.115}:${NACOS_PORT:8848}
namespace: public namespace: ${NACOS_NAMESPACE:public}
group: DEFAULT_GROUP group: ${NACOS_GROUP:DEFAULT_GROUP}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
file-extension: yaml
extension-configs:
- data-id: video-gateway.yaml
group: ${NACOS_GROUP:DEFAULT_GROUP}
refresh: true
gateway: gateway:
discovery: discovery:
locator: locator:
@@ -54,7 +68,7 @@ spring:
- StripPrefix=1 - StripPrefix=1
sentinel: sentinel:
transport: transport:
dashboard: 5.180.253.115:8080 dashboard: ${SENTINEL_DASHBOARD_HOST:5.180.253.115}:${SENTINEL_DASHBOARD_PORT:8080}
port: 8719 port: 8719
eager: true eager: true
scg: scg:
@@ -67,3 +81,21 @@ management:
web: web:
exposure: exposure:
include: '*' include: '*'
endpoint:
health:
show-details: always
gateway:
enabled: true
# 不需要鉴权的URL列表
gateway:
ignore:
urls:
- /auth/login
- /auth/register
- /auth/captcha
- /*/v3/api-docs
- /swagger-resources/**
- /swagger-ui/**
- /actuator/**
- /favicon.ico