微服务组件Gateway核心使用小结

为什么需要网关?网关的基本功能是什么?

当我们将应用拆成微服务之后,客户端对于这些服务的调用就会变得十分复杂(每个客户端只能自己去记录每一个服务的调用地址在进行调用),所以我们可以通过网关来统一管理这些服务的路由。
除此之外网关还有以下好处:

  1. 统一认证,将所有服务都使用网关作为入口,避免每个服务调用都需要认证。
  2. 统一处理跨域请求,避免不同场景处理的复杂度。
  3. 统一配置网关路由,避免客户端代码请求地址编写的复杂性。

Spring Cloud Gateway和Nginx有什么区别?

两者使用的场景不同,nginx作为请求的第一道关卡,其作为开源免费的的反向代理服务器,其并发处理能力以及非常小的内存开销是gateway未能拥有的。
gateway作为请求到达每一个微服务应用前的最后一道关卡,在和nacos整合之后,即可通过nacos获取各个服务信息,结合配置的路由配置即可将请求转发实际请求的服务上。

路由、断言、过滤器是什么?

  1. 路由(route):由id、目标uri、断言集合和过滤器组成,只有符合断言的请求地址才能真正请求到这条配置的uri地址。
  2. 断言(predicate):路由的组成部分,通过predicate可以决定要请求到目标地址的请求条件。
  3. 过滤器(filter): 可以对请求之前或者请求之后的参数进行修改。

如何搭建一个微服务网关?如何使用断言进行路由匹配?

第一步肯定是基于Spring Boot创建一个网关应用了,然后引入相关依赖了,注意版本间是有兼容性的,具体可以参照下面这张表格:

在这里插入图片描述

以笔者为例,笔者父pom中的版本如下

  <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
        <spring-boot.version>2.2.5.RELEASE</spring-boot.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    </properties>

完成后我们就可以通过properties配置自己的路由规则了,如下predicates的配置,namepath意为通过路径进行断言,args/system,如果笔者希望访问system的服务,那么我们就可以键入127.0.0.1:9000/system/xxx即可将结果转发到127.0.0.1:9001/system/xxx

# 应用名称为gateway
spring.application.name=gateway
# 端口号
server.port=9000

# 将gateway注册到nacos上
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 命名空间
spring.cloud.nacos.discovery.namespace=dev
# 组名
spring.cloud.nacos.discovery.group=myGroup


