springcloud网关集成websocket框架

项目自动化发布

集成重点

本文章 主要搭建流程: vue(前端)+ nginx(代理服务器) + spring cloud gateway网关 + (各个微服务应用) + websocket(全双工通讯工具)

网关配置 gateway-yml 配置websocket升级协议

复制代码
        # WebSocket 路由(优先级最高)
        - id: ruoyi-app-websocket
          uri: lb://ruoyi-app
          predicates:
            - Path=/ws/log/**
          filters:
            # 关键:转发 WebSocket 升级协议头
            # 保留原始 Host 头
            - name: PreserveHostHeader
            # - name: AddResponseHeader
              # args:
                # name: Access-Control-Allow-Origin
                # 与后端跨域一致
                # value: "http://127.0.0.1:80" 
            - name: AddResponseHeader
              args:
                name: Access-Control-Allow-Credentials
                value: "true"
            # 路径重写(若后端路径是 /ws/log,无需修改)
            - RewritePath=/ws/log/(?<segment>.*), /ws/log/$\{segment}
          metadata:
            # 标记为 WebSocket 路由,网关会自动处理 Upgrade 头
            response-timeout: 3000000
            connect-timeout: 10000          
        # 部署服务普通 HTTP 路由(已有则无需修改)
        - id: ruoyi-app
          uri: lb://ruoyi-app
          predicates:
            - Path=/app/**
          filters:
            # 移除 /deploy 前缀(根据实际配置调整)
            - StripPrefix=1 

nginx配置重点是配置/ws/ websocket代理

复制代码
# 前端项目 Nginx 配置
server {
    # 1. 访问端口(自定义,如 80、8080,需与防火墙放行端口一致)
    listen       9999;
    # 服务器 IP 或域名(本地测试填服务器 IP,线上填域名,如 www.xxx.com)
    server_name  xxx.xxx.xxx.xxx;

    # 2. 前端项目根路径(即 dist 目录的绝对路径,关键!)
    root     /home/data/auotobuilding/admin-ui/dist;
    # 字符编码(避免中文乱码)
    charset utf-8;

    # 3. 跨域配置(前端访问后端微服务时需要,如WebSocket、API请求,按需开启)
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';


    location /prod-api/ {
            proxy_connect_timeout    600;
            proxy_read_timeout       600;
            proxy_send_timeout       600;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://127.0.0.1:8888/; #转发到本机项目端口
    }
    # -------------- WebSocket 代理配置(新增核心部分)--------------
    # 匹配 WebSocket 路径(你的路径是 /ws/log,用 /ws/ 匹配所有以 /ws/ 开头的请求)
    location /ws/ {
        # 1. 转发到网关地址(关键!Nginx 先转发到网关,再由网关转发到 System 服务)
        proxy_pass http://127.0.0.1:8888; # 网关的 IP:端口(若依网关默认 8888)

        # 2. 核心:转发 WebSocket 升级头(必须配置,否则握手失败)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade"; # 注意:双引号不能少

        # 3. 转发客户端真实 IP 和 Host(便于后端日志排查)
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;

        # 4. 禁用缓存(避免握手请求被缓存)
        proxy_cache off;
        proxy_buffering off;

        # 5. 长连接超时设置(关键!默认 60s,需调大,与网关/后端一致)
        proxy_read_timeout 300s; # 5分钟超时(建议与网关的 response-timeout 一致)
        proxy_send_timeout 300s;

        # 6. 跨域配置(与前端一致,避免跨域拦截)
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
        add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    }


    # 配置默认首页(前端打包后的首页,一般是 index.html)
    location / {
        # 解决单页应用(Vue/React)路由刷新 404 问题(关键!)
        try_files $uri $uri/ /index.html;
        # 默认首页
        index  index.html index.htm;
    }

    # 错误页面配置(可选,美化错误页面)
    error_page  404              /404.html;
    location = /404.html {
        root /data/frontend/dist;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html; # Nginx 默认错误页面路径
    }
}

前端集成

vue 复制代码
// websocket会使用到
import { getToken } from '@/utils/auth' // 若依内置:获取 JWT Token
import{ formatDate} from "@/utils"
import { Message } from 'element-ui'


/**
* 初始化 WebSocket 连接
*/
initWebSocket() {
const token = getToken()
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const gatewayHost = process.env.VUE_APP_BASE_API.replace('http://', '').replace('https://', '') // 网关地址
// WebSocket 连接地址:ws://网关地址/ws/log?taskId=xxx&token=xxx
const url = `${protocol}//127.0.0.1:8888/ws/log?taskId=${this.taskId}&token=${token}`
console.log('初始化 WebSocket 链接', url)
this.webSocket = new WebSocket(url)



# 若依管理系统/测试环境
VUE_APP_BASE_API = 'http://xxx.xxx.xxx.xxx:9999/prod-api/'

环境配置文件 .env.staging

复制代码
# 页面标题
VUE_APP_TITLE = 若依管理系统

BABEL_ENV = staging

NODE_ENV = staging

# 测试环境配置
ENV = 'staging'

# 若依管理系统/测试环境 重点是这里, 需要配置nginx代理地址
VUE_APP_BASE_API = 'http://xxx.xxx.xxx.xxx:9999/prod-api/'

后端集成

集成websocket pom.xml

xml 复制代码
        <!-- WebSocket 核心依赖(Spring Boot 内置) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

AuthFilter.java

java 复制代码
    /**
     * 获取请求token
     */
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(SecurityConstants.AUTHORIZATION_HEADER);
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
            token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
        }
        
        // 重点,通过request的方式获取拼接地址栏的token ws是从地址栏发送的token这里需要特殊处理
        if (StringUtils.isEmpty(token)) {
            try {
                token = request.getQueryParams().get("token").get(0);
            } catch (Exception e) {
                return null;
            }
        }
        return token;
    }

