Files
hzhub/docs/gateway-migration-plan.md
大壮 c2513849b4 feat: 添加ERP服务和系统服务,完善员工门户功能
## 新增服务模块

### 1. ERP服务 (hzhub-erp)
- 新增独立的ERP数据适配服务
- 支持SQL Server 2008 R2数据源
- 提供动态API配置管理系统
- 包含客户管理、销售数据等业务接口

### 2. 系统服务 (hzhub-system)
- 新增独立的系统管理服务
- 用户、角色、权限、部门、菜单管理
- 租户管理、操作日志、在线用户监控
- 工作流引擎(warm-flow)集成
- 企业微信审批同步功能

### 3. API网关 (hzhub-gateway)
- 新增Spring Cloud Gateway网关服务
- JWT认证、路由分发、限流熔断
- XSS防护、请求日志记录
- 统一入口端口8080

## 后台管理功能增强

### ERP动态API管理
- 新增动态API配置管理界面
- API测试、文档预览、统计监控
- 错误日志查看、缓存管理
- 从数据库表自动导入API配置

### 系统管理增强
- 企业微信配置管理
- 企业微信审批同步配置
- 部门和用户管理优化

## 员工门户功能完善

### 业务页面
- 审批中心:工作流审批、待办任务
- CRM管理:客户关系管理
- 经销商管理:经销商数据展示
- 供应链管理:采购、库存、销售
- BI报表:数据可视化分析
- ERP数据探索:SQL Server数据查询

### 个人中心
- 基本设置:个人信息管理
- 安全设置:密码修改、登录日志
- 锁屏功能:自动锁屏、手动锁屏

### 其他功能
- 标签页管理:多标签页导航
- 页面缓存:keepAlive缓存机制
- 会话超时:自动检测并提示

## 经销商门户

### 页面路由
- 新增经销商管理页面路由
- AI聊天界面完善

## 文档更新

- ERP API数据库初始化指南
- ERP API前端完整实现文档
- ERP API测试和验证指南
- Gateway路由迁移计划
- 项目配置文档更新

## 部署脚本

- 统一启动/停止/重启脚本
- Docker Compose配置优化
- Nginx配置文件更新

## 技术栈

- 后端: Spring Boot 3.5.8, Java 17
- 前端: Vue 3, TypeScript, Element Plus, Vben Admin
- 工作流: warm-flow 1.8.2
- 网关: Spring Cloud Gateway
- 数据库: MySQL 8.0, SQL Server 2008 R2
- 缓存: Redis 7
- 向量库: Weaviate 1.25.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 08:00:19 +00:00

16 KiB
Raw Blame History

HZHub-Gateway 迁移方案

一、现状分析

当前架构(去中心化)

前端门户 → Nginx/Vite代理 → 直连后端服务
  admin    →  localhost:6039  (hzhub-ai)
  employee →  localhost:6039  (hzhub-ai) + localhost:8082 (hzhub-erp)
  dealer   →  localhost:6039  (hzhub-ai) + localhost:8082 (hzhub-erp)

hzhub-gateway 已存在但未部署仅有基础路由配置3条路由 + CORS

hzhub-ai 中可迁移的网关级功能

功能 位置 是否适合迁移
Sa-Token JWT 验证 SaTokenConfig + SecurityConfig 是 — 全局统一鉴权
XSS 过滤 XssFilter (javax.servlet Filter) 是 — WebFilter 可实现
请求/响应加密 CryptoFilter (RSA 加解密) ⚠️ 部分 — 需适配 WebFlux
接口限流 @RateLimiter + Redisson 是 — 可用 Redis + RequestRateLimiter
幂等性校验 @Idempotent 否 — 业务级,保留后端
SSE 流式响应 ChatController 否 — 业务级,保留后端
WebSocket 握手鉴权 PlusWebSocketInterceptor 否 — 协议不同,保留后端
数据权限拦截 MyBatis 拦截器 否 — ORM 层,保留后端

核心挑战

  1. Servlet vs WebFluxGateway 基于 Spring WebFlux响应式hzhub-ai 的 Filter 是 Servlet 规范,不能直接复用,需改写为 WebFilter
  2. JWT 解析Gateway 只需验证 JWT 合法性并透传用户信息,无需完整 Sa-Token 登录态
  3. 加密适配CryptoFilter 依赖 ServletInputStream,需改写为 ServerHttpRequestDecorator

二、目标架构

