diff --git a/video-gateway/pom.xml b/video-gateway/pom.xml
index 58927f2..c26f197 100644
--- a/video-gateway/pom.xml
+++ b/video-gateway/pom.xml
@@ -24,6 +24,12 @@
spring-cloud-starter-alibaba-nacos-discovery
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
com.alibaba.cloud
@@ -41,5 +47,55 @@
org.springframework.boot
spring-boot-starter-actuator
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ commons-io
+ commons-io
+ 2.15.1
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.3
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.3
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.3
+ runtime
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
\ No newline at end of file
diff --git a/video-gateway/src/main/java/com/nuaa/video/gateway/config/CorsConfig.java b/video-gateway/src/main/java/com/nuaa/video/gateway/config/CorsConfig.java
new file mode 100644
index 0000000..4144ecb
--- /dev/null
+++ b/video-gateway/src/main/java/com/nuaa/video/gateway/config/CorsConfig.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/video-gateway/src/main/java/com/nuaa/video/gateway/config/DynamicRouteConfig.java b/video-gateway/src/main/java/com/nuaa/video/gateway/config/DynamicRouteConfig.java
new file mode 100644
index 0000000..b0ecd4b
--- /dev/null
+++ b/video-gateway/src/main/java/com/nuaa/video/gateway/config/DynamicRouteConfig.java
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/video-gateway/src/main/java/com/nuaa/video/gateway/config/GatewayFallbackConfig.java b/video-gateway/src/main/java/com/nuaa/video/gateway/config/GatewayFallbackConfig.java
new file mode 100644
index 0000000..fff0c5b
--- /dev/null
+++ b/video-gateway/src/main/java/com/nuaa/video/gateway/config/GatewayFallbackConfig.java
@@ -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 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 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);
+ }
+}
\ No newline at end of file
diff --git a/video-gateway/src/main/java/com/nuaa/video/gateway/filter/AuthGlobalFilter.java b/video-gateway/src/main/java/com/nuaa/video/gateway/filter/AuthGlobalFilter.java
new file mode 100644
index 0000000..6fb68a5
--- /dev/null
+++ b/video-gateway/src/main/java/com/nuaa/video/gateway/filter/AuthGlobalFilter.java
@@ -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 ignoreUrls;
+
+ private final AntPathMatcher pathMatcher = new AntPathMatcher();
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ public Mono 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 unauthorized(ServerWebExchange exchange, String message) {
+ ServerHttpResponse response = exchange.getResponse();
+ response.setStatusCode(HttpStatus.UNAUTHORIZED);
+ response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
+
+ Map 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/video-gateway/src/main/java/com/nuaa/video/gateway/filter/RequestLogFilter.java b/video-gateway/src/main/java/com/nuaa/video/gateway/filter/RequestLogFilter.java
new file mode 100644
index 0000000..02635fa
--- /dev/null
+++ b/video-gateway/src/main/java/com/nuaa/video/gateway/filter/RequestLogFilter.java
@@ -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 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> 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;
+ }
+}
\ No newline at end of file
diff --git a/video-gateway/src/main/java/com/nuaa/video/gateway/handler/GlobalExceptionHandler.java b/video-gateway/src/main/java/com/nuaa/video/gateway/handler/GlobalExceptionHandler.java
new file mode 100644
index 0000000..2e894d6
--- /dev/null
+++ b/video-gateway/src/main/java/com/nuaa/video/gateway/handler/GlobalExceptionHandler.java
@@ -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 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 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/video-gateway/src/main/resources/application-dev.yml b/video-gateway/src/main/resources/application-dev.yml
new file mode 100644
index 0000000..a6a47c6
--- /dev/null
+++ b/video-gateway/src/main/resources/application-dev.yml
@@ -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
\ No newline at end of file
diff --git a/video-gateway/src/main/resources/application.yml b/video-gateway/src/main/resources/application.yml
index d89b735..66798c2 100644
--- a/video-gateway/src/main/resources/application.yml
+++ b/video-gateway/src/main/resources/application.yml
@@ -4,12 +4,26 @@ server:
spring:
application:
name: video-gateway
+ profiles:
+ active: dev
+ config:
+ import:
+ - optional:nacos:${spring.application.name}.yaml
cloud:
nacos:
discovery:
- server-addr: 5.180.253.115:8848
- namespace: public
- group: DEFAULT_GROUP
+ server-addr: ${NACOS_HOST:5.180.253.115}:${NACOS_PORT:8848}
+ namespace: ${NACOS_NAMESPACE:public}
+ 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:
discovery:
locator:
@@ -54,7 +68,7 @@ spring:
- StripPrefix=1
sentinel:
transport:
- dashboard: 5.180.253.115:8080
+ dashboard: ${SENTINEL_DASHBOARD_HOST:5.180.253.115}:${SENTINEL_DASHBOARD_PORT:8080}
port: 8719
eager: true
scg:
@@ -66,4 +80,22 @@ management:
endpoints:
web:
exposure:
- include: '*'
\ No newline at end of file
+ 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
\ No newline at end of file