集成配置类

java 复制代码
@Configuration
@RefreshScope
@Data
@ConfigurationProperties(prefix = "websocket.allowed.origins")
public class WebSocketAllowedOriginProperties {
    private List<String> allowedOrigins = new ArrayList<>();

}

/**
 * WebSocket 配置(适配 Spring Cloud 微服务+若依 JWT)
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private WebSocketAllowedOriginProperties webSocketAllowedOriginProperties;

    /**
     * 日志推送处理器(核心)
     */
    @Bean
    public LogWebSocketHandler logWebSocketHandler() {
        return new LogWebSocketHandler();
    }

    /**
     * 握手拦截器(校验 JWT Token + taskId)
     */
    @Bean
    public HandshakeInterceptor webSocketHandshakeInterceptor() {
        return new WebSocketHandshakeInterceptor();
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        List<String> allowedOrigins = webSocketAllowedOriginProperties.getAllowedOrigins();
        // 前端连接路径:ws://网关地址/ws/log?taskId=xxx&token=xxx
        registry.addHandler(logWebSocketHandler(), "/ws/log")
                .addInterceptors(webSocketHandshakeInterceptor())
                // 跨域配置:允许前端地址(前后端分离核心)  这里如果配置不对, 前端会报错, 编码1002异常
                .setAllowedOrigins(allowedOrigins.toArray(new String[0]))
        ;
    }

}

日志推送处理器

java 复制代码
/**
 * WebSocket 日志推送处理器(线程安全+会话管理)
 */
public class LogWebSocketHandler extends TextWebSocketHandler {