前端门户 → Gateway(:8080) → 后端服务
                      /ai/**     →  hzhub-ai(:8081)
                      /erp/**    →  hzhub-erp(:8082)
                      /system/** →  hzhub-ai(:8081)

Gateway 承担:

  • 统一鉴权:验证 JWT Token解析用户信息注入请求头
  • XSS 防护:对 POST/PUT 请求体进行 XSS 清洗
  • 接口限流:基于 Redis 的令牌桶限流
  • 请求/响应加密:按需解密请求、加密响应
  • 路由转发:按路径分发到对应服务
  • 跨域处理:全局 CORS

后端服务hzhub-ai / hzhub-erp承担

  • 业务逻辑CRUD、AI 对话、工作流等
  • 数据权限MyBatis 层的多租户数据隔离
  • 幂等性:防重复提交
  • SSE/WebSocket:流式推送

服务间信任模型

外部请求 → Gateway → 后端服务
              │          │
           验证JWT     信任Gateway
                      透传的用户头
  • Gateway 验证 JWT 通过后,将用户信息注入请求头:X-User-IdX-User-NameX-Client-Id
  • 后端服务在 SecurityConfig 中配置:来自 Gateway 的请求跳过 JWT 验证
  • 判断依据:检查请求头 X-Gateway-Verified: true(或共享内网 IP 白名单)
  • 开发环境:保留直连后端的鉴权能力(双重模式)

三、实施步骤

Phase 1: Gateway 基础增强(认证 + 路由)

目标:让 Gateway 能正常转发请求并验证 JWT

3.1 修改 pom.xml

<!-- 已有spring-cloud-starter-gateway, spring-cloud-starter-loadbalancer -->

<!-- 新增 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
    <version>1.39.0</version>
</dependency>
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-jwt</artifactId>
    <version>1.39.0</version>
</dependency>

3.2 JWT 认证全局过滤器

// src/main/java/org/hzhub/gateway/filter/AuthGlobalFilter.java
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final SaTokenConfig saTokenConfig;

    // 放行路径
    private static final List<String> WHITE_LIST = List.of(
        "/erp/test/", "/erp/customer/", "/actuator/"
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 白名单放行
        if (isWhiteList(path)) {
            return chain.filter(exchange);
        }

        // 从 Header 获取 Token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            return unauthorized(exchange, "未登录或登录已过期");
        }

        // 验证 JWT使用 sa-token-jwt
        String jwtToken = token.substring(7);
        try {
            SaTokenInfo info = SaManager.getTokenInfo(jwtToken);
            if (info == null || !info.isLogin()) {
                return unauthorized(exchange, "登录已过期");
            }
            // 透传用户信息到后端
            ServerHttpRequest mutated = exchange.getRequest().mutate()
                .header("X-User-Id", info.getLoginId())
                .header("X-Client-Id", exchange.getRequest().getHeaders().getFirst("ClientID"))
                .header("X-Gateway-Verified", "true")
                .build();
            return chain.filter(exchange.mutate().request(mutated).build());
        } catch (Exception e) {
            return unauthorized(exchange, "Token 无效");
        }
    }

    private boolean isWhiteList(String path) {
        return WHITE_LIST.stream().anyMatch(path::startsWith);
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange, String msg) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        byte[] bytes = ("{\"code\":401,\"msg\":\"" + msg + "\"}").getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
        return exchange.getResponse().writeWith(Mono.just(buffer));
    }

    @Override
    public int getOrder() { return -100; } // 最高优先级
}

3.3 更新 application.yml

server:
  port: 8080

spring:
  application:
    name: hzhub-gateway

  # Redis 配置(限流 + Token 验证共享)
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}

  cloud:
    gateway:
      routes:
        - id: hzhub-ai
          uri: http://${AI_HOST:localhost}:${AI_PORT:8081}
          predicates:
            - Path=/ai/**,/system/**
          filters:
            - StripPrefix=1

        - id: hzhub-erp
          uri: http://${ERP_HOST:localhost}:${ERP_PORT:8082}
          predicates:
            - Path=/erp/**
          filters:
            - StripPrefix=1

      globalcors:
        cors-configurations:
          '[/**]':
            allowedOriginPatterns: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
            allowCredentials: true
            maxAge: 3600

# Sa-TokenJWT 验证用,与后端服务共享 secret
sa-token:
  jwt-secret-key: ${JWT_SECRET:abcdefghijklmnopqrstuvwxyz}
  token-name: Authorization

# 日志
logging:
  level:
    org.hzhub.gateway: debug

Phase 2: XSS 过滤 + 限流

3.4 XSS 全局过滤器WebFlux 版)

// src/main/java/org/hzhub/gateway/filter/XssGlobalFilter.java
@Component
public class XssGlobalFilter implements GlobalFilter, Ordered {

    private static final List<String> EXCLUDE_PATHS = List.of("/ai/upload");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        HttpMethod method = exchange.getRequest().getMethod();

        // GET/DELETE 不处理;排除白名单
        if (method == HttpMethod.GET || method == HttpMethod.DELETE
            || EXCLUDE_PATHS.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        // 包装请求,对 Body 进行 XSS 清洗
        ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public Flux<DataBuffer> getBody() {
                return super.getBody().map(buffer -> {
                    byte[] bytes = new byte[buffer.readableByteCount()];
                    buffer.read(bytes);
                    String body = new String(bytes, StandardCharsets.UTF_8);
                    String cleaned = cleanXss(body);
                    byte[] cleanedBytes = cleaned.getBytes(StandardCharsets.UTF_8);
                    DataBufferFactory factory = buffer.factory();
                    DataBuffer newBuffer = factory.allocateBuffer(cleanedBytes.length);
                    newBuffer.write(cleanedBytes);
                    return newBuffer;
                });
            }
        };

        return chain.filter(exchange.mutate().request(decorated).build());
    }

    private String cleanXss(String value) {
        if (value == null) return null;
        return value
            .replaceAll("<script>", "&lt;script&gt;")
            .replaceAll("</script>", "&lt;/script&gt;")
            .replaceAll("javascript:", "")
            .replaceAll("on\\w+\\s*=", "");
    }

    @Override
    public int getOrder() { return -50; }
}

3.5 限流配置(基于 Redis

# 在 application.yml 的 gateway 配置中添加
spring.cloud.gateway.default-filters:
  - name: RequestRateLimiter
    args:
      redis-rate-limiter.replenishRate: 10    # 每秒补充令牌数
      redis-rate-limiter.burstCapacity: 20    # 令牌桶容量
      key-resolver: "#{@ipKeyResolver}"       # 按 IP 限流
// src/main/java/org/hzhub/gateway/config/RateLimiterConfig.java
@Configuration
public class RateLimiterConfig {

    /**
     * 按 IP 地址限流
     */
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> {
            String ip = exchange.getRequest().getRemoteAddress() != null
                ? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
                : "unknown";
            return Mono.just("gateway:ratelimit:" + ip);
        };
    }

    /**
     * 按路径+IP 限流(更细粒度)
     */
    @Bean
    public KeyResolver pathIpKeyResolver() {
        return exchange -> {
            String path = exchange.getRequest().getURI().getPath();
            String ip = exchange.getRequest().getRemoteAddress() != null
                ? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
                : "unknown";
            return Mono.just("gateway:ratelimit:" + path + ":" + ip);
        };
    }
}

