老蒋的知识库

  • 首页
  • 文章归档
  • 关于页面

  • 搜索

Spring Boot Gateway转发Http、Websocket,Nacos服务发现,设置路由白名单

发表于 2025-03-05 | 分类于 Java | 0 | 阅读次数 19
  1. Mvn包配置
  2. 配置文件
  3. 代码

Mvn包配置


    <dependencies>
        <!--  只需要网关服务需要  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        
        <!-- SpringCloud Alibaba Nacos 服务注册 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Nacos 配置中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!-- spring boot 2.4之后默认禁用bootstrap.yml配置文件,生效需要引入 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
    </dependencies>

配置文件

Gateway服务配置文件: bootstrap.yml

server:
  port: 8080

spring:
  application:
    name: gateway
  profiles:
    active: @profiles.active@
  cloud:
    nacos:
      discovery: # 服务注册
        server-addr: @nacos.server@
        username: @nacos.username@
        password: @nacos.password@
        namespace: @nacos.namespace-id@
        service: ${spring.application.name}-${spring.profiles.active}
      config: # 配置管理
        server-addr: @nacos.server@
        username: @nacos.username@
        password: @nacos.password@
        namespace: @nacos.namespace-id@
        file-extension: yml
        shared-configs: # 指定额外的配置文件
          - data-id: application.yml
          - data-id: redis.yml
    loadbalancer:
      nacos:
        enabled: true # 开启nacos负载均衡策略
    gateway:
      routes: # 网关路由配置
        - id: auth-service # 路由id,自定义,只要唯一即可
          #uri: http://127.0.0.1:8088 # 路由的目标地址 http就是固定地址
          uri: lb://auth-${spring.profiles.active} # 路由的目标地址 lb就是负载均衡,后面跟Nacos服务发现名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/auth/** # 这个是按照路径匹配,只要以 /auth/ 开头就符合要求
          filters: # 路由过滤器,对请求或者响应做出处理
            - StripPrefix=1 # 去掉路径一条前缀,这里是: /auth
          metadata:
            auth-white-list:
              - /login/**
              - /logout/**
              - /v3/api-docs/**

        - id: chat-service # 路由id,自定义,只要唯一即可
          #uri: http://127.0.0.1:8088 # 路由的目标地址 http就是固定地址
          uri: lb://chat-${spring.profiles.active} # 路由的目标地址 lb就是负载均衡,后面跟Nacos服务发现名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/chat/** # 这个是按照路径匹配,只要以 /chat/ 开头就符合要求
          filters: # 路由过滤器,对请求或者响应做出处理
            - StripPrefix=1 # 去掉路径一条前缀,这里是: /chat
          metadata:
            auth-white-list:
              - /v3/api-docs/**

代码

路由管理器,主要用于获取metadata.auth-white-list 的白名单列表数据,用于跳过鉴权。

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

/***
 * 路由管理
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class RouteMetadataService {

    private final RouteLocator routeLocator;
    private final Map<String, List<String>> routeExcludePaths = new HashMap<>();

    /**
     * 加载所有路由的元数据
     */
    public void loadMetadata() {
        Flux<Route> routes = routeLocator.getRoutes();
        routes.subscribe(route -> {
            // 从元数据获取鉴权白名单,路径在白名单中,无需鉴权
            List<String> authWhiteList = Optional.ofNullable(route.getMetadata().get("auth-white-list"))
                    .map(obj -> {
                        if (obj instanceof List) {
                            // 直接转换为List<String>
                            return (List<String>) obj;
                        } else if (obj instanceof Map) {
                            // 如果是Map,提取所有值组成List
                            Map<?, ?> map = (Map<?, ?>) obj;
                            return map.values().stream()
                                    .filter(String.class::isInstance)
                                    .map(String.class::cast)
                                    .collect(Collectors.toList());
                        } else {
                            log.warn("auth-white-list 类型不匹配: {}", obj.getClass().getName());
                            return List.<String>of();
                        }
                    })
                    .orElse(List.of());  // 默认返回空列表
            routeExcludePaths.put(route.getId(), authWhiteList);
        });
    }

    /**
     * 获取指定路由的白名单路径列表
     */
    public List<String> getauthWhiteList(String routeId) {
        return routeExcludePaths.getOrDefault(routeId, List.of());
    }

    /**
     * 动态路由更新时调用此方法刷新缓存
     */
    @PostConstruct
    public void refreshMetadata() {
        routeExcludePaths.clear();
        loadMetadata();
    }
}

