1. 为什么需要网关
想象这么一个场景, 如果我们有成千上万的微服务, 客户端如果一个一个的去连接, 显然这么做是不现实的, 这个时候网关就出现了~ 如下图右边所示, 所有的请求都必须要经过网关, 然后由网关将请求路由到其他的微服务.
网关的重要性
既然网关作为所有请求的入口, 那么它必须具备哪些要素呢?
2. 网关的要素
2.1. 稳定性, 高可用
服务网关作为请求的入口, 它必须 7*24 小时可用, 网关瘫痪, 系统全挂, 所以稳定性和高可用性是网关的第一也是最重要的要素
2.2. 性能, 并发性
所有的请求都经过网关, 所以压力非常巨大, 并发要求极高
2.3. 安全性
防止外部的恶意访问!
2.4. 扩展性
理论上网关适合处理所有的非业务场景处理, 例如: 协议转发, 防刷, 流量监控, 日志
3. 常用的网关方案
3.1Nginx + lua
Nginx 适合高并发的处理
3.2Kong
这款软件是基于 Nginx+Lua 的, 而且比 Nginx 的配置更简单, 但是这款软件是要收费的
3.3Tyk
开源的轻量级网关, 这是 go 语言开发的
3.4Spring cloud zuul
本篇文章的主角! 虽然是主角, 但是它的性能还是无法和 Nginx 比, 但是据说 zuul2.0 提升了性能, 我们用 SpringCloud, 用 zuul 会更加方便和简单.
4.Zuul
Zuul 的本质就是: 路由 + 过滤器 = Zuul, 它的核心就是一系列的过滤器. Zuul 提供了四种过滤器 API 他们是:
前置 (Pre)
路由 (Route)
后置 (Post)
错误 (Error)
Zuul 的架构图如下所示, 我们可以看到过滤器之间没有直接通信的, 他们之间有一个 RequestContext 上下文.
Zuul 的架构图
我们接下来看一下请求的生命周期: 请求首先到达的是前置过滤器, 对请求路由前的前置加工, 比如对参数校验我们就放在这个 filter 中. routing Filter 就是将请求路由到 Origin Server 中去, 当请求处理完后会由 post filter 做处理. 图的右下角还有一个 error filter, 当三种过滤器发生错误出现异常后, 就会到达这个 error filter. 左下角还有一个 custom filter, 这个 filter 其实可以放到 Prefilter 这里, 也可以放到 post filter.
请求生命周期
4.1 代码与实战
4.1.1pom.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.imooc</groupId>
- <artifactId>API-gateway</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <packaging>jar</packaging>
- <name>API-gateway</name>
- <description>Demo project for Spring Boot</description>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.0.6.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
- <java.version>1.8</java.version>
- <spring-cloud.version>Finchley.SR2</spring-cloud.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-config</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-dependencies</artifactId>
- <version>${spring-cloud.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- </dependencies>
- </dependencyManagement>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
- 4.1.2
main 函数
- @SpringBootApplication
- @EnableZuulProxy
- public class ApiGatewayApplication {
- public static void main(String[] args) {
- SpringApplication.run(ApiGatewayApplication.class, args);
- }
- }
4.1.3 关于路由的转发排除和自定义
zuul 默认的路由就是: 服务名 / 真正的访问路径
在 YAML 配置文件中的相关配置项
- zuul:
- routes:
- # /myProduct/product/list -> /product/product/list
- aaaaaa:
- path: /myProduct/**
- serviceId: product
- sensitiveHeaders:
- #简洁写法
- # product: /myProduct/**
- #排除某些路由
- ignored-patterns:
- - /**/product/listForOrder
- management:
- security:
- enabled: false
- 4.1.4Cookie
当 zuul 进行路由转发请求的时候, cookie 默认是不转发的, 所以需要配置 sensitiveHeaders 这个配置项. 为什么就说要改这个配置项就能成功呢? 我们可以通过源码来分析, 在 zuulProperties 源码中, 我们可以看到 private Set<String> sensitiveHeaders = new LinkedHashSet(Arrays.asList("Cookie", "Set-Cookie", "Authorization")); 这么一行代码, 也就是说请求头中的这些信息, zuul 是会全部过滤掉的.
- zuul:
- #全部服务忽略敏感头 (全部服务都可以传递 cookie)
- sensitive-headers:
- routes:
- #aaaa 的名字可以随便写, 但是 path 不要乱写
- aaaaaa:
- path: /myProduct/**
- serviceId: product
- sensitiveHeaders:
- 4.1.5 动态路由
- 动态路由需要两个配置来配合, 一个就是我上一篇文章写的 config, 还有一个就是我们需要一个配置项:
- @Component
- public class ZuulConfig {
- @ConfigurationProperties("zuul")
- @RefreshScope
- public ZuulProperties zuulProperties() {
- return new ZuulProperties();
- }
- }
- 过滤器典型应用场景有以下, 对于前置过滤有限流, 鉴权, 参数校验调整. 对于后置过滤器可以做统计与日志记录.
- 5.Zuul 的综合使用
- 先看一个图:
- 系统架构图
- 5.1 过滤器
- 我们先通过 Zuul 的 PreFilter 和 PostFilter 实现两个功能: 如果用户在访问后台微服务时没有提供 token 值, 我们则拒绝访问. 第二个功能时, 当我们处理完请求快要返回应答时, 在 response 中请求头中放入一个信息.
- 我们先看看 Prefilter 的实现:
- @Component
- public class TokenFilter extends ZuulFilter {
- @Override
- public String filterType() {
- return PRE_TYPE;
- }
- @Override
- public int filterOrder() {
- return PRE_DECORATION_FILTER_ORDER - 1;
- }
- @Override
- public boolean shouldFilter() {
- return true;
- }
- @Override
- public Object run() {
- RequestContext requestContext = RequestContext.getCurrentContext();
- HttpServletRequest request = requestContext.getRequest();
- // 这里从 url 参数里获取, 也可以从 cookie, header 里获取
- String token = request.getParameter("token");
- if (StringUtils.isEmpty(token)) {
- requestContext.setSendZuulResponse(false);
- requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
- }
- return null;
- }
- }
- 注意代码里面的 PRE_DECORATION_FILTER_ORDER, 这个 ORDER 是 Zuul 原生提供的, 也是我们在自定义 Filter 时, 官方比较建议使用的一个 ORDER. 在接下来看看 RUN 方法, 我们看到里面使用了 RequestContext 来获取 request 对象, 并从 request 中获取 token 值, 如果 token 为 null, 我们就会设置 ZuulResponse 为 false, 同时设置应答码为 401.
- 接下来我们看看 PostFilter 的代码:
- @Component
- public class addResponseHeaderFilter extends ZuulFilter{
- @Override
- public String filterType() {
- return POST_TYPE;
- }
- @Override
- public int filterOrder() {
- return SEND_RESPONSE_FILTER_ORDER - 1;
- }
- @Override
- public boolean shouldFilter() {
- return true;
- }
- @Override
- public Object run() {
- RequestContext requestContext = RequestContext.getCurrentContext();
- HttpServletResponse response = requestContext.getResponse();
- response.setHeader("X-Foo", UUID.randomUUID().toString());
- return null;
- }
- }
- 我们在请求头中放入了 X-Foo 的属性. 在 filterOrder 方法里面我们使用了另外一个官方比较推荐的 Order.
- 5.2 限流
- 限流应该在请求被转发之前调用, 而且可以说应该是所用调用中最前端的逻辑. 常用的限流算法就是令牌桶算法:
- 令牌桶算法
- 就是说我们往一个桶里面按照一定的速率放令牌, 如果桶满了, 就把多余的令牌丢掉, 当请求到达系统的时候, 如果能够从桶中获得令牌, 就让请求被执行, 同时同种减少一个令牌, 如果桶中没有令牌了, 那么就拒绝请求. 我们看一下在 Zuul 中如何实现.
- @Component
- public class RateLimitFilter extends ZuulFilter{
- private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);
- /**
- * to classify a filter by type. Standard types in Zuul are "pre" for pre-routing filtering,
- * "route" for routing to an origin, "post" for post-routing filters, "error" for error handling.
- * We also support a "static" type for static responses see StaticResponseFilter.
- * Any filterType made be created or added and run by calling FilterProcessor.runFilters(type)
- *
- * @return A String representing that type
- */
- @Override
- public String filterType() {
- return PRE_TYPE;
- }
- /**
- * filterOrder() must also be defined for a filter. Filters may have the same filterOrder if precedence is not
- * important for a filter. filterOrders do not need to be sequential.
- *
- * @return the int order of a filter
- */
- @Override
- public int filterOrder() {
- return SERVLET_DETECTION_FILTER_ORDER - 1;
- }
- /**
- * a "true" return from this method means that the run() method should be invoked
- *
- * @return true if the run() method should be invoked. false will not invoke the run() method
- */
- @Override
- public boolean shouldFilter() {
- return true;
- }
- /**
- * if shouldFilter() is true, this method will be invoked. this method is the core method of a ZuulFilter
- *
- * @return Some arbitrary artifact may be returned. Current implementation ignores it.
- */
- @Override
- public Object run() {
- if (!RATE_LIMITER.tryAcquire()) {
- throw new RateLimitException();
- }
- return null;
- }
- }
这里我们直接使用了 google 的令牌桶算法, 当然感兴趣的同学可以试试网上配合 Redis 等数据库来完成限流的方案, 这里我们就使用谷歌提供的算法. 我们的 filterOrder 使用了最高优先级的 order, 因为限流必须要在最前面使用. 同时我们每秒钟往桶里面放 100 个令牌, 每次来一个请求, 我们就拿走一个令牌.
5.3Zuul 鉴权和添加用户服务
我们假设这样一个场景, 对于 / order/create 请求只能买家访问,/order/finish 只能卖家访问,/product/list 都可访问, 如何让 Zuul 来做这样的鉴权? 也就是让 zuul 判断登录用户究竟是买家还是卖家, 我们可以看看下面的 AuthFilter:
- @Component
- public class AuthFilter extends ZuulFilter {
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
- @Override
- public String filterType() {
- return PRE_TYPE;
- }
- @Override
- public int filterOrder() {
- return PRE_DECORATION_FILTER_ORDER - 1;
- }
- @Override
- public boolean shouldFilter() {
- return true;
- }
- @Override
- public Object run() {
- RequestContext requestContext = RequestContext.getCurrentContext();
- HttpServletRequest request = requestContext.getRequest();
/**
*/order/create 只能买家访问 (cookie 里有 openid)
*/order/finish 只能卖家访问 (cookie 里有 token, 并且对应的 Redis 中值)
*/product/list 都可访问
- */
- if ("/order/order/create".equals(request.getRequestURI())) {
- Cookie cookie = CookieUtil.get(request, "openid");
- if (cookie == null || StringUtils.isEmpty(cookie.getValue())) {
- requestContext.setSendZuulResponse(false);
- requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
- }
- }
- if ("/order/order/finish".equals(request.getRequestURI())) {
- Cookie cookie = CookieUtil.get(request, "token");
- if (cookie == null
- || StringUtils.isEmpty(cookie.getValue())
- || StringUtils.isEmpty(stringRedisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_TEMPLATE, cookie.getValue())))) {
- requestContext.setSendZuulResponse(false);
- requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
- }
- }
- return null;
- }
- }
在 run 方法逻辑中我们判断了用户的请求路径, 做了相应的鉴权, 但是我们可以看到这其中存在问题, 如果以后我们要对买家的逻辑或者卖家的逻辑做更改, 怎么办? 都在这一个 Filter 里面修改吗? 不现实, 所以我们决定将这个 Filter 拆分.
对于卖家的 Filter:
- @Component
- public class AuthSellerFilter extends ZuulFilter {
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
- @Override
- public String filterType() {
- return PRE_TYPE;
- }
- @Override
- public int filterOrder() {
- return PRE_DECORATION_FILTER_ORDER - 1;
- }
- @Override
- public boolean shouldFilter() {
- RequestContext requestContext = RequestContext.getCurrentContext();
- HttpServletRequest request = requestContext.getRequest();
- if ("/order/order/finish".equals(request.getRequestURI())) {
- return true;
- }
- return false;
- }
- @Override
- public Object run() {
- RequestContext requestContext = RequestContext.getCurrentContext();
- HttpServletRequest request = requestContext.getRequest();
/**
*/order/finish 只能卖家访问 (cookie 里有 token, 并且对应的 Redis 中值)
- */
- Cookie cookie = CookieUtil.get(request, "token");
- if (cookie == null
- || StringUtils.isEmpty(cookie.getValue())
- || StringUtils.isEmpty(stringRedisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_TEMPLATE, cookie.getValue())))) {
- requestContext.setSendZuulResponse(false);
- requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
- }
- return null;
- }
- }
我们使用了 shouldFilter 对路径先做判断, 如果返回 true 再执行 run 方法. 再来看看买家的 Filter 逻辑:
- @Component
- public class AuthBuyerFilter extends ZuulFilter {
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
- @Override
- public String filterType() {
- return PRE_TYPE;
- }
- @Override
- public int filterOrder() {
- return PRE_DECORATION_FILTER_ORDER - 1;
- }
- @Override
- public boolean shouldFilter() {
- RequestContext requestContext = RequestContext.getCurrentContext();
- HttpServletRequest request = requestContext.getRequest();
- if ("/order/order/create".equals(request.getRequestURI())) {
- return true;
- }
- return false;
- }
- @Override
- public Object run() {
- RequestContext requestContext = RequestContext.getCurrentContext();
- HttpServletRequest request = requestContext.getRequest();
/**
*/order/create 只能买家访问 (cookie 里有 openid)
- */
- Cookie cookie = CookieUtil.get(request, "openid");
- if (cookie == null || StringUtils.isEmpty(cookie.getValue())) {
- requestContext.setSendZuulResponse(false);
- requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
- }
- return null;
- }
- }
这样我们就将鉴权的 Filter 拆分的更细更加合理
5.4 跨域
当我们做前后端分离的时候, 我们知道, 发起 Ajax 请求如果不是同源的话会引起跨域问题. 那么我们的 Zuul 在解决跨域问题的时候, 其实也就是在解决 Spring 的跨域问题, 有的同学认为我们可以在被调用的类或者方法上增加 @CrossOrigin 注解来解决, 但是我们的类和方法成千上万, 这样做会大大增加成本. 这里我们使用另外一个方法, 也就是在 Zuul 里面增加 CorsFilter 过滤器. 一个简单的配置类就可以搞定
- @Configuration
- public class CorsConfig {
- @Bean
- public CorsFilter corsFilter() {
- final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
- final CorsConfiguration config = new CorsConfiguration();
- config.setAllowCredentials(true);// 允许 coockie 跨域
- config.setAllowedOrigins(Arrays.asList("*")); //http:www.a.com
- config.setAllowedHeaders(Arrays.asList("*"));// 允许的请求头
- config.setAllowedMethods(Arrays.asList("*"));// 允许的请求方法
- config.setMaxAge(300l);// 多长时间内不用再检查是否跨域
- source.registerCorsConfiguration("/**", config);
- return new CorsFilter(source);
- }
- }
关于 Zuul 的知识我们就讲解到这里, 但是这里我还是要写一段相对比较悲观的话, Zuul 和 Eureka 一样也是不会再被维护的范畴了, 后续的文章中我们将会讲解 Spring-cloud-Gateway, 它是目前为止 Zuul 的完美替代
来源: http://www.jianshu.com/p/3e1b7a6cac5f