我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载;通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。本文我们把焦点聚集在对外服务这块,这样的实现是否合理,或者是否有更好的实现方式呢?
先来说说这样架构需要做的一些事儿以及存在的不足:
- 首先,破坏了服务无状态特点。为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外可续对接口访问的控制处理。
- 其次,无法直接复用既有接口。当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
面对类似上面的问题,我们要如何解决呢?下面进入本文的正题:服务网关!
为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器,它就是本文将来介绍的:服务网关。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
准备工作:我们先启动注册中心,然后注册两个服务,一个Service A和Service B,这时候打开注册中心http://localhost:1111/
我们会看到:
然后开始写我们的服务网关:
新建一个springBoot项目,pom.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.sun</groupId> <artifactId>api-gateway</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <name>api-gateway</name> <description>Spring Cloud project</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <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> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Brixton.RELEASE</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> |
启动类上加上@EnableZuulProxy
注解开启Zuul,
并且把我们的服务过滤器加载进来,后期我们要用过滤器来拦截进行业务操作。
application.properties:
配置文件中除了基本的配置外,需要将我们的服务注册到注册中心去发现其他服务,这样我们就可以我们就可以实现对serviceId的映射,如果是通过指定serviceId的方式,eureka依赖是必须要的。我们也可以用url的方式去映射,但这样不够透明友好。
该配置定义的规则是:当我们访问http://localhost:5555/api-a/add?a=1&b=2
的时候,Zuul会将该请求路由到:http://localhost:2222/add?a=1&b=2
上。当我们访问http://localhost:5555/api-b/add?a=1&b=2
的时候,Zuul会将该请求路由到:http://localhost:3333/add?a=1&b=2
上。
服务过滤器:
过滤器只需要继承ZuulFilter实现其四个抽象函数就可以对所有请求进行拦截和过滤,书写业务逻辑。
如下我们拦截检查请求中是否有accesstoken,若有就进行路由,若没有就拒绝访问,返回401错误。
package com.didispace.filter; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; public class AccessFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(AccessFilter.class); /*** * filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下: * pre:可以在请求被路由之前调用 * routing:在路由请求时候被调用 * post:在routing和error过滤器之后被调用 * error:处理请求时发生错误时被调用 */ @Override public String filterType() { return "pre"; } //通过int值来定义过滤器的执行顺序 @Override public int filterOrder() { return 0; } //返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关.true为开false为关。 @Override public boolean shouldFilter() { return true; } /*** * 过滤器的具体逻辑。需要注意,这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求, * 不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以 * 进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。 */ @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); log.info("省缺accessToken已拦截"); return null; } log.info("access token ok"); return null; } } |
启动服务后我们访问
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,并返回计算内容
还有很多其他的过滤类型就不一一展开了。
filterType
生命周期介绍:
Springcloud核心组件:
参考自:码云程序猿DD。感谢他的分享!