微服务系列导航

  • 第一篇:初学者如何快速入门微服务,面试前如何快速熟悉微服务
  • 第二篇:微服务Spring Cloud Alibaba之Nacos篇, Nacos 就是注册中心 + 配置中心的组合
  • 第三篇:微服务Spring Cloud Alibaba之Sentinel篇,使用熔断器防止雪崩
  • 第四篇:Spring Cloud Gateway面试攻略,微服务网关的作用以及案例

此文目录

    • 微服务系列导航
    • 一、什么是微服务网关
    • 二、支持特征
    • 三、工作原理
    • 四、面试题
      • 4.1 过滤器和网关的对比
      • 4.2 zuul和spring cloud gateway的对比
      • 4.3 网关与nginx区别
      • 4.4 gateway的组成
    • 五、项目实战
      • 5.1 MateCloud项目源码
      • 5.2核心依赖
      • 5.3 网关统一认证
      • 5.4 网关动态路由
    • 六、写在最后

一、什么是微服务网关

Spring Cloud Gateway 是 Spring 官方基于 Spring 5.x,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,Spring Cloud Gateway 旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式。Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全性、监视/指标和弹性。

二、支持特征

  • 基于 Spring Framework 5,Project Reactor和Spring Boot 2.0之上
  • 动态路由
  • Predicates 和 Filters 作用于特定路由
  • 断路器集成
  • 集成 Spring Cloud DiscoveryClient
  • 易于编写的 Predicates 和 Filters
  • 限流
  • 路径重写

三、工作原理

下图从总体上概述了Spring Cloud Gateway的工作方式:

客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑。

四、面试题

4.1 过滤器和网关的对比

  • 过滤器:对单个服务器的请求进行拦截控制
  • 网关:对所有的服务器的请求进行拦截控制

4.2 zuul和spring cloud gateway的对比

  • zuul:是Netflix的,是基于servlet实现的,阻塞式的api,不支持长连接。
  • gateway:是springcloud自己研制的微服务网关,是基于Spring5构建,能够实现响应式非阻塞式的Api,支持长连接

4.3 网关与nginx区别

  • 相同点:都是可以实现对api接口的拦截,负载均衡、反向代理、请求过滤等,可以实现和网关一样的效果。

  • 不同点:

  1. Nginx采用C语言编写,Gateway属于Java语言编写的, 能够更好让我们使用java语言来实现对请求的处理。
  2. Nginx 属于服务器端负载均衡器。
  3. Gateway 属于本地负载均衡器。

4.4 gateway的组成

  • 路由 : 网关的基本模块,有ID,目标URI,一组断言和一组过滤器组成
  • 断言:就是访问该旅游的访问规则,可以用来匹配来自http请求的任何内容,例如headers或者参数
  • 过滤器:这个就是我们平时说的过滤器,用来过滤一些请求的,gateway有自己默认的过滤器,具体请参考官网,我们也可以自定义过滤器,但是要实现两个接口,ordered和globalfilter

五、项目实战

5.1 MateCloud项目源码

项目GITHUB码云
MateCloud后端源码https://github/matevip/matecloudhttps://gitee/matevip/matecloud
Artemis前端源码https://github/matevip/artemishttps://gitee/matevip/artemis

5.2核心依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

注意:

  • Spring Cloud Gateway 不使用 Web 作为服务器,而是 使用 WebFlux 作为服务器 ,Gateway 项目已经依赖了 starter-webflux,所以这里千万不要依赖 starter-web
  • 由于过滤器等功能依然需要 Servlet 支持,故这里还需要依赖javax.servlet:javax.servlet-api

地址:https://gitee/matevip/matecloud/blob/dev/mate-gateway/pom.xml

5.3 网关统一认证

package vip.mate.gateway.filter;

import io.jsonwebtoken.Claims;
import lombok.AllArgsConstructor;
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.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import vip.mate.core.cloud.props.MateApiProperties;
import vip.mate.coremon.constant.MateConstant;
import vip.mate.coremon.constant.Oauth2Constant;
import vip.mate.coremon.util.ResponseUtil;
import vip.mate.coremon.util.SecurityUtil;
import vip.mate.coremon.util.StringPool;
import vip.mate.coremon.util.TokenUtil;
import vip.mate.core.redis.core.RedisService;

/**
 * 网关统一的token验证
 *
 * @author pangu
 * @since 1.5.8
 */
