背景
通过之前的学习,我们知道注册中心Eureka,可以讲服务注册到该注册中心,Ribbon和Feign可以实现服务负载均衡地调用,Hystrix可以实现服务熔断,但我们还缺点什么?
微服务架构图:
- 外部调用方:浏览器、其他客户端
- 负载均衡:nginx
- OpenService:开放服务,服务消费者。集群部署
- ServiceA/B:内部服务,服务提供者。两个服务进行集群部署,每个服务有三个实例。
- 虚线框内服务调用可以使用Ribbon进行负载均衡访问,通过Eureka注册中心进行服务注册与订阅
现在还有一些问题需要解决:
- 如果我们的微服务中有很多独立的服务都要对外提供服务,那么我们要如何去管理这些接口?特别是当项目非常庞大的情况下要如何让管理?
- 在微服务中,一个独立的系统被拆分成很多个独立的服务,为了确保安全,权限管理也是一个不可回避的问题,如果在每个服务商都添加上相同的权限验证代码来确保系统不被非法访问,那么工作量也太大了,且维护也不方便。
为了解决上述问题,微服务架构中提出了API网关的概念,它就像一个安检站一样,所有的外部请求都需要经过它的调度与过滤,然后API网关来实现全球路由、负载均衡、权限验证等功能。
Spring Cloud这个一站式的微服务开发框架基于Netflix Zuul实现了Spring Cloud Zuul,采用了Spring Cloud Zuul即可实现一套基于API网关服务。
Zuul的作用
-
按照不同策略,将请求转发到不同的服务上去;
-
聚合API接口,统一对外暴露,提高系统的安全性;
-
实现请求统一的过滤,以及服务的熔断降级;
项目结构:
Zuul API网关
- 创建普通SpringBoot工程
- 添加依赖
spring-cloud-starter-netflix-eureka-client
和spring-cloud-starter-netflix-zuul
- 主入口激活
@EnableEurekaClient
和@EnableZuulProxy
- application.properties文件中配置路由规则
server.port=9001
#指定该服务的名字,该名称将在服务被调用时使用
spring.application.name=zuul-eureka-client-gateway
#Eureka配置:服务注册到哪里
eureka.client.service-url.defaultZone=http://eureka7001:7001/eureka
#Zuul配置:api_zuul 可任意写
#path:表示请求的拦截规则,以/api-zuul开头的任意目录以及子目录中所有请求都会被拦截
zuul.routes.api-zuul.path=/api-8001/**
#service-id:指定服务名字,用于对这个服务下的某个特定请求进行拦截
zuul.routes.api-zuul.serviceId=zuul-eureka-client-provider
- 通过
http://localhost:8001/api-zuul/test
可以访问端口号是8001的服务提供者zuul-eureka-client-provider的test方法(相当于是请求http://localhost:8001/test )。
Zuul请求过滤
- 定义一个过滤器并继承自ZuulFilter,并将该Filter作为一个Bean;
/**
* 自定义网关过滤器类并继承过滤器父类
*/
@Component
public class AuthFilter extends ZuulFilter {
//返回值决定当前过滤器的类型(执行时间)
@Override
public String filterType() {
//pre:前置过滤器,在执行转发(访问服务提供者)之前执行,通常用作身份认证
return "pre";
}
//过滤器序号:根据返回值大小决定执行的先后顺序,数字越小执行越先级越高
@Override
public int filterOrder() {
return 0;
}
//过滤器是否启动:true启动,false不启动
@Override
public boolean shouldFilter() {
return true;
}
//过滤器执行方法:返回值目前版本没有特殊作用,因此可写null
@Override
public Object run() throws ZuulException {
//获取当前请求上下文对象
RequestContext context = RequestContext.getCurrentContext();
//获取用户请求对象
HttpServletRequest request = context.getRequest();
//获取请求中的请求参数token(身份令牌用于请求身份验证)
String token = request.getParameter("token");
//验证身份有效性(实际应用中从数据库取数据进行验证)
if(token==null || !"123".equals(token)){
//设定false表示请求不继续执行(不转发给服务器)
context.setSendZuulResponse(false);
//设置响应码401表示权限不足也可设置500或其他编码
context.setResponseStatusCode(401);
//设置响应类型及编码格式
context.addZuulResponseHeader("context-type","text/html;charset=utf-8");
//设置响应内容
context.setResponseBody("非法请求");
}else{
System.out.println("请求合法继续执行请求准备进入服务或下一个过滤器");
}
return null;
}
}
-
合法请求:
http://localhost:8001/api-zuul/test?token=123
非法请求:http://localhost:8001/api-zuul/test
-
基于Zuul的这些过滤器,可以实现各种丰富的功能,而这些过滤器类型则对应于请求的典型生命周期。
-
PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
-
ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache
HttpClient或Netfilx Ribbon请求微服务。 -
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP
-
Header、收集统计信息和指标、将响应从微服务发送给客户端等。
-
ERROR:在其他阶段发生错误时执行该过滤器。
除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
-
-
Zuul中默认实现的Filter
Zuul路由规则
- 在前面的例子中:
#Zuul配置:api_zuul 可任意写
#path:表示请求的拦截规则,以/api-zuul开头的任意目录以及子目录中所有请求都会被拦截
zuul.routes.api-zuul.path=/api-zuul/**
#service-id:指定服务名字,用于对这个服务下的某个特定请求进行拦截
zuul.routes.api-zuul.serviceId=zuul-eureka-client-provider
#-----------------------------可以化简为---------------------------
zuul.routes.zuul-eureka-client-provider=/api-8001/**
- 其他配置:
# 忽略某些请求:访问出现404资源不存在 如果需要同时忽略多个请求可以使用逗号分隔 也可以使用通配符*和**
zuul.ignored-patterns=/api-8001/test3
# 配置统一网关路由前缀:http://localhost:8001/myapi/api-8001/test3?token=123
zuul.prefix=/myapi
- 通配符
# 可使用的通配符有: * ** ?
# ? 单个字符
# * 任意多个字符,不包含多级路径
# ** 任意多个字符,包含多级路径
- 一般情况下API网关只是作为各个微服务的统一入口,但有时候我们可能也需要在API网关服务上做一些特殊的业务逻辑处理,那么我们也可以让请求到达API网关后,仔转发给自己本身,由API网关自己来处理,那么我们可以进行如下操作:
@RestController
public class GateWayController {
@RequestMapping("/api/local")
public Object test(){
return "网关工程自己的控制器方法";
}
}
#配置自我转发,将某些请求转发到当前的网关请求
#http://localhost:8001/myapi/gateway
zuul.routes.gateway.path=/gateway/**
zuul.routes.gateway.url=forward:/api/local
Zuul异常处理
Spring Cloud Zuul对异常的处理非常的方便,从Zuul请求的生命周期图中可以看到,异常过滤器是对前置过滤、路由过滤、后置过滤过程中出现的异常(图中虚线框内)进行处理:
方式一、禁用zuul默认的异常处理SendErrorFilter过滤器,然后自定义我们自己的ErrorFilter过滤器
# 禁用默认异常拦截器,启用自定义异常拦截器
zuul.SendErrorFilter.error.disable=true
//自定义异常过滤器
@Component
public class MyErrorFilter extends ZuulFilter {
@Override
public String filterType() {
//标明异常过滤器:其他过滤器出现异常,自动执行当前过滤器
return "error";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException) context.getThrowable();
HttpServletResponse response = context.getResponse();
response.setStatus(exception.nStatusCode);
response.setContentType("test/html;charset=utf-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.println("出现异常了,异常码:"+exception.nStatusCode+" 异常信息:"+exception.getMessage());
}catch(IOException e){
e.printStackTrace();
}finally {
if(writer != null){
writer.close();
}
}
return null;
}
}
方式二、自定义全局异常页面
/**
* 自定义全局异常页面
* 注意:自定义全局异常页面和自定义异常过滤器有冲突,二选一即可
*/
@RestController
public class ErrorHandleController implements ErrorController {
@Override
public String getErrorPath() {
return "/error";
}
@RequestMapping("/error")
public Object error(){
RequestContext ctx = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException) ctx.getThrowable();
return exception.nStatusCode+"---"+exception.getMessage();
}
}