diff --git a/gateway/.drone.yml b/gateway/.drone.yml
new file mode 100644
index 0000000..94bcee9
--- /dev/null
+++ b/gateway/.drone.yml
@@ -0,0 +1,119 @@
+clone:
+ image: registry.t-aaron.com/drone/git:latest
+
+kind: pipeline
+type: kubernetes
+name: gateway
+
+volumes:
+ - name: maven-cache
+ host:
+ path: /opt/maven-cache-default
+ - name: sonar-cache
+ host:
+ path: /opt/sonar-cache-default
+
+steps:
+ - name: download-dependencies
+ image: registry.t-aaron.com/maven:3.8.6-openjdk-11-slim
+ volumes:
+ - name: maven-cache
+ path: /root/.m2
+ commands:
+ - echo "配置 Maven 镜像源..."
+ - mkdir -p /root/.m2
+ - cp settings.xml /root/.m2/settings.xml
+ - echo "开始下载 Maven 依赖..."
+ - mvn dependency:go-offline -B
+ - echo "依赖下载完成!"
+ - echo "将本地 Maven 缓存同步到工作区用于后续构建..."
+ - mkdir -p /drone/src/.m2
+ - cp -a /root/.m2/. /drone/src/.m2/
+
+ - name: package
+ image: registry.t-aaron.com/maven:3.8.6-openjdk-11-slim
+ volumes:
+ - name: maven-cache
+ path: /root/.m2
+ commands:
+ - echo "配置 Maven 镜像源..."
+ - mkdir -p /root/.m2
+ - cp settings.xml /root/.m2/settings.xml
+ - echo "开始构建 JAR 包..."
+ - mvn clean package -DskipTests -B
+ - echo "JAR 包构建完成!"
+ - ls -la target/*.jar
+ when:
+ event: [ push, pull_request ]
+ depends_on:
+ - download-dependencies
+
+ - name: sonar-scan
+ image: registry.t-aaron.com/maven:3.8.6-openjdk-11-slim
+ volumes:
+ - name: maven-cache
+ path: /root/.m2
+ - name: sonar-cache
+ path: /root/.sonar/cache
+ commands:
+ - echo "配置 Maven 镜像源..."
+ - mkdir -p /root/.m2
+ - cp settings.xml /root/.m2/settings.xml
+ - echo "开始 SonarQube 代码质量检查..."
+ - echo "清理之前的构建文件..."
+ - rm -rf target/ .mvn/ .classpath .project .settings/
+ - echo "编译代码..."
+ - mvn clean compile
+ - echo "执行 SonarQube 扫描..."
+ - mvn sonar:sonar -Dsonar.projectKey=gateway -Dsonar.host.url=https://sonar-ops.t-aaron.com/sonar -Dsonar.login=$SONAR_TOKEN -Dsonar.projectName="Gateway" -Dsonar.projectVersion=${DRONE_COMMIT_SHA:0:8} -Dsonar.sources=src/main/java -Dsonar.java.binaries=target/classes
+ - echo "SonarQube 代码质量检查完成!"
+ environment:
+ SONAR_TOKEN:
+ from_secret: SONAR_TOKEN
+ when:
+ event: [ push, pull_request ]
+ depends_on:
+ - download-dependencies
+
+ - name: build-and-push
+ image: registry.t-aaron.com/plugins/kaniko
+ settings:
+ registry: registry.t-aaron.com
+ repo: registry.t-aaron.com/tuoheng/gateway
+ cache: true
+ cache_repo: registry.t-aaron.com/kaniko/cache-gateway
+ build_args:
+ - MAVEN_MIRROR_URL=https://maven.aliyun.com/repository/public
+ username:
+ from_secret: REGISTRY_USERNAME
+ password:
+ from_secret: REGISTRY_PASSWORD
+ tags:
+ - latest
+ - ${DRONE_COMMIT_SHA:0:8}
+ dockerfile: Dockerfile
+ context: .
+ when:
+ event: [ push, tag ]
+ depends_on:
+ - package
+
+ - name: deploy-to-k8s
+ image: registry.t-aaron.com/alpine/k8s:1.25.9
+ commands:
+ - echo "部署/更新 gateway 到 default 命名空间"
+ - |
+ kubectl create deployment gateway \
+ --image=registry.t-aaron.com/tuoheng/gateway:${DRONE_COMMIT_SHA:0:8} \
+ --port=8080 -n default --dry-run=client -o yaml | kubectl apply -f -
+ - kubectl set image deployment/gateway gateway=registry.t-aaron.com/tuoheng/gateway:${DRONE_COMMIT_SHA:0:8} -n default --record=true || true
+ - kubectl create service clusterip gateway --tcp=8080:8080 -n default --dry-run=client -o yaml | kubectl apply -f -
+ - echo "等待 Deployment 就绪..."
+ - kubectl rollout status deployment/gateway -n default --timeout=300s
+ - echo "查看服务与Pod状态"
+ - kubectl get deploy,svc -n default | grep -i gateway || true
+ - kubectl get pods -n default -l app=gateway || kubectl get pods -n default | grep gateway || true
+ when:
+ event: [ push ]
+ depends_on:
+ - build-and-push
\ No newline at end of file
diff --git a/gateway/.gitignore b/gateway/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/gateway/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/gateway/Dockerfile b/gateway/Dockerfile
new file mode 100644
index 0000000..184a471
--- /dev/null
+++ b/gateway/Dockerfile
@@ -0,0 +1,27 @@
+# 生产阶段 - 仅复制预构建的 JAR 文件
+FROM registry.t-aaron.com/openjdk:11-jre-slim
+
+# 创建应用用户
+RUN groupadd -r appuser && useradd -r -g appuser appuser
+
+# 设置工作目录
+WORKDIR /app
+
+# 复制预构建的 JAR 文件
+COPY target/*.jar app.jar
+
+# 更改文件所有者
+RUN chown -R appuser:appuser /app
+
+# 切换到应用用户
+USER appuser
+
+# 暴露端口
+EXPOSE 8080
+
+# 健康检查
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD netstat -an | grep :8080 | grep LISTEN || exit 1
+
+# 启动应用
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/gateway/README.md b/gateway/README.md
new file mode 100644
index 0000000..13a2d63
--- /dev/null
+++ b/gateway/README.md
@@ -0,0 +1,48 @@
+# Test pipeline trigger
+# Test pipeline with secrets permission
+# Test with image mirroring
+# Test with local images - drone/git and drone/placeholder
+# Test with updated RBAC permissions
+# Test with uploaded alpine image
+# Test host volume mount
+# Test host volume mount again
+# Test after server config update
+# Test with emptyDir volume
+Trigger new build
+Trigger Kaniko build
+Retrigger build with correct image
+Trigger build test
+Test build
+Test registry mirror
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+xx
+a
+a
+a
+xx
+xx
+xx
diff --git a/gateway/pom.xml b/gateway/pom.xml
new file mode 100644
index 0000000..0caeac4
--- /dev/null
+++ b/gateway/pom.xml
@@ -0,0 +1,122 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.7.18
+
+
+ com.tuoheng
+ gateway
+ 0.0.1-SNAPSHOT
+ gateway
+ Spring Boot 2.7.x Servlet Gateway
+
+ 11
+ 2021.0.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-gateway
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+ 2021.0.5.0
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+ 2021.0.5.0
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-bootstrap
+ 3.1.5
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-loadbalancer
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.8
+
+
+
+ prepare-agent
+
+
+
+ report
+ compile
+
+ report
+
+
+
+
+
+
+
+ org.sonarsource.scanner.maven
+ sonar-maven-plugin
+ 3.9.1.2184
+
+
+
+
\ No newline at end of file
diff --git a/gateway/settings.xml b/gateway/settings.xml
new file mode 100644
index 0000000..7749838
--- /dev/null
+++ b/gateway/settings.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ aliyun-all
+ Aliyun Maven (all)
+ https://maven.aliyun.com/repository/public
+ *
+
+
+
diff --git a/gateway/src/main/java/com/tuoheng/gateway/GatewayApplication.java b/gateway/src/main/java/com/tuoheng/gateway/GatewayApplication.java
new file mode 100644
index 0000000..186d9ac
--- /dev/null
+++ b/gateway/src/main/java/com/tuoheng/gateway/GatewayApplication.java
@@ -0,0 +1,13 @@
+package com.tuoheng.gateway;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+@SpringBootApplication
+@EnableDiscoveryClient
+public class GatewayApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(GatewayApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/gateway/src/main/java/com/tuoheng/gateway/config/SecurityConfig.java b/gateway/src/main/java/com/tuoheng/gateway/config/SecurityConfig.java
new file mode 100644
index 0000000..b65652c
--- /dev/null
+++ b/gateway/src/main/java/com/tuoheng/gateway/config/SecurityConfig.java
@@ -0,0 +1,22 @@
+package com.tuoheng.gateway.config;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+
+@Configuration
+@EnableWebFluxSecurity
+public class SecurityConfig {
+ @Bean
+ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
+ http
+ .authorizeExchange(exchanges -> exchanges
+ .pathMatchers("/a/**").authenticated()
+ .pathMatchers("/b/**").authenticated()
+ .anyExchange().permitAll()
+ )
+ .oauth2ResourceServer(oauth2 -> oauth2.jwt());
+ return http.build();
+ }
+}
\ No newline at end of file
diff --git a/gateway/src/main/java/com/tuoheng/gateway/filter/JwtPermissionFilter.java b/gateway/src/main/java/com/tuoheng/gateway/filter/JwtPermissionFilter.java
new file mode 100644
index 0000000..ff17242
--- /dev/null
+++ b/gateway/src/main/java/com/tuoheng/gateway/filter/JwtPermissionFilter.java
@@ -0,0 +1,77 @@
+package com.tuoheng.gateway.filter;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+
+@Component
+public class JwtPermissionFilter implements GlobalFilter, Ordered {
+
+
+ /**
+ * 这边能获取到Token里面的值
+ * @param exchange
+ * @param chain
+ * @return
+ */
+ @Override
+ public Mono filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
+
+ // 获取完整的请求URL
+ String fullUrl = exchange.getRequest().getURI().toString();
+ System.out.println("用户访问的完整URL: " + fullUrl);
+
+ // 获取请求的path
+ String path = exchange.getRequest().getPath().toString();
+ System.out.println("用户访问的path: " + path);
+
+ // 获取请求的host
+ String host = exchange.getRequest().getHeaders().getHost().toString();
+ System.out.println("用户访问的host: " + host);
+
+ String hostName = exchange.getRequest().getHeaders().getHost().getHostName();
+ System.out.println("用户访问的域名: " + hostName);
+
+ // 获取Referer(如果有)
+ String referer = exchange.getRequest().getHeaders().getFirst("Referer");
+ System.out.println("Referer: " + referer);
+// 从Spring Security上下文获取JWT
+ return ReactiveSecurityContextHolder.getContext()
+ .flatMap(securityContext -> {
+ if (securityContext.getAuthentication() instanceof JwtAuthenticationToken) {
+ JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) securityContext.getAuthentication();
+ Jwt jwt = jwtAuth.getToken();
+ String username = jwt.getClaimAsString("username");
+ String clientId = jwt.getClaimAsString("client_id");
+ String tenantCode = jwt.getClaimAsString("tenant_code");
+ String authorities = jwt.getClaimAsString("clientIds");
+
+ // 你可以在这里做权限判断
+ System.out.println("网关解析到token字段:");
+ System.out.println("用户名: " + username);
+ System.out.println("客户端ID: " + clientId);
+ System.out.println("租户代码: " + tenantCode);
+ System.out.println("用户权限: " + authorities);
+
+ //该域名没有权限
+ if(!authorities.contains(hostName)){
+ exchange.getResponse().setStatusCode(org.springframework.http.HttpStatus.FORBIDDEN);
+ return exchange.getResponse().setComplete();
+ }
+ }
+ return chain.filter(exchange);
+ });
+
+ }
+
+ @Override
+ public int getOrder() {
+ return -1; // 优先级高
+ }
+}
\ No newline at end of file
diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties
new file mode 100644
index 0000000..b5475db
--- /dev/null
+++ b/gateway/src/main/resources/application.properties
@@ -0,0 +1,26 @@
+server.port=8080
+
+# 应用名称
+spring.application.name=gateway
+
+# Nacos 服务发现配置
+spring.cloud.nacos.discovery.server-addr=nacos:8848
+spring.cloud.nacos.discovery.namespace=public
+spring.cloud.nacos.discovery.group=DEFAULT_GROUP
+spring.cloud.nacos.discovery.enabled=true
+
+# Gateway 路由配置 - 使用服务发现
+spring.cloud.gateway.routes[0].id=resource-server-a
+spring.cloud.gateway.routes[0].uri=lb://aserver
+spring.cloud.gateway.routes[0].predicates[0]=Path=/a/**
+spring.cloud.gateway.routes[0].filters[0]=RewritePath=/a/(?.*), /api/${segment}
+spring.cloud.gateway.routes[0].filters[1]=TokenRelay
+
+spring.cloud.gateway.routes[1].id=resource-server-b
+spring.cloud.gateway.routes[1].uri=lb://bserver
+spring.cloud.gateway.routes[1].predicates[0]=Path=/b/**
+spring.cloud.gateway.routes[1].filters[0]=RewritePath=/b/(?.*), /api/${segment}
+spring.cloud.gateway.routes[1].filters[1]=TokenRelay
+
+# OAuth2 配置
+spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://oidc:8080/oauth2/jwks
\ No newline at end of file
diff --git a/gateway/src/main/resources/bootstrap.properties b/gateway/src/main/resources/bootstrap.properties
new file mode 100644
index 0000000..4bc2f0c
--- /dev/null
+++ b/gateway/src/main/resources/bootstrap.properties
@@ -0,0 +1,9 @@
+# Bootstrap configuration for Nacos config center
+spring.application.name=gateway
+
+# Nacos config center configuration
+spring.cloud.nacos.config.server-addr=nacos:8848
+spring.cloud.nacos.config.namespace=public
+spring.cloud.nacos.config.group=DEFAULT_GROUP
+spring.cloud.nacos.config.file-extension=properties
+spring.cloud.nacos.config.enabled=true
diff --git a/pom.xml b/pom.xml
index 0fe87b6..4b08ada 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,12 +1,53 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- com.tuoheng
- hyf
- 1.0-SNAPSHOT
+ com.tuoheng.hxf
+ parent
+ 0.0.1-SNAPSHOT
- thingsboard-gateway-ws
- Archetype - thingsboard-gateway-ws
- http://maven.apache.org
+
+ thingsboard-gateway-ws-demo
+ jar
+
+ thingsboard-gateway-ws
+ WebSocket Gateway for ThingsBoard
+
+
+ UTF-8
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-gateway
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+
+
+ org.springframework.security
+ spring-security-oauth2-jose
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
diff --git a/src/main/java/com/tuoheng/gateway/GatewayApplication.java b/src/main/java/com/tuoheng/gateway/GatewayApplication.java
new file mode 100644
index 0000000..71fb3da
--- /dev/null
+++ b/src/main/java/com/tuoheng/gateway/GatewayApplication.java
@@ -0,0 +1,12 @@
+package com.tuoheng.gateway;
+
+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);
+ }
+}
diff --git a/src/main/java/com/tuoheng/gateway/config/GatewayConfig.java b/src/main/java/com/tuoheng/gateway/config/GatewayConfig.java
new file mode 100644
index 0000000..a641ed2
--- /dev/null
+++ b/src/main/java/com/tuoheng/gateway/config/GatewayConfig.java
@@ -0,0 +1,35 @@
+package com.tuoheng.gateway.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Gateway 路由配置
+ * 配置 WebSocket 转发规则
+ */
+@Slf4j
+@Configuration
+public class GatewayConfig {
+
+ /**
+ * 配置路由规则
+ * 将 /ws/api/** 转发到 ws://iot.t-aaron.com:18080/api/**
+ */
+ @Bean
+ public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
+ return builder.routes()
+ .route("websocket-route", r -> r
+ .path("/ws/api/**")
+ .filters(f -> f
+ .stripPrefix(1) // 移除 /ws 前缀
+ .filter(new com.tuoheng.gateway.filter.WebSocketFilter().apply(
+ new com.tuoheng.gateway.filter.WebSocketFilter.Config()))
+ )
+ .uri("ws://iot.t-aaron.com:18080")
+ )
+ .build();
+ }
+}
diff --git a/src/main/java/com/tuoheng/gateway/config/SecurityConfig.java b/src/main/java/com/tuoheng/gateway/config/SecurityConfig.java
new file mode 100644
index 0000000..dbced4f
--- /dev/null
+++ b/src/main/java/com/tuoheng/gateway/config/SecurityConfig.java
@@ -0,0 +1,52 @@
+package com.tuoheng.gateway.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+
+@Configuration
+@EnableWebFluxSecurity
+public class SecurityConfig {
+
+ @Bean
+ @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.enabled", havingValue = "true", matchIfMissing = false)
+ public SecurityWebFilterChain securityWebFilterChainWithJwt(ServerHttpSecurity http) {
+ http
+ .authorizeExchange(exchanges -> exchanges
+ .pathMatchers("/ws/api/**").authenticated()
+ .anyExchange().permitAll()
+ )
+ .oauth2ResourceServer(oauth2 -> oauth2.jwt())
+ .csrf().disable();
+ return http.build();
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.enabled", havingValue = "false", matchIfMissing = true)
+ public SecurityWebFilterChain securityWebFilterChainWithoutJwt(ServerHttpSecurity http) {
+ http
+ .authorizeExchange(exchanges -> exchanges
+ .anyExchange().permitAll()
+ )
+ .csrf().disable();
+ return http.build();
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.enabled", havingValue = "true")
+ public ReactiveJwtDecoder jwtDecoder() {
+ // 这是一个示例密钥,生产环境中应该使用配置文件中的密钥或从认证服务器获取
+ String secret = "your-256-bit-secret-key-here-must-be-at-least-32-characters-long";
+ SecretKey key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+ return NimbusReactiveJwtDecoder.withSecretKey(key).build();
+ }
+}
diff --git a/src/main/java/com/tuoheng/gateway/filter/JwtPermissionFilter.java b/src/main/java/com/tuoheng/gateway/filter/JwtPermissionFilter.java
new file mode 100644
index 0000000..e980412
--- /dev/null
+++ b/src/main/java/com/tuoheng/gateway/filter/JwtPermissionFilter.java
@@ -0,0 +1,74 @@
+package com.tuoheng.gateway.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+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;
+
+/**
+ * JWT 权限过滤器
+ * 用于验证用户的 JWT token 并检查域名访问权限
+ */
+@Slf4j
+@Component
+public class JwtPermissionFilter implements GlobalFilter, Ordered {
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
+ // 获取请求信息
+ String fullUrl = exchange.getRequest().getURI().toString();
+ String path = exchange.getRequest().getPath().toString();
+ String host = exchange.getRequest().getHeaders().getHost().toString();
+ String hostName = exchange.getRequest().getHeaders().getHost().getHostName();
+ String referer = exchange.getRequest().getHeaders().getFirst("Referer");
+
+ log.info("用户访问的完整URL: {}", fullUrl);
+ log.info("用户访问的path: {}", path);
+ log.info("用户访问的host: {}", host);
+ log.info("用户访问的域名: {}", hostName);
+ log.info("Referer: {}", referer);
+
+ // 从 Spring Security 上下文获取 JWT
+ return ReactiveSecurityContextHolder.getContext()
+ .flatMap(securityContext -> {
+ if (securityContext.getAuthentication() instanceof JwtAuthenticationToken) {
+ JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) securityContext.getAuthentication();
+ Jwt jwt = jwtAuth.getToken();
+
+ // 提取 JWT 中的字段
+ String username = jwt.getClaimAsString("username");
+ String clientId = jwt.getClaimAsString("client_id");
+ String tenantCode = jwt.getClaimAsString("tenant_code");
+ String authorities = jwt.getClaimAsString("clientIds");
+
+ log.info("网关解析到token字段:");
+ log.info("用户名: ", username);
+ log.info("客户端ID: {}", clientId);
+ log.info("租户代码: {}", tenantCode);
+ log.info("用户权限: {}", authorities);
+
+ // 检查该域名是否有权限访问
+ if (authorities == null || !authorities.contains(hostName)) {
+ log.warn("用户 {} 无权访问域名: {}", username, hostName);
+ exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
+ return exchange.getResponse().setComplete();
+ }
+
+ log.info("用户 {} 通过权限验证,允许访问域名: {}", username, hostName);
+ }
+ return chain.filter(exchange);
+ })
+ .switchIfEmpty(chain.filter(exchange));
+ }
+
+ @Override
+ public int getOrder() {
+ return -1; // 优先级高,确保在其他过滤器之前执行
+ }
+}
diff --git a/src/main/java/com/tuoheng/gateway/filter/WebSocketFilter.java b/src/main/java/com/tuoheng/gateway/filter/WebSocketFilter.java
new file mode 100644
index 0000000..dbbeb57
--- /dev/null
+++ b/src/main/java/com/tuoheng/gateway/filter/WebSocketFilter.java
@@ -0,0 +1,59 @@
+package com.tuoheng.gateway.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+/**
+ * WebSocket 过滤器
+ * 用于处理 WebSocket 连接的转发和日志记录
+ */
+@Slf4j
+@Component
+public class WebSocketFilter extends AbstractGatewayFilterFactory {
+
+ public WebSocketFilter() {
+ super(Config.class);
+ }
+
+ @Override
+ public GatewayFilter apply(Config config) {
+ return (exchange, chain) -> {
+ logRequest(exchange);
+
+ return chain.filter(exchange).then(Mono.fromRunnable(() -> {
+ logResponse(exchange);
+ }));
+ };
+ }
+
+ /**
+ * 记录 WebSocket 请求信息
+ */
+ private void logRequest(ServerWebExchange exchange) {
+ HttpHeaders headers = exchange.getRequest().getHeaders();
+ String path = exchange.getRequest().getURI().getPath();
+ String upgrade = headers.getFirst(HttpHeaders.UPGRADE);
+ String connection = headers.getFirst(HttpHeaders.CONNECTION);
+
+ log.info("WebSocket 请求 - Path: {}, Upgrade: {}, Connection: {}", path, upgrade, connection);
+ log.debug("请求头: {}", headers);
+ }
+
+ /**
+ * 记录 WebSocket 响应信息
+ */
+ private void logResponse(ServerWebExchange exchange) {
+ log.info("WebSocket 响应 - Status: {}", exchange.getResponse().getStatusCode());
+ }
+
+ /**
+ * 配置类
+ */
+ public static class Config {
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..c49e34b
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,37 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: thingsboard-gateway-ws
+ cloud:
+ gateway:
+ routes:
+ - id: websocket-route
+ uri: ws://iot.t-aaron.com:18080
+ predicates:
+ - Path=/ws/api/**
+ filters:
+ - StripPrefix=1
+ - name: WebSocketFilter
+
+# 启用 JWT 认证(默认为 false,即不启用)
+# spring.security.oauth2.resourceserver.jwt.enabled: false
+
+# 如果需要启用 JWT 认证,请配置以下内容:
+# spring:
+# security:
+# oauth2:
+# resourceserver:
+# jwt:
+# enabled: true
+# issuer-uri: https://your-auth-server.com
+# # 或者使用 jwk-set-uri:
+# # jwk-set-uri: https://your-auth-server.com/.well-known/jwks.json
+
+logging:
+ level:
+ org.springframework.cloud.gateway: DEBUG
+ org.springframework.web.reactive: DEBUG
+ org.springframework.security: DEBUG
+ com.tuoheng.gateway: DEBUG