    /**
     * 任务ID -> WebSocket会话映射(ConcurrentHashMap 保证线程安全)
     */
    private static final Map<String, WebSocketSession> TASK_SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * 连接建立成功:绑定 taskId 与会话
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String taskId = (String) session.getAttributes().get("taskId");
        if (taskId != null && !TASK_SESSION_MAP.containsKey(taskId)) {
            TASK_SESSION_MAP.put(taskId, session);
            // 推送连接成功日志
            sendLog(taskId, "✅ WebSocket 连接成功,开始监控 JGit+JSch 日志...");
        }
    }

    /**
     * 连接关闭:移除会话映射
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String taskId = (String) session.getAttributes().get("taskId");
        if (taskId != null) {
            TASK_SESSION_MAP.remove(taskId);
        }
    }

    /**
     * 静态方法:推送日志到前端(供 JGit/JSch 工具类调用)
     * @param taskId 任务唯一ID
     * @param log 日志内容(单行)
     */
    public static void sendLog(String taskId, String log) {
        if (taskId == null || log == null || log.trim().isEmpty()) {
            return;
        }
        WebSocketSession session = TASK_SESSION_MAP.get(taskId);
        if (session == null || !session.isOpen()) {
            return;
        }
        try {
            // 发送文本日志(UTF-8 编码)
            session.sendMessage(new TextMessage(log));
        } catch (Exception e) {
            // 发送失败:移除无效会话
            TASK_SESSION_MAP.remove(taskId);
        }
    }

    /**
     * 任务结束:关闭会话(主动释放资源)
     */
    public static void closeSession(String taskId) {
        WebSocketSession session = TASK_SESSION_MAP.get(taskId);
        if (session != null && session.isOpen()) {
            try {
                sendLog(taskId, "✅ 任务执行完成,WebSocket 连接即将关闭");
                session.close();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                TASK_SESSION_MAP.remove(taskId);
            }
        }
    }
}

websocket握手处理

java 复制代码
/**
 * WebSocket 握手拦截器(若依 JWT 权限校验)
 */
@Slf4j
public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {

    /**
     * 握手前校验(核心:验证 Token + taskId)
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        // 1. 转换 HTTP 请求,获取参数(taskId:任务唯一标识,token:若依 JWT Token)
        ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
        HttpServletRequest httpRequest = servletRequest.getServletRequest();
        String taskId = httpRequest.getParameter("taskId");
        String token = httpRequest.getParameter("token");

        // 2. 校验必填参数
        if (taskId == null || taskId.trim().isEmpty()) {
            response.setStatusCode(org.springframework.http.HttpStatus.BAD_REQUEST);
            return false;
        }
        if (token == null || token.trim().isEmpty()) {
            response.setStatusCode(org.springframework.http.HttpStatus.UNAUTHORIZED);
            return false;
        }

        // 3. 校验若依 JWT Token 有效性(复用若依内置工具类)
        Claims claims = JwtUtils.parseToken(token);
        if (claims == null) {
            response.setStatusCode(org.springframework.http.HttpStatus.UNAUTHORIZED);
            return false;
        }
        // 4. 验证用户是否登录(可选:若依 SecurityUtils 校验)
        if (SecurityUtils.getUserId() == null) {
            response.setStatusCode(org.springframework.http.HttpStatus.UNAUTHORIZED);
            return false;
        }

        // 5. 存储 taskId 到会话属性(后续日志推送关联)
        attributes.put("taskId", taskId);
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        // 握手后操作(无需处理)
        log.info("WebSocket 握手成功");
    }
}
相关推荐
Qoitech 中国2 小时前
Otii 应用场景系列:使用 Otii Arc和Otii Ace进行差分测量
嵌入式硬件·物联网·自动化·集成测试·智能硬件
¿Quién soy yo3 小时前
Postman+Newman接口自动化测试:一键生成精美HTML测试报告完整教程
测试工具·自动化·html·postman·持续集成
宇钶宇夕3 小时前
魏德米勒 UR20-FBC-PN-IRT-V2 从站全解析:产品特性、模块详情、接线图与地址配置指南(地址修改部分)
运维·自动化
不叫猫先生4 小时前
基于AI代理浏览器的自动化数据爬取实践
人工智能·爬虫·自动化
MadPrinter4 小时前
FindQC 实战 (一):基于 SerpApi 的电商高质量图片自动化筛选算法初探
运维·python·算法·自动化
胡萝卜的兔4 小时前
ci/cd自动化部署
运维·ci/cd·自动化
明达智控技术4 小时前
MR30分布式IO:破解汽车焊接产线控制难题
物联网·自动化
明达智控技术4 小时前
MR30分布式IO赋能注塑机智能化升级
分布式·物联网·自动化
萧鼎4 小时前
告别 PR!用 Python + MoviePy 自动化剪辑视频
python·自动化·音视频