Phase 3: 后端服务适配

3.6 hzhub-ai 的 SecurityConfig 改造

在 hzhub-ai 的 SecurityConfig 中新增:当请求来自 Gateway 时,跳过 JWT 验证。

// hzhub-ai 的 SecurityConfig.java 中修改
private boolean isFromGateway(ServerHttpRequest request) {
    String verified = request.getHeaders().getFirst("X-Gateway-Verified");
    return "true".equals(verified);
}

或者更简单:在 Sa-Token 拦截器中添加检查:

// 如果 Gateway 已验证,直接放行
if ("true".equals(request.getHeader("X-Gateway-Verified"))) {
    return true;
}

3.7 hzhub-erp 同理

在 hzhub-erp 的 SecurityConfig.java 中添加同样的 Gateway 信任逻辑。


Phase 4: 前端路由切换

3.8 hzhub-admin管理后台

修改 .env.development

VITE_GLOB_API_URL=http://localhost:8080

修改 vite.config.ts 代理:

'/api': {
  target: 'http://localhost:8080',  // 改为 Gateway
  changeOrigin: true,
  rewrite: path => path.replace(/^\/api/, ''),
}

3.9 hzhub-portal-employee员工门户

修改 .env.development

VITE_API_URL=http://localhost:8080

request.ts 中已自动使用此 baseURL无需改动。

