项目自动化发布
集成重点
本文章 主要搭建流程: 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 握手成功");
}
}