admin管理员组

文章数量:1608828

Spring Cloud Gateway由浅入深

    • 1、如何理解微服务网关Gateway
    • 2、核心名词讲解
    • 3、搭建微服务网关
    • 4、异常处理
    • 5、服务熔断
    • 6、微服务防护
    • 7、跨域设置
    • 8、服务限流
    • 9、禁止访问资源
    • 思考?

1、如何理解微服务网关Gateway

Spring Cloud Gateway 旨在提供一种简单而有效的方式来路由到 API,并为它们提供横切关注点,例如:安全性、监控/指标和弹性。
按照官方的说法,Spring Cloud Gateway主要用于路由、安全拦截等。

2、核心名词讲解

Spring Cloud Gateway包含但不仅限于路由、谓词工厂、过滤器这些专业名词。

  • 路由:网关的基本构建块。它由 ID、目标 URI、谓词集合和过滤器集合定义。如果聚合谓词为真,则匹配路由。id代表唯一标识,随便填写,uri与服务名称相结合,常用格式lb://微服务名称。

  • 谓词:匹配来自 HTTP 请求的任何内容,例如标头或参数。

  • 过滤器:在发送下游请求之前或之后修改请求和响应。

3、搭建微服务网关

本章节只讲解Spring Cloud Gateway网关相关、服务注册发现将不会体现
pom文件引入Maven依赖

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

application.yaml文件

server:
  port: 8301
spring:
  application:
    name: Test-Gateway
  cloud:
    gateway:
      routes:
        - id: febs-auth
          uri: lb://Test-Auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
        - id: FEBS-Server-System
          uri: lb://Test-Server-System
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
        - id: FEBS-Server-Test
          uri: lb://Test-Server-Test
          predicates:
            - Path=/test/**
          filters:
            - StripPrefix=1

predicates谓词,用于匹配规则。filters过滤器,StripPrefix=1表示截取请求前一位,例如/auth/token - -> /token。
访问http://127.0.0.1/8301/auth/token接口,在Gateway作用下相当于访问Test-Auth微服务的token接口。

4、异常处理

Spring Cloud Gateway默认使用DefaultErrorWebExceptionHandler构建异常信息对象,ErrorWebFluxAutoConfiguration配置中,采用了
@ConditionalOnMissingBean(value=ErrorWebExceptionHandler.class)注解,所以我们可以继承DefaultErrorWebExceptionHandler,自定义异常处理。

创建IGatewayExceptionHandler类。

/**
 * Gateway异常处理
 */
@Slf4j
public class IGatewayExceptionHandler extends DefaultErrorWebExceptionHandler {


    public IGatewayExceptionHandler(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);
        log.error(
                "请求发生异常,请求URI:{},请求方法:{},异常信息:{}",
                request.path(), request.methodName(), error.getMessage()
        );
        String errorMessage;
        if (error instanceof NotFoundException) {
            String serverId = StringUtils.substringAfterLast(error.getMessage(), "Unable to find instance for ");
            serverId = StringUtils.replace(serverId, "\"", StringUtils.EMPTY);
            errorMessage = String.format("无法找到%s服务", serverId);
        } else if (StringUtils.containsIgnoreCase(error.getMessage(), "connection refused")) {
            errorMessage = "目标服务拒绝连接";
        } else if (error instanceof TimeoutException) {
            errorMessage = "访问服务超时";
        } else if (error instanceof ResponseStatusException
                && StringUtils.containsIgnoreCase(error.getMessage(), HttpStatus.NOT_FOUND.toString())) {
            errorMessage = "未找到该资源";
        } else {
            errorMessage = "网关转发异常";
        }
        Map<String, Object> errorAttributes = new HashMap<>(3);
        errorAttributes.put("code",500);
        errorAttributes.put("message", errorMessage);
        return errorAttributes;
    }

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

    @Override
    protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }

}

将IGatewayExceptionHandler注入SpringIOC容器中去。

@Configuration
public class IGatewayExceptionConfiguration {

