为什么需要微服务网关
不同的微服务一般有不同的网络地址,而外部的客户端可能需要调用多个服务的接口才能完成一个业务需求。比如一个电影购票的手机APP,可能会调用电影分类微服务,用户微服务,支付微服务等。如果客户端直接和微服务进行通信,会存在以下问题:
- 客户端会多次请求不同地址的微服务,增加客户端的复杂性
- 存在跨域请求,在一定场景下处理相对复杂
- 认证复杂,每一个服务都需要独立认证
- 难以重构,随着项目的迭代,可能需要重新划分微服务,如果客户端直接和微服务通信,那么重构会难以实施
- 某些微服务可能使用了其他协议,直接访问有一定困难
上述问题,都可以借助微服务网关解决。微服务网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过微服务网关。
什么是 Zuul
Zuul 是 Netflix 开源的微服务网关,他可以和 Eureka,Ribbon,Hystrix 等组件配合使用。Zuul 组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求
- 审查与监控
- 动态路由:动态将请求路由到不同后端集群
- 压力测试:逐渐增加指向集群的流量,以了解性能
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理:边缘位置进行响应,避免转发到内部集群
- 多区域弹性:跨域 AWS Region 进行请求路由,旨在实现 ELB(ElasticLoad Balancing) 使用多样化
使用 Zuul 后,架构图演变成如下形式:
Zuul 路由转发
1)创建网关微服务,pom 文件引入依赖
<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>
2)创建 application.yml
server:
port: 9012
spring:
application:
name: tensquare-web
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka/
instance:
prefer-ip-address: true
zuul:
routes:
tensquare-base:
path: /base/**
serviceId: tensquare-base
tensquare-qa:
path: /qa/**
serviceId: tensquare-qa
3)编写启动类
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class);
}
}
Zuul过滤器
为什么需要 Zuul 过滤器呢
因为我们一旦用了网关,所有的请求都会先经过网关,然后由网关路由到具体的微服务,但是 header 头信息在经过一次请求之后就会消失,这样经过网关之后,微服务就获取不到头信息了,所以过滤器可以帮助我们转发头信息,还可以统一鉴权。
Zuul 过滤器快速入门
1)前台网关的 token 转发
一个过滤器类,继承 ZuulFilter 类
@Component
public class WebFilter extends ZuulFilter {
/**
* 判断过滤器是前置(pre)还是后置(post)
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器的执行顺序,数字越小,越先执行
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 此过滤器是否开启
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器真正要执行的动作
* @return 返回任何对象都放行
* 停止运行:request.setSendZuulResponse(false)
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
System.out.println("Web过滤器执行了。。。");
// 获取request上下文
RequestContext requestContext = RequestContext.getCurrentContext();
// 获取 request 对象
HttpServletRequest request = requestContext.getRequest();
// 获取 header
String header = request.getHeader("Authorization");
// 转发
requestContext.addZuulRequestHeader("Authorization",header);
return null;
}
}
各方法详解:
filterType
:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
pre
:可以在请求被路由之前调用route
:在路由请求时候被调用post
:在route和error过滤器之后被调用error
:处理请求时发生错误时被调用
filterOrder
:通过int值来定义过滤器的执行顺序
shouldFilter
:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回true,所以该过滤器总是生效
run
:过滤器的具体逻辑。
2)后台网关的 token 校验并转发
因为是管理后台使用,所以需要在过滤器中对 token 进行验证。
① 修改配置文件
#以下为自定义内容
jwt:
config:
key: itcast
2)编写过滤器类
@Component
public class ManagerFilter extends ZuulFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("ManagerFilter执行了。。。");
// 获取 request 上下文
RequestContext currentContext = RequestContext.getCurrentContext();
// 获取request对象
HttpServletRequest request = currentContext.getRequest();
/**
* header头信息在经过一次请求之后就会消失,而通过过滤器会经过两个方法, OPTIONS 方法正是第一个要执行的方法,
* OPTIONS 方法是判断该往哪个微服务转发,如果不放行那么 header 头信息就会消失
*/
if (request.getMethod().equals("OPTIONS")) {
return null;
}
// 如果是登录页面也放行,因为登录页面不需要权限
if (request.getRequestURL().indexOf("login") > 0) {
return null;
}
// 获取头信息
String header = request.getHeader("Authorization");
if (header != null && !"".equals(header)) {
if (header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
Claims claims = jwtUtil.parseJWT(token);
if (claims != null) {
if (claims.get("roles").equals("admin")) {
currentContext.addZuulRequestHeader("Authorization",header);
return null;
}
}
} catch (Exception e) {
e.printStackTrace();
// 停止运行
currentContext.setSendZuulResponse(false);
}
}
}
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(403);
currentContext.setResponseBody("权限不足");
currentContext.getResponse().setContentType("text/html;charset=UTF-8");
return null;
}
}
需要注意:run
方法返回任何对象都放行,这里我们通过 currentContext.setSendZuulResponse(false)
令 zuul 过滤该请求,不对其进行路由,然后通过 currentContext.setResponseStatusCode(403)
设置了其返回的错误码