SpringCloud—Gateway

网关

为什么需要网关

在整个微服务架构中,包含了许多不同的应用,比如下图所示的mst-user-service、mst-good-service和mst-order-service是三个独立的应用,这些服务都需要对客户端的请求的进行鉴权(Authentication),最简单粗暴的方法就是像图中一样,为每个微服务应用都实现一套用于校验的过滤器或拦截器。

但这种做法相当于做了很多不必要的重复工作,有了网关之后,可以在网关服务中做统一鉴权操作,如下图所示

网关的基本功能

  • 网关核心功能是路由转发,因此不要有耗时操作在网关上处理,让请求快速转发到后端服务上。
  • 网关还能做统一的熔断、限流、认证、日志监控等。

关于Spring Cloud Gateway

Spring Cloud Gateway是由spring官方基于Spring5.0、Spring Boot2.0、Project Reactor等技术开发的网关,使用非阻塞API,它支持Websockets,目的是代替原先版本中的Spring Cloud Netfilx Zuul,目前Netfilx已经开源了Zuul2.0,但Spring 没有考虑集成,而是推出了自己开发的Spring Cloud GateWay。Spring Cloud Gateway使用的netty+webflux实现。

代码示例

需要用到3个项目,eureka-server、gateway、biz-user-service。

eureka-server

eureka注册中心的搭建前面的文章也已经介绍过了,这里直接贴代码。

  1. pom.xml文件中引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  2. application.yml配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server:
    port: 9110
    spring:
    application:
    name: eureka-server
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:9110/eureka #注册中心地址,其他程序通过此URL进行注册
    register-with-eureka: false #当前应用就是Eureka Server,不需要注册到eureka
    fetch-registry: false #因为euraka-server是单个节点,不需要同步其他节点的数据
    logging:
    config: classpath:logback.xml
  3. 启动引导类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaServerApplication {

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

    }

biz-user-service

biz-user-service是一个用户业务的服务,在前面的文章里也用到过。

  1. pom.xml文件中引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. application.yml配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    server:
    port: 8001
    spring:
    application:
    name: biz-user-service
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:9110/eureka
    logging:
    config: classpath:logback.xml
  3. controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Slf4j
    @RestController
    @RequestMapping(value = "/user", method = {RequestMethod.GET, RequestMethod.POST})
    public class UserController {


    @Autowired
    private UserService userService;


    @RequestMapping("/getById")
    public ResponseData getById(@RequestParam Long id) {
    User user = userService.getById(id);
    log.info("用户|查询用户|id={}|{}", id, user);
    return ResponseData.data(user);
    }



    @RequestMapping("/getCoupon")
    public ResponseData getCoupon(@RequestParam Long userId, @RequestParam String type) {
    log.info("用户|查询用户的优惠券|userId={},type={}", userId, type);
    return ResponseData.success();
    }


    }
  4. 启动引导类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @SpringBootApplication
    @EnableEurekaClient
    public class UserServiceBootstrap {


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

    }

gateway网关

这里开始就是编写网关项目了。

  1. pom.xml文件中引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    <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. application.yml配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    server:
    port: 8015
    spring:
    application:
    name: gateway
    cloud:
    gateway:
    discovery:
    locator:
    # 是否和服务注册与发现组件结合,设置为 true 后可以直接使用应用名称调用服务
    enabled: true
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:9110/eureka
  3. 定义一个全局的过滤器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    package com.lzumetal.springcloud.gateway.filter;

    import io.netty.buffer.ByteBufAllocator;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.core.io.buffer.DataBufferUtils;
    import org.springframework.core.io.buffer.NettyDataBufferFactory;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;

    import java.net.URI;
    import java.nio.CharBuffer;
    import java.nio.charset.StandardCharsets;
    import java.util.Map;
    import java.util.concurrent.atomic.AtomicReference;

    @Component
    @Slf4j
    public class ParameterGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpRequest serverHttpRequest = exchange.getRequest();
    String uriString = serverHttpRequest.getURI().toString();
    String method = serverHttpRequest.getMethodValue();
    if ("POST".equals(method)) {
    //从请求里获取Post请求体
    String bodyStr = resolveBodyFromRequest(serverHttpRequest);
    log.info("【POST请求】|{}|{}", uriString, bodyStr);
    //TODO 得到Post请求的请求参数后,做你想做的事

    //下面的将请求体再次封装写回到request里,传到下一级,否则,由于请求体已被消费,后续的服务将取不到值
    URI uri = serverHttpRequest.getURI();
    ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build();
    DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
    Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);

    request = new ServerHttpRequestDecorator(request) {
    @Override
    public Flux<DataBuffer> getBody() {
    return bodyFlux;
    }
    };
    //封装request,传给下一级
    return chain.filter(exchange.mutate().request(request).build());
    } else if ("GET".equals(method)) {
    Map requestQueryParams = serverHttpRequest.getQueryParams();
    log.info("【GET请求】|{}|{}", uriString, requestQueryParams);
    //TODO 得到Get请求的请求参数后,做你想做的事

    return chain.filter(exchange);
    }
    return chain.filter(exchange);
    }

    /**
    * 从Flux<DataBuffer>中获取字符串的方法
    *
    * @return 请求体
    */
    private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
    //获取请求体
    Flux<DataBuffer> body = serverHttpRequest.getBody();

    AtomicReference<String> bodyRef = new AtomicReference<>();
    body.subscribe(buffer -> {
    CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
    DataBufferUtils.release(buffer);
    bodyRef.set(charBuffer.toString());
    });
    //获取request body
    return bodyRef.get();
    }

    private DataBuffer stringBuffer(String value) {
    byte[] bytes = value.getBytes(StandardCharsets.UTF_8);

    NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
    DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
    buffer.write(bytes);
    return buffer;
    }

    @Override
    public int getOrder() {
    return 0;
    }
    }
  4. 配置路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Configuration
    public class UserServiceRouteConfig {

    public void addRoute(RouteLocatorBuilder.Builder routeBuilder) {
    routeBuilder.route("user-service",
    predicateSpec -> predicateSpec.path("/user/**")
    .uri("lb://biz-user-service"));
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class RouteConfig {

    @Autowired
    private UserServiceRouteConfig userServiceRouteConfig;

    @Bean
    public RouteLocator activityRouter(RouteLocatorBuilder builder) {
    RouteLocatorBuilder.Builder builderRoutes = builder.routes();
    userServiceRouteConfig.addRoute(builderRoutes);
    return builderRoutes.build();
    }
    }
  5. 启动引导类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @SpringBootApplication
    @EnableDiscoveryClient
    public class GatewayBootstrap {

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

    }

验证网关

启动上面的3个服务,在浏览器中访问:http://localhost:9110/ 。可以看到 gateway 和 biz-user-service 两个服务已经启动并注册了。

直接访问 biz-user-service 的接口:

通过网关服务访问biz-user-service 的接口:

并且看到有日志打印:

1
INFO 38264 --- [ctor-http-nio-3] c.l.s.g.filter.ParameterGlobalFilter : 【GET请求】|http://localhost:8015/user/getById?id=2|{id=[2]}

------ 本文完 ------