• SpringCloud实战十一:Gateway之 Spring Cloud Gateway


    1.网关是怎么演化来的
    • 单体应用拆分成多个服务后,对外需要一个统一入口,解耦客户端与内部服务
      在这里插入图片描述
    2.网关的基本功能
    • 网关核心功能是路由转发,因此不要有耗时操作在网关上处理,让请求快速转发到后端服务上
    • 网关还能做统一的熔断、限流、认证、日志监控等
      在这里插入图片描述
    3.关于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。这里需要注意一下gateway使用的netty+webflux实现,不要加入web依赖(不要引用webmvc),否则初始化会报错 ,需要加入webflux依赖。

    gateway与zuul的简单比较:gateway使用的是异步请求,zuul是同步请求,gateway的数据封装在ServerWebExchange里,zuul封装在RequestContext里,同步方便调式,可以把数据封装在ThreadLocal中传递。

    Spring Cloud Gateway有三个核心概念:路由、断言、过滤器
    过滤器:gateway有两种filter:GlobalFilter、GatewayFilter,全局过滤器默认对所有路由有效。
    文档地址:https://cloud.spring.io/spring-cloud-static/Finchley.SR2/multi/multi_spring-cloud.html

    网关作为所有请求流量的入口,在实际生产环境中为了保证高可靠和高可用,尽量避免重启,需要用到动态路由配置,在网关运行过程中更改路由配置

    4.代码实践

    需要用到3个项目,eureka-server、gateway、consumer-service

    • 1.eureka-server 服务发现注册,供gateway转发请求时获取服务实例 ip+port,使用前面博客中的示例代码
    • 2.新建 gateway 网关项目,项目引用如下:
    <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>
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-actuator</artifactId>
    </dependency> 

    在主类上启用服务发现注册注解 @EnableDiscoveryClient
    配置文件内容如下:

    server:
      port: 9999
    spring:
      profiles:
        active: dev
      application:
        name: gateway-service
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true
              # 服务名小写
              lower-case-service-id: true
          routes:
          - id: consumer-service
            # lb代表从注册中心获取服务,且已负载均衡方式转发
            uri: lb://consumer-service
            predicates:
            - Path=/consumer/**
            # 加上StripPrefix=1,否则转发到后端服务时会带上consumer前缀
            filters:
            - StripPrefix=1
    
    # 注册中心
    eureka:
      instance:
        prefer-ip-address: true
      client:
        service-url:
          defaultZone: http://zy:zy123@localhost:10025/eureka/
    
    
    # 暴露监控端点
    management:
      endpoints:
        web:
          exposure:
            include: '*'
      endpoint:
        health:
          show-details: always 

    上面就完成了网关代码部分,下面新建consumer-service

    • 3.consumer-service 消费者服务 ,通过网关路由转发到消费者服务,并返回信息回去,因此是个mvc的项目
      项目引用如下:
    <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> 

    在主类上启用服务发现注册注解 @EnableDiscoveryClient
    在配置文件中添加配置:

    server.port=9700
    spring.application.name=consumer-service
    eureka.instance.prefer-ip-address=true
    # 配置eureka-server security的账户信息
    eureka.client.serviceUrl.defaultZone=http://zy:zy123@localhost:10025/eureka/
    
    •  

    新建 IndexController ,添加一个 hello 方法,传入name参数,访问后返回 hi + name 字符串

    @RestController
    public class IndexController {
    
        @RequestMapping("/hello")
        public String hello(String name){
            return "hi " + name;
        }
    } 
    • 4.分别启动3个项目,访问 http://localhost:10025 看eureka上gateway与consumer-service实例是否注册了,可以看到已经注册了,分别在9700、9999端口
      在这里插入图片描述

    通过网关访问consumer-service的hello方法,http://localhost:9999/consumer/hello?name=zy ,效果如下,说明请求已经转发到consumer-service服务上了
    在这里插入图片描述

    以上完成了网关的基本代码,下面继续介绍一些常用的过滤器,通过过滤器实现统一认证鉴权、日志、安全等检验

    • 5.在网关项目中添加 GlobalFilter 全局过滤器,打印每次请求的url,代码如下:
    /**
     * 全局过滤器
     * 所有请求都会执行
     * 可拦截get、post等请求做逻辑处理
     */
    @Component
    public class RequestGlobalFilter implements GlobalFilter,Ordered {
    
        //执行逻辑
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest serverHttpRequest= exchange.getRequest();
            String uri = serverHttpRequest.getURI().toString();
            System.out.println(" uri : " + uri);//打印每次请求的url
            String method = serverHttpRequest.getMethodValue();
            if ("POST".equals(method)) {
                //从请求里获取Post请求体
                String bodyStr = resolveBodyFromRequest(serverHttpRequest);
                //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();
                //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 1;
        }
    } 

    重新运行网关项目,并访问 http://localhost:9999/consumer/hello?name=zy ,查看控制台,可看到 uri 日志被打印出来了
    在这里插入图片描述

    • 6.在网关项目中添加 GatewayFilter 过滤器 ,我们给consumer-service 添加 token 认证过滤器 ,和全局过滤器u同的是,GatewayFilter需要在配置文件中指定那个服务使用此过滤器,代码如下:
    /**
     * 可对客户端header 中的 Authorization 信息进行认证
     */
    @Component
    public class TokenAuthenticationFilter extends AbstractGatewayFilterFactory {
    
        private static final String Bearer_ = "Bearer ";
    
        @Override
        public GatewayFilter apply(Object config) {
            return (exchange, chain) -> {
                ServerHttpRequest request = exchange.getRequest();
                ServerHttpRequest.Builder mutate = request.mutate();
                ServerHttpResponse response = exchange.getResponse();
                try {
                    //String token = exchange.getRequest().getQueryParams().getFirst("authToken");
                    //1.获取header中的Authorization
                    String header = request.getHeaders().getFirst("Authorization");
                    if (header == null || !header.startsWith(Bearer_)) {
                        throw new RuntimeException("请求头中Authorization信息为空");
                    }
                    //2.截取Authorization Bearer
                    String token = header.substring(7);
                    //可把token存到redis中,此时直接在redis中判断是否有此key,有则校验通过,否则校验失败
                    if(!StringUtils.isEmpty(token)){
                        System.out.println("验证通过");
                        //3.有token,把token设置到header中,传递给后端服务
                        mutate.header("userDetails",token).build();
                    }else{
                        //4.token无效
                        System.out.println("token无效");
                        DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,"无效的请求");
                        return response.writeWith(Mono.just(bodyDataBuffer));
                    }
                }catch (Exception e){
                    //没有token
                    DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,e.getMessage());
                    return response.writeWith(Mono.just(bodyDataBuffer));
                }
                ServerHttpRequest build = mutate.build();
                return chain.filter(exchange.mutate().request(build).build());
            };
        }
    
        /**
         * 自定义返回错误信息
         * @param response
         * @param status
         * @param message
         * @return
         */
        public DataBuffer responseErrorInfo(ServerHttpResponse response , String status ,String message){
            HttpHeaders httpHeaders = response.getHeaders();
            httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
            httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
    
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            Map<String,String> map = new HashMap<>();
            map.put("status",status);
            map.put("message",message);
            DataBuffer bodyDataBuffer = response.bufferFactory().wrap(map.toString().getBytes());
            return bodyDataBuffer;
        }
    } 

    在配置文件中指定consumer-service服务使用 TokenAuthenticationFilter ,配置如下:

    routes:
    - id: consumer-service
      uri: lb://consumer-service
      predicates:
      - Path=/consumer/**
      filters:
      # 进行token过滤
      - TokenAuthenticationFilter
      - StripPrefix=1 

    运行项目,再次访问 http://localhost:9999/consumer/hello?name=zy
    在这里插入图片描述

    • 7.前后端分离项目解决网关跨域问题,在网关主类中添加以下代码:
    @Bean
    	public WebFilter corsFilter() {
    		return (ServerWebExchange ctx, WebFilterChain chain) -> {
    			ServerHttpRequest request = ctx.getRequest();
    			if (!CorsUtils.isCorsRequest(request)) {
    				return chain.filter(ctx);
    			}
    
    			HttpHeaders requestHeaders = request.getHeaders();
    			ServerHttpResponse response = ctx.getResponse();
    			HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
    			HttpHeaders headers = response.getHeaders();
    			headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
    			headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
    			if (requestMethod != null) {
    				headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
    			}
    			headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
    			headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "all");
    			headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
    			if (request.getMethod() == HttpMethod.OPTIONS) {
    				response.setStatusCode(HttpStatus.OK);
    				return Mono.empty();
    			}
    			return chain.filter(ctx);
    		};
    	} 

    代码已上传至码云,源码,项目使用的版本信息如下:

    - SpringBoot 2.0.6.RELEASE
    - SpringCloud Finchley.SR2

    转自:https://blog.csdn.net/dark868/article/details/106532216/
  • 相关阅读:
    Win10 .Net Framework 3.5 安装错误 0x800F0954
    论如何去掌握一门新技术
    SpringBoot入门(二):日志及自定义属性
    SpringBoot入门(一):从HelloWorld开始
    【总结】Java面试题
    【SpringBoot-创建项目】一.通过Idea创建SpringBoot项目
    130道 Java多线程面试题汇总
    100道精选Redis面试题,最新Java面试题
    最新Redis面试题,附答案
    Little定律
  • 原文地址:https://www.cnblogs.com/javalinux/p/14265506.html
Copyright © 2020-2023  润新知