gateway网关的简单使用

  • 什么是网关呢?主要的作用是什么

根据名称,网关意思就是网络的一个关卡,用于规范个拦截“过往”的请求。其有三个作用:

1。做请求的路由转发,就比如我们的微服务各个端口都是不一样的,而前端在调用接口时需要配置的是统一的端口,我们就可以使用Gateway网关的路由转发功能,前端配置网关的地址,请求到达网关后会根据后面的断言等规则将请求发送给对应的微服务。 

2。断言,用来判断请求转发给哪个微服务(路径断言),请求参数中的一些校验

3。过滤器。这个是对转发过程上的一些过滤,比如路径前缀的转发跳过,权限过滤,流量控制过滤(结合redis做限流)

 

Gateway三个核心概念

 

路由Routes:当请求到达Gateway网关时,Gateway Handler Mapping会根据URI匹配Routes路由信息,然后将Global Web Handler进行微服务应用程序的接口调用,调用之前会经过一系列的Filter,接口调用成功之后,也会经过一系列的Filter,最终返回给客户端,这就是Gateway网关的一个大致工作原理。

 

断言Predicates:是一些关于请求的判断,满足这些条件时才会进行放行

 

过滤器Filter:过滤器Filter是在调用微服务的过程中,可以在过滤器中增加一些额外的操作,比如:增加header头信息、去掉某些请求参数等等,有两种类型的过滤器:Gateway Filter路由过滤器 和 Global Filter全局过滤器。

 

1.配置以及使用:

现有:注册中心端口9000,common服务端口9002

  • 1.1 依赖引入

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
这里切记不可引入外部的tomcat
gateway默认使用的是webFlux,而Tomcat使用的是netty,项目中不应引入Tomcat-embed-core依赖。
如果引入将会报错:org.springframework.core.io.buffer.DefaultDataBufferFactory cannot be cast to org.springframework.core.io.buffer.NettyDataBufferFactory
  • 1.2 启动类:

@SpringBootApplication
public class SpringGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringGatewayApplication.class, args);
    }
}

  

  • 1.3 网关配置:

这里使用.yml格式
server:
  port: 9003
spring:
  application:
    name: flowershop-gateway
  cloud:
    gateway:
      # 单个规则配置
      routes:
        - id: gateway-flower #路由id,唯一
          uri: http://localhost:9002/ #路由地址,针对哪个服务的路由
          predicates: #断言
            - Path=/** #路径断言,比如这样写 - Path=/api/**,则请求的路径开头时必须包含api的
            # - Header=custom-name, \d+ #Header头断言,表示请求的Header中必须要有custom-name字段,并且value是数字
  • 1.4 测试运行

可以看到,我们的网关服务为9003,去请求9002服务的数据时是正常返回的

2.gateway做动态路由

上面的例子是针对每个服务做固定的路由,而在实际开发中我们的ip比较多可能随时迁移到其他的机器上有可能也会改变,这样我们还需要每改变一次就要动代码。下面我们介绍一种可以直接针对服务实例名就可以添加路由的方式。

这里需要用到服务的注册中心,我们使用的是Eureka,获取到注册中心上所有的实例,然后做动态路由

2.1.引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2.2. 配置注册中心,使用lb(注册中心负载均衡的获取uri)获取uri

eureka:
  client:
    fetch-registry: true # 从 eureka 服务端获取注册信息
    register-with-eureka: true # 将自身注册到 eureka 服务端
    service-url:
      defaultZone: http://localhost:9000/eureka
spring:
  application:
    name: flowershop-gateway
  cloud:
    gateway:
      # 单个规则配置
      routes:
        - id: gateway-flower #路由id,唯一
          uri: lb://flowershop-common #路由地址,针对哪个服务的路由
          predicates: #断言
            - Path=/** #路径断言,比如这样写 - Path=/api/**,则请求的路径开头时必须包含api的
#            - Header=custom-name, \d+ #Header头断言,表示请求的Header中必须要有custom-name字段,并且value是数字
重新clean、compile一下本服务。重新启动。输入本服务的IP端口测试

 

验证成功

3.gateway 自动获取路由配置

由于后期微服务的越来越多,我们并不需要将新增的服务都来配置一遍,因此使用springcloud的自动获取路由功能。

3.1.自动获取路由配置

spring:
  application:
    name: flowershop-gateway
  cloud:
    gateway:
      # 动态服务转发
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
只需要加入这个就可以开启自动获取路由功能了。
我们访问时前缀需要加上需要请求服务的实例名
比如我们上述一直请求的服务的实例名为:flowershop-common
我们进行访问如下:

 验证成功,那么此时如果我们还需要对其中一些服务进行特殊处理,比如限制其中一个服务使用断言,那么就需要将此服务添加到配置中,如下

spring:
  application:
    name: flowershop-gateway
  cloud:
    gateway:
      # 动态服务转发
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      # 单个规则配置
      routes:
        - id: gateway-flower #路由id,唯一
          uri: lb://flowershop-common #路由地址,针对哪个服务的路由
          predicates: #断言
            - Path=/** #路径断言,比如这样写 - Path=/api/**,则请求的路径开头时必须包含api的
此时的动态路由跟单个规则的配置都会生效。

4.做过滤器的使用

在单服务中我们可以使用加密token结合过滤器拦截器做一些权限拦截,操作日志记录。但是如果使用了微服务,且需要一个整体而不是各自微服务的权限拦截或者日志记录的话就需要用到我们的gateway的过滤器。下面是一个简单的示例:

每次请求需要校验token的合法性,不合法的请求需要进行拦截,并且有个别一些请求不需要校验直接放行

先进性依赖剔除修改,不然启动会报错

 

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <exclusions>
        <!-- gateway默认使用的是webFlux,而Tomcat使用的是netty,项目中不应引入Tomcat-embed-core依赖。-->
                <exclusion>
                    <groupId>org.apache.tomcat.embed</groupId>
                    <artifactId>tomcat-embed-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

 

这里我们使用 GlobalFilter做全局过滤器

/**
 * @Description: 全局过滤器
 * @Author: zhl
 * @Date: 2024/7/3 15:23
 */
