一、微服务架构为什么要有API网关?
在微服务中,一个独立的系统被拆分成了很多个独立的服务,这些独立的服务都要对外提供服务,我们该如何去管理这些接口,还有为了确保安全,权限管理也是一个不可回避的问题,如果在每一个服务上都添加上相同的权限验证代码来确保系统不被非法访问,那么工作量也就太大了,而且维护也非常不方便。为了解决上述问题,微服务架构中提出了 API 网关的概念
二、Spring Cloud Zuul是什么?
Spring Cloud 这个一站式的微服务开发框架基于 Netflix Zuul 实现了Spring Cloud Zuul,采用 Spring Cloud Zuul 即可实现一套 API 网关服务。所有外部的请求都需要经过它的调度与过滤, 然后 API 网关来实现请求路由、负载均衡、权限验证等功能
三、Spring Cloud Zuul的快速使用
注意:如果要使用 Zuul 进行请求过滤请参考第四,异常处理请参考第六
1.创建一个普通的 Spring Boot 工程名为 06-springcloud-api-gateway,然后添加下面依赖
<!--springboot的父级依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- 添加 spring cloud 的 zuul 的起步依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- 添加 spring cloud 的 eureka 的客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<!--依赖管理必须加,要不然spring-cloud-starter-netflix-eureka-server版本无法做到-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR10</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--在本地仓库找不到就到官网去找-->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
2、在入口类上添加@EnableZuulProxy 注解,开启 Zuul 的 API 网关服务功能:
@SpringBootApplication
@EnableZuulProxy // 开启 Zuul 的 API 网关服务功能
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}
3、在 application.properties 文件中配置路由规则
# 配置服务内嵌的 Tomcat 端口
server.port=8080
# 配置服务的名称
spring.application.name=06-springcloud-api-gateway
# 配置路由规则
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign
# 配置 API 网关到注册中心上, API 网关也将作为一个服务注册到 eureka-server 上
eureka.client.service-url.defaultZone=http://eureka8761:8761/eureka/,http://eureka8762:8762/eureka/
四、使用 Zuul 进行请求过滤
定义一个过滤器类并继承自 ZuulFilter,并将该 Filter 作为一个 Bean即可
package com.chang.springcloud.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class AuthFilter extends ZuulFilter {
/** pre:可以在请求被路由之前调用 FilterConstants.PRE_TYPE
route:在路由请求时候被调用
post:在route和error过滤器之后被调用 FilterConstants.POST_TYPE
error:处理请求时发生错误时被调用
**/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;//"pre"
}
//表示过滤器的执行顺序,当过滤器很多时,我们可以通过
//该方法的返回值来指定过滤器的执行顺序
@Override
public int filterOrder() {
优先级为0,数字越大,优先级越低
return 0;
}
//判断过滤器是否执行,true 表示执行,false 表示不执行。
@Override
public boolean shouldFilter() {
return true;
}
//表示过滤的具体逻辑,如果请求地址中携带了 token 参数的话,
//则认为是合法请求,否则为非法请求
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
if (token == null) {
//如果是非法请求的话,表示不对该请求进行路由,然后设置响应码和响应值。
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.addZuulResponseHeader("content-type","text/html;charset=utf-8");
ctx.setResponseBody(" 非法访问");
}
//返回值目前暂时没有任何意义,可以返回任意值
return null;
}
}
(2)有时候我们可能也需要在 API 网关服务上做一些特殊的业务逻辑处理,那么我们可以让请求到达 API 网关后,再转发给自己本身,由 API 网关自己来处理
1.在 API 网关服务上做一些特殊的业务逻辑处理
@RestController
public class GateWayController {
@RequestMapping("/api/local")
public String hello() {
return "exec the api gateway.";
}
}
2.然后在application.properties添加下面配置
#访问路径为http://localhost:8080/gateway?token=1234,会先过过滤器,然后直接到下面自己项目找
zuul.routes.gateway.path=/gateway/**
#会转发到自己项目进行处理
zuul.routes.gateway.url=forward:/api/local
五、Zuul 的路由规则
(1)api-wkcto可以随便写,但上下要一致,下面意思是路径要有/api-wkcto/
# 配置路由规则
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign
(2)可以将(1)进行简化
zuul.routes.05-springcloud-service-feign=/api-wkcto/**
(3)映射规则可以不写,会采用默认的规则,默认规则如下
# 默认的规则
zuul.routes.05-springcloud-service-feign.path=/05-springcloud-service-feign/**
zuul.routes.05-springcloud-service-feign.serviceId=05-springcloud-service-feign
由此,采用默认规则我们可以加上下面配置
# 忽略掉服务提供者的默认规则
zuul.ignored-services=01-springcloud-service-provider
# 忽略掉某一些接口路径
zuul.ignored-patterns=/**/hello/**
# 配置网关路由的前缀,访问是为 http://localhost:8080/myapi/web/hello
zuul.prefix=/myapi
下面说一下zuul的路由规则
通配符 | 含义 |
? | 匹配任意单个字符 |
** | 匹配任意路径数量的字符 |
* | 匹配一层路径的任意字符 |
六、Zuul 的 异常处理
我们可以有两种方式统一处理异常:
1、禁用 zuul 默认的异常处理 SendErrorFilter 过滤器,然后自定义我们自己的Errorfilter 过滤器
zuul.SendErrorFilter.error.disable=true
@Component
public class ErrorFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(ErrorFilter.class);
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
try {
RequestContext context = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException)context.getThrowable();
logger.error(" 进入系统异常拦截", exception);
HttpServletResponse response = context.getResponse();
response.setContentType("application/json; charset=utf8");
response.setStatus(exception.nStatusCode);
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.print("{code:"+ exception.nStatusCode +",message:\""+
exception.getMessage() +"\"}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if(writer!=null){
writer.close();
}
}
} catch (Exception var5) {
ReflectionUtils.rethrowRuntimeException(var5);
}
return null;
}
}
2、自定义全局 error 错误页面
@RestController
public class ErrorHandlerController 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();
}
}