# 路由id名称,必须唯一
spring.cloud.gateway.routes[0].id=system
# 转发的路由地址
spring.cloud.gateway.routes[0].uri=http://127.0.0.1:9001
# 断言配置,使用path进行匹配,只要是/system/**都进行转发
spring.cloud.gateway.routes[0].predicates[0].name=Path
spring.cloud.gateway.routes[0].predicates[0].args[0]=/system/**

如下所示,笔者需要访问system/test/test,直接通过gateway访问即可

curl 127.0.0.1:9000/system/test/test

可以看到请求确实来到的system服务上

在这里插入图片描述

更多断言可以参考官方文档

https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/

Gateway中的过滤器是什么?

假如我们想配置一个局部过滤器,我们可以集成AbstractGatewayFilterFactory编写一个类,如下所示,可以看到笔者编写了一个LoginAdminGatewayFilterFactory ,然后我们就来实现LoginAdminGatewayFilter

@Component
public class LoginAdminGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

    @Resource
    LoginAdminGatewayFilter loginAdminGatewayFilter;

    @Override
    public GatewayFilter apply(Object config) {
        return loginAdminGatewayFilter;
    }
}

可以看到笔者基于gateway实现了一个自定义的权限校验,通过集成GatewayFilter即可完成路由请求前的拦截,通过请求映射、参数的信息实现个性化拦截处理。

@Component
public class LoginAdminGatewayFilter implements GatewayFilter, Ordered {

    private static final Logger LOG = LoggerFactory.getLogger(LoginAdminGatewayFilter.class);

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 请求地址中不包含/admin/的,不是控台请求,不需要拦截
        if (!path.contains("/admin/")) {
            return chain.filter(exchange);
        }
        if (path.contains("/system/admin/user/login")
                || path.contains("/system/admin/user/logout")
                || path.contains("/system/admin/kaptcha")) {
            LOG.info("不需要控台登录验证:{}", path);
            return chain.filter(exchange);
        }
        //获取header的token参数
        String token = exchange.getRequest().getHeaders().getFirst("token");
        LOG.info("控台登录验证开始,token:{}", token);
        if (token == null || token.isEmpty()) {
            LOG.info( "token为空,请求被拦截" );
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        Object object = redisTemplate.opsForValue().get(token);
        if (object == null) {
            LOG.warn( "token无效,请求被拦截" );
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        } else {
            LOG.info("已登录:{}", object);


            // 增加权限校验,gateway里没有LoginUserDto,所以全部用JSON操作
            LOG.info("接口权限校验,请求地址:{}", path);
            boolean exist = false;
            JSONObject loginUserDto = JSON.parseObject(String.valueOf(object));
            JSONArray requests = loginUserDto.getJSONArray("requests");
            // 遍历所有【权限请求】,判断当前请求的地址是否在【权限请求】里
            for (int i = 0, l = requests.size(); i < l; i++) {
                String request = (String) requests.get(i);
                if (path.contains(request)) {
                    exist = true;
                    break;
                }
            }
            if (exist) {
                LOG.info("权限校验通过");
            } else {
                LOG.warn("权限校验未通过");
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }

            return chain.filter(exchange);
        }
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

完成后,我们希望system服务可以应用这个过滤器只需将LoginAdminGatewayFilterFactory 的前半部分LoginAdmin配置到filters即可,如下所示

# 路由id名称,必须唯一
spring.cloud.gateway.routes[0].id=system
# 转发的路由地址
spring.cloud.gateway.routes[0].uri=http://127.0.0.1:9001
# 断言配置,使用path进行匹配,只要是/system/**都进行转发
spring.cloud.gateway.routes[0].predicates[0].name=Path
spring.cloud.gateway.routes[0].predicates[0].args[0]=/system/**
# gateway的system路由引入一个局部过滤器LoginAdminGatewayFilterFactory 
spring.cloud.gateway.routes[0].filters[0].name=LoginAdmin
spring.cloud.gateway.routes[0].filters[0].args[0]=true

我们再次发送上文的system请求,可以看到这个局部过滤器确实拦截到了请求,而且参数中都带着请求的各种信息。

在这里插入图片描述

除了局部过滤器,还有一个全局过滤器(作用于全路由上),示例如下所示:

@Slf4j
@Component
@Order(value = Integer.MIN_VALUE)
public class AccessLogGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //filter的前置处理
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().pathWithinApplication().value();
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        return chain
                //继续调用filter
                .filter(exchange)
                //filter的后置处理
                .then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            HttpStatus statusCode = response.getStatusCode();
            log.info("请求路径:{},远程IP地址:{},响应码:{}", path, remoteAddress, statusCode);
        }));
    }
}

如何集成Nacos注册中心并实现负载均衡?

先说第一个问题如何集成到Nacos注册中心,其实上文我们已经做到了,就是这几条配置使得gateway指向nacos

# 将gateway注册到nacos上
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 命名空间
spring.cloud.nacos.discovery.namespace=dev
# 组名
spring.cloud.nacos.discovery.group=myGroup

再来说说负载均衡,我们上文中配置system的转发地址为

spring.cloud.gateway.routes[0].uri=http://127.0.0.1:9001

这样配置当我们请求system服务时,gateway过滤中的LoadBalancerClientFilter进行请求转发时,就会使用原始请求进行转发参见下面的源码

 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
        //如果转发地址配置包含lb则进行负载均衡算法获取转发实例
        if (url != null && ("lb".equals(url.getScheme()) || "lb".equals(schemePrefix))) {
            ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
            if (log.isTraceEnabled()) {
                log.trace("LoadBalancerClientFilter url before: " + url);
            }
			//获取实例
            ServiceInstance instance = this.choose(exchange);
            if (instance == null) {
                throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());
            } else {
            	//解析这个实例组装生成requestUrl 
                URI uri = exchange.getRequest().getURI();
                String overrideScheme = instance.isSecure() ? "https" : "http";
                if (schemePrefix != null) {
                    overrideScheme = url.getScheme();
                }

                URI requestUrl = this.loadBalancer.reconstructURI(new DelegatingServiceInstance(instance, overrideScheme), uri);
                if (log.isTraceEnabled()) {
                    log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
                }
				//将requestUrl 存到exchange,继续走到下一个filter
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
                return chain.filter(exchange);
            }
        } else {
        //转发地址没有配置lb则直接带着原始请求进入下一个filter,你可以理解为直接请求地址
            return chain.filter(exchange);
        }
    }

所以我们只需将请求地址改为lb://服务名

spring.cloud.gateway.routes[0].uri=lb://system

再次curl 127.0.0.1:9000/system/test/test,查看LoadBalancerClientFilter可以看到这个请求确实走到了负载均衡获取实例的choose方法上。

在这里插入图片描述

如何集成Nacos实现一处修改到处生效?

首先就是引入nacos配置相关依赖

 <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>


        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

完成后配置bootstrap.properties,注意是bootstrap,否则配置不会生效。然后配置如下内容,可以看到笔者的应用名为gatewayfile-extensionproperties,所以我们就在nacos命名空间为dev,组为myGroup,添加一个为gateway.properties的配置

# 端口号
server.port=9000
# 应用名称为gateway
spring.application.name=gateway

#eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# 将gateway注册到nacos上
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848


spring.cloud.nacos.discovery.namespace=dev

spring.cloud.nacos.discovery.group=myGroup


# 命名空间
spring.cloud.nacos.config.namespace=dev
# 组名
spring.cloud.nacos.config.group=myGroup



# 使用nacos上的路由配置
spring.cloud.nacos.config.file-extension=properties
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

nacos上配置的gateway.properties内容如下


# 路由id名称,必须唯一
spring.cloud.gateway.routes[0].id=system
# 转发的路由地址
spring.cloud.gateway.routes[0].uri=lb://system
# 断言配置,使用path进行匹配,只要是/system/**都进行转发
spring.cloud.gateway.routes[0].predicates[0].name=Path
spring.cloud.gateway.routes[0].predicates[0].args[0]=/system/**
spring.cloud.gateway.routes[0].filters[0].name=LoginAdmin
spring.cloud.gateway.routes[0].filters[0].args[0]=true

spring.cloud.gateway.routes[1].id=business
#spring.cloud.gateway.routes[1].uri=http://127.0.0.1:9002
spring.cloud.gateway.routes[1].uri=lb://business
spring.cloud.gateway.routes[1].predicates[0].name=Path
spring.cloud.gateway.routes[1].predicates[0].args[0]=/business/**
spring.cloud.gateway.routes[1].filters[0].name=LoginAdmin
spring.cloud.gateway.routes[1].filters[0].args[0]=true

spring.cloud.gateway.routes[2].id=file
#spring.cloud.gateway.routes[2].uri=http://127.0.0.1:9003
spring.cloud.gateway.routes[2].uri=lb://file
spring.cloud.gateway.routes[2].predicates[0].name=Path
spring.cloud.gateway.routes[2].predicates[0].args[0]=/file/**
spring.cloud.gateway.routes[2].filters[0].name=LoginAdmin
spring.cloud.gateway.routes[2].filters[0].args[0]=true

spring.redis.host=r-uf6ljbcdaxobsifyctpd.redis.rds.aliyuncs.com
spring.redis.port=6379
spring.redis.password=Redis000

再次键入上方的请求我们发现,请求可以正常转发

gateway全局异常处理了解过嘛?

如下所示,集成ErrorWebExceptionHandler 即可

/**
 * 用于网关的全局异常处理
 * @Order(-1):优先级一定要比ResponseStatusExceptionHandler低
 */
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {

	private final ObjectMapper objectMapper;

	@SuppressWarnings({"rawtypes", "unchecked", "NullableProblems"})
	@Override
	public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
		ServerHttpResponse response = exchange.getResponse();
		if (response.isCommitted()) {
			return Mono.error(ex);
		}

		// JOSN格式返回
		response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
		if (ex instanceof ResponseStatusException) {
			response.setStatusCode(((ResponseStatusException) ex).getStatus());
		}

		return response.writeWith(Mono.fromSupplier(() -> {
			DataBufferFactory bufferFactory = response.bufferFactory();
			try {
				//todo 返回响应结果,根据业务需求,自己定制
				CommonResponse resultMsg = new CommonResponse("500",ex.getMessage(),null);
				return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMsg));
			}
			catch (JsonProcessingException e) {
				log.error("Error writing response", ex);
				return bufferFactory.wrap(new byte[0]);
			}
		}));
	}
}

参考文献

Spring Cloud Gateway夺命连环10问?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shark-chili

您的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值