@Slf4j
@Component
@AllArgsConstructor
public class PreUaaFilter implements GlobalFilter, Ordered {

	private final MateApiProperties mateApiProperties;

	private final RedisService redisService;

	/**
	 * 路径前缀以/mate开头,如mate-system
	 */
	public static final String PATH_PREFIX = "/mate";

	/**
	 * 索引自1开头检索,跳过第一个字符就是检索的字符的问题
	 */
	public static final int FROM_INDEX = 1;

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		// 如果未启用网关验证,则跳过
		if (!mateApiProperties.getEnable()) {
			return chain.filter(exchange);
		}

		// 如果在忽略的url里,则跳过
		String path = replacePrefix(exchange.getRequest().getURI().getPath());
		String requestUrl = exchange.getRequest().getURI().getRawPath();
		if (ignore(path) || ignore(requestUrl)) {
			return chain.filter(exchange);
		}

		// 验证token是否有效
		ServerHttpResponse resp = exchange.getResponse();
		String headerToken = exchange.getRequest().getHeaders().getFirst(Oauth2Constant.HEADER_TOKEN);
		if (headerToken == null) {
			return unauthorized(resp, "没有携带Token信息!");
		}
		String token = TokenUtil.getToken(headerToken);
		Claims claims = SecurityUtil.getClaims(token);
		if (claims == null) {
			return unauthorized(resp, "token已过期或验证不正确!");
		}

		// 判断token是否存在于redis,对于只允许一台设备场景适用。
		// 如只允许一台设备登录,需要在登录成功后,查询key是否存在,如存在,则删除此key,提供思路。
		boolean hasKey = redisService.hasKey("auth:" + token);
		log.debug("查询token是否存在: " + hasKey);
		if (!hasKey) {
			return unauthorized(resp, "登录超时,请重新登录");
		}
		return chain.filter(exchange);
	}

	/**
	 * 检查是否忽略url
	 * @param path 路径
	 * @return boolean
	 */
	private boolean ignore(String path) {
		return mateApiProperties.getIgnoreUrl().stream()
				.map(url -> url.replace("/**", ""))
				.anyMatch(path::startsWith);
	}

	/**
	 * 移除模块前缀
	 * @param path 路径
	 * @return String
	 */
	private String replacePrefix(String path) {
		if (path.startsWith(PATH_PREFIX)) {
			return path.substring(path.indexOf(StringPool.SLASH, FROM_INDEX));
		}
		return path;
	}

	private Mono<Void> unauthorized(ServerHttpResponse resp, String msg) {
		return ResponseUtil.webFluxResponseWriter(resp, MateConstant.JSON_UTF8, HttpStatus.UNAUTHORIZED, msg); }

	@Override
	public int getOrder() {
		return MateConstant.MATE_UAA_FILTER_ORDER;
	}

}

5.4 网关动态路由

package vip.mate.gateway.service.impl;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

/**
 * 动态更新路由网关service
 * 1)实现一个Spring提供的事件推送接口ApplicationEventPublisherAware
 * 2)提供动态路由的基础方法,可通过获取bean操作该类的方法。该类提供新增路由、更新路由、删除路由,然后实现发布的功能。
 * @author pangu
 */
@Slf4j
@Service
@AllArgsConstructor
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

    private final RouteDefinitionWriter routeDefinitionWriter;

    /**
     * 发布事件
     */
    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    /**
     * 删除路由
     * @param id
     * @return
     */
    public String delete(String id) {
        try {
            log.info("gateway delete route id {}",id);
            this.routeDefinitionWriter.delete(Mono.just(id));
            return "delete success";
        } catch (Exception e) {
            return "delete fail";
        }
    }

    /**
     * 更新路由
     * @param definition
     * @return
     */
    public String update(RouteDefinition definition) {
        try {
            log.info("gateway update route {}",definition);
            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
        } catch (Exception e) {
            return "update fail,not find route  routeId: "+definition.getId();
        }
        try {
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception e) {
            return "update route fail";
        }
    }

    /**
     * 增加路由
     * @param definition
     * @return
     */
    public String add(RouteDefinition definition) {
        log.info("gateway add route {}",definition);
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }
}

六、写在最后

网关更多功能可以查看官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
和MateCloud的网关实战源码,去探索一下。

送您一句话:安全生产、人人有责

更多推荐

Spring Cloud Gateway面试攻略,微服务网关的作用以及案例