    private final ServerProperties serverProperties;
    private final ApplicationContext applicationContext;
    private final ResourceProperties resourceProperties;
    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public IGatewayExceptionConfiguration(ServerProperties serverProperties,
                                          ResourceProperties resourceProperties,
                                          ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                          ServerCodecConfigurer serverCodecConfigurer,
                                          ApplicationContext applicationContext){
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.resourceProperties = resourceProperties;
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes){
        IGatewayExceptionHandler exceptionHandler = new IGatewayExceptionHandler (
                errorAttributes,
                this.resourceProperties,
                this.serverProperties.getError(),
                this.applicationContext);
        exceptionHandler.setViewResolvers(this.viewResolvers);
        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}

Spring检测@Configuration注解时,通过有参构造将IGatewayExceptionHandler需要参数注入进来。

测试并查看控制台打印信息:

请求发生异常,请求URI:/system/testOpenFeign,请求方法:GET,异常信息:Connection refused: no further information
[231e9045] 500 Server Error for HTTP GET "/system/testOpenFeign"

5、服务熔断

当网关转发的微服务长时间未响应,该请求将会一直维持无法释放,占用系统资源,我们可以采用网关熔断技术来解决。
yaml文件添加Hystrix

修改yaml文件:

server:
  port: 8301
spring:
  application:
    name: Test-Gateway
  cloud:
    gateway:
      routes:
        - id: febs-auth
          uri: lb://Test-Auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
        - id: FEBS-Server-System
          uri: lb://Test-Server-System
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
            - name: Hystrix
              args:
                name: systemFallback
                fallbackUri: forward:/fallback/FEBS-Server-System
        - id: FEBS-Server-Test
          uri: lb://Test-Server-Test
          predicates:
            - Path=/test/**
          filters:
            - StripPrefix=1

 
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000

添加GlobalGatewayFallbackController

@RestController
public class GlobalGatewayFallbackController {


    @GetMapping("/fallback/{name}")
    public Mono<FebsResponse> fallback(@PathVariable String name){
        String message = String.format("服务%s访问超时",name);
        return Mono.just(new FebsResponse().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).message(message));
    }

}

设置超时时间3s,fallbackUri执行自定义回滚Controller。
测试结果:

6、微服务防护

虽然我们集成了Gateway网关对微服务进行统一的路由,但是我们仍然可以通过资源服务的端口进行访问,通用的解决方案为:采用token进行校验,Gateway携带token请求,资源服务统一拦截请求进行token校验,如果是网关的请求放行,否则进行拦截并返回错误信息。

添加IGlobalRequestFilter:

@Slf4j
@Component
public class IGlobalRequestFilter implements GlobalFilter {

    private final String TOKEN_HEADER = "GatewayHeader";

    private final String TOKEN_VALUE = "febs:febs";


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 打印转发日志
        printLog(exchange);
        ServerHttpRequest request = exchange.getRequest();
        byte[] token = Base64Utils.encode(TOKEN_VALUE.getBytes());
        ServerHttpRequest build = request.mutate().header(TOKEN_HEADER, new String(token)).build();
        ServerWebExchange newExchange = exchange.mutate().request(build).build();
        return chain.filter(newExchange);
    }

    /**打印转发日志**/
    private void printLog(ServerWebExchange exchange){
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
        LinkedHashSet<URI> uris = exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
        URI originUri = null;
        if (uris != null) {
            originUri = uris.stream().findFirst().orElse(null);
        }
        if (url != null && route != null && originUri != null) {
            log.info("转发请求:{}://{}{} --> 目标服务:{},目标地址:{}://{}{},转发时间:{}",
                    originUri.getScheme(), originUri.getAuthority(), originUri.getPath(),
                    route.getId(), url.getScheme(), url.getAuthority(), url.getPath(), LocalDateTime.now()
            );
        }
    }
}

资源服务器配置:

public class ServerProtectInterceptor implements HandlerInterceptor {

    private final String TOKEN_HEADER = "GatewayHeader";

    private final String TOKEN_VALUE = "febs:febs";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        // 从请求头中获取 Token
        String token = request.getHeader(TOKEN_HEADER );
        String Token = new String(Base64Utils.encode(TOKEN_VALUE .getBytes()));
        // 校验 Token的正确性
        if (StringUtils.equals(Token, token)) {
            return true;
        } else {
            FebsResponse febsResponse = new FebsResponse();
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.getWriter().write(JSONObject.toJSONString(febsResponse.message("请通过网关获取资源")));
            return false;
        }
    }
}

public class ServerProtectConfigure implements WebMvcConfigurer {

    @Bean
    public HandlerInterceptor serverProtectInterceptor() {
        return new ServerProtectInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(serverProtectInterceptor());
    }
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ServerProtectConfigure.class)
public @interface EnableServerProtect {

}

ServerProtectInterceptor 实现HandlerInterceptor,从请求中获取Token进行校验。
ServerProtectConfigure 配置类将ServerProtectInterceptor注入SpringIOC。
EnableServerProtect 注解表示开启ServerProtectConfigure配置

7、跨域设置

添加跨域配置。

@Configuration
public class IGateWayCorsConfigure {

    @Bean
    public CorsWebFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        CorsConfiguration cors = new CorsConfiguration();
        cors.setAllowCredentials(true);
        cors.addAllowedOrigin(CorsConfiguration.ALL);
        cors.addAllowedHeader(CorsConfiguration.ALL);
        cors.addAllowedMethod(CorsConfiguration.ALL);
        source.registerCorsConfiguration("/**", cors);
        return new CorsWebFilter(source);
    }

}

8、服务限流

9、禁止访问资源

思考?

经常有人问到,Nginx和Gateway都是网关,为什么Gateway不可以替代Nginx?

Nginx主要用来做流量入口、负载均衡、反向代理,属于流量网关

  • 负载均衡:针对于服务器端流量分发,采用不同负载策略,实现流量的分发
  • 反向代理:将外部访问地址转向服务器内部资源地址

Gateway将流量分发到不同的微服务上,最要用来做路由、过滤器,属于业务网关

参考资料: mrbird鸟哥的FEBS开源项目、FEBS看云文档。

本文标签: 由浅入深SpringCloudGateway