AuthFilter鉴权过滤器


import com.fasterxml.jackson.core.JsonProcessingException;
import com.jagger.ai.services.gateway.service.RouteMetadataService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
// 设置优先级高于WebsocketRoutingFilter的等级(Integer.MAX_VALUE - 1),否则不会过滤Websocket请求,
// 要大于等于2,否则影响 StripPrefix 截取路由获取path(不影响后续转发)
@Order(Integer.MAX_VALUE - 2)
public class AuthFilter implements GlobalFilter {

    private final RouteMetadataService routeMetadataService; // gateway路由元数据
    private final PathMatcher pathMatcher = new AntPathMatcher();  // Spring内置的路径匹配器

    private final ObjectMapper objectMapper; // JSON 序列化工具

    private final RedisService redisService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.debug("全局鉴权开始");
        HttpHeaders headers = exchange.getRequest().getHeaders();

        // 检查是否包含 WebSocket 升级头
        boolean isWebSocket = headers.containsKey(HttpHeaders.UPGRADE) &&
                headers.getConnection().contains("Upgrade") &&
                "websocket".equalsIgnoreCase(headers.getFirst(HttpHeaders.UPGRADE));

        String token;
        if (isWebSocket) {
            // WebSocket 请求处理
            log.debug("检测到 WebSocket 请求");
            token = exchange.getRequest().getQueryParams().getFirst("token");
            log.debug("WebSocket token: {}", token);
        } else {

            log.info("开始鉴权Http");
            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);

            if (route != null) {
                // 过滤白名单路由鉴权
                List<String> authWhiteList = routeMetadataService.getauthWhiteList(route.getId());
                String path = exchange.getRequest().getURI().getPath();
                if (authWhiteList.stream().anyMatch(pattern -> pathMatcher.match(pattern, path))) {
                    log.debug("白名单路由: {}", path);
                    return chain.filter(exchange);
                }
            }

            // 从请求头获取Token
            token = exchange.getRequest().getHeaders().getFirst("Authorization");
        }

        // 验证Token逻辑(如调用Auth服务)
        User user = isValidToken(token);
        if (user == null) {
            return Result.error(HttpStatus.UNAUTHORIZED).toMono(exchange.getResponse(), objectMapper);
        }

        // 鉴权通过,添加用户信息到请求头
        try {
            objectMapper.writeValueAsString(user);
            ServerHttpRequest request = exchange.getRequest()
                    .mutate()
                    .header(HttpHeaderKeys.USER_INFO, objectMapper.writeValueAsString(user)) // 添加完整用户 JSON
                    .build();
            // 构建新的 ServerWebExchange 并继续传递
            ServerWebExchange newExchange = exchange.mutate().request(request).build();
            return chain.filter(newExchange);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

    }

    /***
     * 鉴权判断
     */
    private User isValidToken(String token) {
        log.debug("鉴权token: {}", token);
        return redisService.getCacheObject(RedisPath.USER_TOKEN + token);
    }
}

调用示例

Http调用,获取token对应用户信息

curl --location --request GET '127.0.0.1:8080/auth/get_user_by_token' \
--header 'Authorization: e49d0fd1-5248-4f5a-bc49-ebaf5cb1ce68'

Websocket因为js不能带有Header信息,所以放在请求地址上

// 创建WebSocket连接(替换为你的WebSocket服务器地址)
const socket = new WebSocket('ws://127.0.0.1:8080/chat/im?token=e49d0fd1-5248-4f5a-bc49-ebaf5cb1ce68');
  • 本文作者: jagger
  • 本文链接: /archives/springbootgateway-zhuan-fa-httpwebsocket-she-zhi-bai-ming-dan-lu-you
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
docker hub push 一直提示无权限
Spring Boot 访问Redis
jagger

jagger

66 日志
31 分类
0 标签
Creative Commons
0%
© 2026 jagger
由 Halo 强力驱动