注意:原来直连 ERP 的请求(/erp/customer/**)也需经过 GatewayGateway 路由已覆盖 /erp/**

3.10 hzhub-portal-dealer经销商门户

同 employee统一指向 Gateway。


Phase 5: Docker Compose 集成

3.11 新增 hzhub-gateway 服务

# docker-compose.yml 新增
hzhub-gateway:
  build:
    context: ../hzhub-gateway
    dockerfile: Dockerfile
  container_name: hzhub-gateway
  ports:
    - "8080:8080"
  environment:
    SPRING_PROFILES_ACTIVE: prod
    REDIS_HOST: hzhub-redis
    REDIS_PORT: 6379
    AI_HOST: hzhub-ai
    AI_PORT: 6039
    ERP_HOST: hzhub-erp
    ERP_PORT: 8082
    JWT_SECRET: ${JWT_SECRET:-abcdefghijklmnopqrstuvwxyz}
  depends_on:
    redis:
      condition: service_healthy
    hzhub-ai:
      condition: service_started
    hzhub-erp:
      condition: service_started
  networks:
    - hzhub-network

3.12 前端 Nginx 代理改为 Gateway

# hzhub-portal-employee 的 Nginx 配置
# 原来: proxy_pass http://hzhub-ai:6039;
# 改为:
location /api/ {
    proxy_pass http://hzhub-gateway:8080;
}
location /erp/ {
    proxy_pass http://hzhub-gateway:8080;
}

3.13 后端服务关闭外部端口暴露

# docker-compose.yml 修改
hzhub-ai:
  # ports:
  #   - "6039:6039"   # 不再对外暴露,仅内网访问
hzhub-erp:
  # ports:
  #   - "8082:8082"   # 不再对外暴露,仅内网访问

四、文件变更清单

新增文件

文件 说明
hzhub-gateway/src/main/java/org/hzhub/gateway/filter/AuthGlobalFilter.java JWT 认证过滤器
hzhub-gateway/src/main/java/org/hzhub/gateway/filter/XssGlobalFilter.java XSS 过滤
hzhub-gateway/src/main/java/org/hzhub/gateway/config/RateLimiterConfig.java 限流 Key 解析器
hzhub-gateway/Dockerfile Gateway 容器构建

修改文件

文件 变更
hzhub-gateway/pom.xml 新增 sa-token-reactor、redis-reactive 依赖
hzhub-gateway/src/main/resources/application.yml 重写路由、Redis、Sa-Token 配置
hzhub-gateway/src/main/java/org/hzhub/HzhubGatewayApplication.java 无需改动
hzhub-ai/.../SecurityConfig.java 添加 Gateway 信任逻辑
hzhub-erp/.../SecurityConfig.java 添加 Gateway 信任逻辑
hzhub-admin/apps/web-antd/.env.development API 地址改为 Gateway
hzhub-admin/apps/web-antd/vite.config.ts 代理目标改为 Gateway
hzhub-portal-employee/.env.development VITE_API_URL 指向 Gateway
hzhub-portal-dealer/.env.development 新增 API URL
hzhub-deploy/docker-compose.yml 新增 Gateway 服务,调整网络

五、分阶段验证

Phase 1 验证

# 1. 启动 Gateway
cd hzhub-gateway && mvn spring-boot:run

# 2. 测试路由转发
curl http://localhost:8080/erp/test/connection
# 应返回 SQL Server 连接信息

# 3. 测试鉴权(未登录)
curl http://localhost:8080/ai/chat/message
# 应返回 401

# 4. 测试鉴权(带 Token
curl -H "Authorization: Bearer <token>" http://localhost:8080/ai/chat/message
# 应正常返回结果

Phase 2 验证

# 测试 XSS 过滤
curl -X POST http://localhost:8080/ai/xxx \
  -H "Content-Type: application/json" \
  -d '{"content": "<script>alert(1)</script>"}'
# 请求体应被清洗

# 测试限流(快速连续请求)
for i in {1..30}; do curl -s http://localhost:8080/ai/xxx; done
# 超过阈值应返回 429

Phase 3 验证

# 前端登录后,所有请求走 Gateway
# 确认 AI 接口、ERP 接口均正常工作
# 确认 Token 过期后自动跳转登录页

六、风险与注意事项

风险 应对
Sa-Token JWT 验证方式与后端不一致 统一使用 StpLogicJwtForSimple,共享 jwt-secret-key
Gateway 成为单点故障 后续可部署多实例 + Nginx 负载均衡
SSE 流式响应经过 Gateway 可能超时 需在 Gateway 配置路由超时时间(默认无超时)
CORS 配置冲突 Gateway 统一处理 CORS后端服务关闭 CORS
开发环境直连 vs 生产走 Gateway 后端服务保留双重模式:有 X-Gateway-Verified 跳过验证,否则自行验证

七、后续优化方向

  1. 服务注册与发现:引入 Nacos替代硬编码的服务地址
  2. 熔断降级:引入 Resilience4j对下游服务做熔断保护
  3. 链路追踪:引入 SkyWalking 或 Micrometer Trace
  4. 灰度发布:基于 Header 的蓝绿部署路由
  5. API 文档聚合Gateway 聚合 Swagger/Knife4j 文档