@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {

    @Autowired
    RedisService redisService;
    @Value("${token.timeout}")
    private Long timeout;

    private final List<String> excludePatterns = new ArrayList<>();

    private static final String TOKEN_KEY = "token";

    String[] swaggerExcludes = new String[]{"/swagger-ui.html", "/swagger-ui.html#/", "/swagger-resources/**", "/webjars/**",
            "/swagger-ui.html#!/**", "/doc.html", "/doc.html#", "/doc.html#/**"};
    String[] pathExcludes = new String[]{"/admin/login", "/common/uploadFile", "/department/getUserByCode", "/admin/getToken", "/user/getUserList",
            "/verify/getVerifyCode", "/verify/examineVcode","/index/mobile/list","/workpermit/uploadFile"};

    @PostConstruct
    public void init() {
        excludePathPatterns(swaggerExcludes);
        excludePathPatterns(pathExcludes);
    }


    /**
     * 实现全局的过滤器
     *
     * @param exchange 封装了request和response方法
     * @param chain    过滤器链
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1 获得请求路径
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        //符合请求路径的放行
        if (pathContains(path)) {
            return dataPass(exchange, chain);
        }
        // 2 处理异常 拦截
        List<String> tokens = request.getHeaders().get(TOKEN_KEY);
        if (CollectionUtil.isEmpty(tokens)) {
            throw new BusinessException(ResultMsg.TOKEN_ERROR);
        }
        // 传入的token
        String token = tokens.get(0);
        if (StringUtils.isEmpty(token)) {
            throw new BusinessException(ResultMsg.TOKEN_ERROR);
        }
        //解码token获取username
        String username = MyDataUtils.getUserByToken(token);
        Object redisToken = redisService.get(RedisConstantsUtils.LOGIN_MAP.getCode(), username);
        if (ObjectUtils.isNotEmpty(redisToken) && redisToken.equals(token)) {//从redis中获取的token一致,放行
            redisService.set(RedisConstantsUtils.LOGIN_MAP.getCode(), username, token, timeout);
            return dataPass(exchange, chain);
        } else {//不一致,拦截
            throw new BusinessException(ResultMsg.TOKEN_ERROR);
        }
//            ServerHttpResponse response = exchange.getResponse();
//            response.setStatusCode(HttpStatus.UNAUTHORIZED);
//            response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
//            DataBuffer wrap = response.bufferFactory().wrap(Response.fail(ResultMsg.USER_UNVERIFIED).toString().getBytes(StandardCharsets.UTF_8));
//            return exchange.getResponse().writeWith(Flux.just(wrap));
    }

    //放行
    private Mono<Void> dataPass(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            System.out.println("放行");
        }));
    }

    /**
     * @param path 请求路径
     * @return f:不包含 t:包含
     */
    private boolean pathContains(String path) {
        boolean flg;
        flg = this.excludePatterns.stream().anyMatch(path::contains);
        return flg;
    }

    private void excludePathPatterns(String... patterns) {
        this.excludePatterns.addAll(Arrays.asList(patterns));
    }

    /**
     * 排序
     * @return 排序
     */
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

gateway自定义异常捕获:

GlobalFilter中的 exchange 封装了request和response方法,没有像拦截器一样的放行,因此需要给出返回状态去标识拦截的请求,这里使用了抛出自定义异常的方式

需要注意的是自定义的异常捕获处理如果是使用 @ExceptionHandler 是无法捕获的

需要去继承重写 DefaultErrorWebExceptionHandler里面的方法来自定义异常返回

public class JsonErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {

    public JsonErrorWebExceptionHandler(ErrorAttributes errorAttributes,
                                        ResourceProperties resourceProperties,
                                        ErrorProperties errorProperties,
                                        ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        // 这里其实可以根据异常类型进行定制化逻辑
        Throwable error = super.getError(request);
        Map<String, Object> errorAttributes = new LinkedHashMap<>(8);
        if (error instanceof BusinessException) {
            errorAttributes.put("body",null);
            errorAttributes.put("code", ((BusinessException) error).getCode());
            errorAttributes.put("exceptionDescription",error.getMessage());
            errorAttributes.put("message", ((BusinessException) error).getDescription());
        }
        return errorAttributes;
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    @Override
    protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
        // 这里其实可以根据errorAttributes里面的属性定制HTTP响应码
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

返回字段就与自定义的返回字段对应好了

body
code
message
exceptionDescription

 

posted @ 2023-06-09 10:15  城北左少爷  阅读(215)  评论(0编辑  收藏  举报