经过以前几篇Spring Cloud中几个核心组件的介绍,咱们已经能够构建一个简略的(不够完善)微服务架构了。好比下图所示: 愿意了解源码的朋友直接求求交流分享技术 一零三八七七四六二六前端
在该架构中,咱们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,经过均衡负载公开至服务调用方。本文咱们把焦点汇集在对外服务这块,这样的实现是否合理,或者是否有更好的实现方式呢?web
先来讲说这样架构须要作的一些事儿以及存在的不足:spring
首先,破坏了服务无状态特色。为了保证对外服务的安全性,咱们须要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特色。从具体开发和测试的角度来讲,在工做中除了要考虑实际的业务逻辑以外,还须要额外可续对接口访问的控制处理。 其次,没法直接复用既有接口。当咱们须要对一个即有的集群内访问接口,实现外部服务访问时,咱们不得不经过在原有接口上增长校验逻辑,或增长一个代理调用来实现权限控制,没法直接复用原有的接口。 面对相似上面的问题,咱们要如何解决呢?下面进入本文的正题:服务网关!api
为了解决上面这些问题,咱们须要将权限控制这样的东西从咱们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,咱们须要一个更强大一些的均衡负载器,它就是本文未来介绍的:服务网关。安全
服务网关是微服务架构中一个不可或缺的部分。经过服务网关统一贯外系统提供REST API的过程当中,除了具有服务路由、均衡负载功能以外,它还具有了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的做用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体可以具有更高的可复用性和可测试性。bash
下面咱们经过实例例子来使用一下Zuul来做为服务的路有功能。架构
准备工做app
在使用Zuul以前,咱们先构建一个服务注册中心、以及两个简单的服务,好比:我构建了一个service-A,一个service-B。而后启动eureka-server和这两个服务。经过访问eureka-server,咱们能够看到service-A和service-B已经注册到了服务中心。 ide
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
复制代码
应用主类使用@EnableZuulProxy注解开启Zuul函数
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}
复制代码
这里用了@SpringCloudApplication注解,以前没有提过,经过源码咱们看到,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker,主要目的仍是简化配置。这几个注解的具体做用这里就不作详细介绍了,以前的文章已经都介绍过。
application.properties中配置Zuul应用的基础信息,如:应用名、服务端口等。
spring.application.name=api-gateway
server.port=5555
复制代码
Zuul配置 完成上面的工做后,Zuul已经能够运行了,可是如何让它为咱们的微服务集群服务,还须要咱们另行配置,下面详细的介绍一些经常使用配置内容。
服务路由 经过服务路由的功能,咱们在对外提供服务的时候,只须要经过暴露Zuul中配置的调用地址就可让调用方统一的来访问咱们的服务,而不须要了解具体提供服务的主机信息了。
在Zuul中提供了两种映射方式:
经过url直接映射,咱们能够以下配置:
# routes to url
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:2222/
复制代码
该配置,定义了,全部到Zuul的中规则为:/api-a-url/**的访问都映射到http://localhost:2222/上,也就是说当咱们访问http://localhost:5555/api-a-url/add?a=1&b=2的时候,Zuul会将该请求路由到:http://localhost:2222/add?a=1&b=2上。
其中,配置属性zuul.routes.api-a-url.path中的api-a-url部分为路由的名字,能够任意定义,可是一组映射关系的path和url要相同,下面讲serviceId时候也是如此。
经过url映射的方式对于Zuul来讲,并非特别友好,Zuul须要知道咱们全部为服务的地址,才能完成全部的映射配置。而实际上,咱们在实现微服务架构时,服务名与服务实例地址的关系在eureka server中已经存在了,因此只须要将Zuul注册到eureka server上去发现其余服务,咱们就能够实现对serviceId的映射。例如,咱们能够以下配置:
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
复制代码
针对咱们在准备工做中实现的两个微服务service-A和service-B,定义了两个路由api-a和api-b来分别映射。另外为了让Zuul能发现service-A和service-B,也加入了eureka的配置。
接下来,咱们将eureka-server、service-A、service-B以及这里用Zuul实现的服务网关启动起来,在eureka-server的控制页面中,咱们能够看到分别注册了service-A、service-B以及api-gateway
http://localhost:5555/api-a/add?a=1&b=2: 经过serviceId映射访问service-A中的add服务 http://localhost:5555/api-b/add?a=1&b=2: 经过serviceId映射访问service-B中的add服务 http://localhost:5555/api-a-url/add?a=1&b=2: 经过url映射访问service-A中的add服务
推荐使用serviceId的映射方式,除了对Zuul维护上更加友好以外,serviceId映射方式还支持了断路器,对于服务故障的状况下,能够有效的防止故障蔓延到服务网关上而影响整个系统的对外服务
服务过滤 在完成了服务路由以后,咱们对外开放服务还须要一些安全措施来保护客户端只能访问它应该访问到的资源。因此咱们须要利用Zuul的过滤器来实现咱们对外服务的安全控制。
在服务网关中定义过滤器只须要继承ZuulFilter抽象类实现其定义的四个抽象函数就可对请求进行拦截与过滤。
好比下面的例子,定义了一个Zuul过滤器,实现了在请求被路由以前检查请求中是否有accessToken参数,如有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
Object accessToken = request.getParameter("accessToken");
if(accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
}
log.info("access token ok");
return null;
}
}
复制代码
自定义过滤器的实现,须要继承ZuulFilter,须要重写实现下面四个方法:
filterType:返回一个字符串表明过滤器的类型,在zuul中定义了四种不一样生命周期的过滤器类型,具体以下:
pre:能够在请求被路由以前调用
routing:在路由请求时候被调用
post:在routing和error过滤器以后被调用
error:处理请求时发生错误时被调用
filterOrder:经过int值来定义过滤器的执行顺序
shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,因此经过此函数可实现过滤器的开关。在上例中,咱们直接返回true,因此该过滤器老是生效。
run:过滤器的具体逻辑。须要注意,这里咱们经过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,而后经过ctx.setResponseStatusCode(401)设置了其返回的错误码,固然咱们也能够进一步优化咱们的返回,好比,经过ctx.setResponseBody(body)对返回body内容进行编辑等。
在实现了自定义过滤器以后,还须要实例化该过滤器才能生效,咱们只须要在应用主类中增长以下内容:
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
复制代码
启动该服务网关后,访问:
http://localhost:5555/api-a/add?a=1&b=2: 返回401错误
http://localhost:5555/api-a/add?a=1&b=2&accessToken=token: 正确路由到server-A,并返回计算内容
最后,总结一下为何服务网关是微服务架构的重要部分,是咱们必需要去作的缘由:
不只仅实现了路由功能来屏蔽诸多服务细节,更实现了服务级别、均衡负载的路由。 实现了接口权限校验与微服务业务逻辑的解耦。经过服务网关中的过滤器,在各生命周期中去校验请求的内容,将本来在对外服务层作的校验前移,保证了微服务的无状态性,同时下降了微服务的测试难度,让服务自己更集中关注业务逻辑的处理。 实现了断路器,不会由于具体微服务的故障而致使服务网关的阻塞,依然能够对外服务。资料源码来源