网关Zuul
十、网关Zuul的使用
1.网关的作用
“网关”在计算机网络的概念里面,是用来实现不同网段之间的区分。192.168.2.10和192.168.3.11这两台电脑处于两个网段的,相当于是两个局域网。于是这两台电脑所处的网段就可以用相应的网关来表示:192.168.2.1网关和192.168.3.1网关。——这是基于255.255.255.0的子网掩码。如果子网掩码是255.255.0.0,这个时候网段就是192.168.1.1
在微服务中,网关的作用就更加的重要了:路由、限流、降级、安全控制、服务聚合。其中 限流、降级、安全控制都属于服务治理。
2.常见的网关
-
zuul:推荐使用zuul,性能和功能足够的好
-
gateway: springcloud使用的,可以使用
-
nginx:也可以做网关,功能没有zuul强大
-
自研的网关
3. zuul的路由转发功能
通过创建一个独立的服务:网关服务,来实现网关的功能
1)引入zuul的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
2)在启动类上开启zuul. @EnableZuulProxy
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class MyZuulApplication {
public static void main(String[] args) {
SpringApplication.run(MyZuulApplication.class, args);
}
}
3)编写配置文件:主要配的是路由表
spring:
application:
name: zuul-gateway
server:
port: 8765
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
#zuul的路由表的配置
zuul:
routes:
api-product: # 可以自己定义一个路由项目的名称
path: /api/product/** # 要进行路由拦截的路径
serviceId: PRODUCT-FEIGN-SERVICE
api-user:
path: /api/user/**
serviceId: USER-SERVICE
4.路由的细节
- 统一前缀和剥离前缀
#zuul的路由表的配置
zuul:
routes:
api-product: # 可以自己定义一个路由项目的名称
path: /product/** # 要进行路由拦截的路径
serviceId: PRODUCT-FEIGN-SERVICE
api-user:
path: /user/**
serviceId: USER-SERVICE
# 默认情况是true,如果说设置成了false,那么/user这个路径在USER-SERVICE的接口中是必须存在的
stripPrefix: false
# 统一前缀
prefix: /api
- 保护敏感路径
希望路由时一些敏感路径不被路由
#zuul的路由表的配置
zuul:
routes:
api-product: # 可以自己定义一个路由项目的名称
path: /product/** # 要进行路由拦截的路径
serviceId: PRODUCT-FEIGN-SERVICE
api-user:
path: /user/**
serviceId: USER-SERVICE
# 默认情况是true,如果说设置成了false,那么/user这个路径在USER-SERVICE的接口中是必须存在的
stripPrefix: false
# 统一前缀
prefix: /api
# 此时下游服务中带/admin/的接口将访问不到
ignored-patterns: /**/admin/**
- 携带cookie
private Set<String> sensitiveHeaders = new LinkedHashSet(Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
从zuul的源码中看出zuul默认情况下会把cookie作为敏感头数据,不会传递cookie
所以需要做如下设置,给sensitive-headers赋一个空值,这样cookie就不会被当作敏感头,于是cookie就可以被传递。
#zuul的路由表的配置
zuul:
routes:
api-product: # 可以自己定义一个路由项目的名称
path: /product/** # 要进行路由拦截的路径
serviceId: PRODUCT-FEIGN-SERVICE
api-user:
path: /user/**
serviceId: USER-SERVICE
# 默认情况是true,如果说设置成了false,那么/user这个路径在USER-SERVICE的接口中是必须存在的
stripPrefix: true
# 统一前缀
prefix: /api
# 此时下游服务中带/admin/的接口将访问不到
ignored-patterns: /**/admin/**
# 敏感头:zuul会把一些请求头中的数据作为敏感的请求头数据,不被转发。
sensitive-headers:
- 网关的超时设置
因为zuul也是用ribbon进行通信,超时设置通过设置ribbon即可
spring:
application:
name: zuul-gateway
server:
port: 8765
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
#zuul的路由表的配置
zuul:
routes:
api-product: # 可以自己定义一个路由项目的名称
path: /product/** # 要进行路由拦截的路径
serviceId: PRODUCT-FEIGN-SERVICE
api-user:
path: /user/**
serviceId: USER-SERVICE
# 默认情况是true,如果说设置成了false,那么/user这个路径在USER-SERVICE的接口中是必须存在的
stripPrefix: true
# 统一前缀
prefix: /api
# 此时下游服务中带/admin/的接口将访问不到
ignored-patterns: /**/admin/**
# 敏感头:zuul会把一些请求头中的数据作为敏感的请求头数据,不被转发。
sensitive-headers:
# 该strip-prefix是针对于外部的prefix进行作用的
# strip-prefix: false
# 设置请求的超时时间为10s
ribbon:
ReadTimeout: 10000
ConnectTimeout: 10000
4.zuul网关的错误回调如何实现
通过编写FallbackProvider接口的实现类,来明确对哪个服务,或者所有服务进行错误回调设置。
package com.qf.my.zuul.fallback;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@Component
public class ProductZuulFallback implements FallbackProvider {
/**
* return null: 开启对所有的路由进行错误回调
* return "PRODUCT-FEIGN-SERVICE": 开启对PRODUCT-FEIGN-SERVICE服务的错误回调
* @return
*/
@Override
public String getRoute() {
return "PRODUCT-FEIGN-SERVICE";
}
/**
* 具体发生错误时回调的内容——设置响应消息
* @param route
* @param cause
* @return
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
/**
* 如果下游出现错误,调用zuul的上游应该拿到一个错误提示,但是这一次调用不应该是失败的。
* @return
* @throws IOException
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;//表示响应成功
}
/**
* 返回响应的状态码
* @return
* @throws IOException
*/
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
/**
* 响应行中的具体的文本,描述响应行的状态的文本
* @return
* @throws IOException
*/
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.getReasonPhrase();
}
@Override
public void close() {
}
/**
* 设置响应消息体,返回json数据
* @return
* @throws IOException
*/
@Override
public InputStream getBody() throws IOException {
//使用jackson来封装响应体
ObjectMapper objectMapper = new ObjectMapper();
Map<String,Object> map = new HashMap<>();
map.put("code",1000);
map.put("message","请检查你的网络");
//把数据写成json字符串
String json = objectMapper.writeValueAsString(map);
//把字符串转换成byte[]数组 写入到inputStream中
return new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
}
/**
* 设置响应消息头: Content-type: application/json
* @return
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
5.zuul的过滤器的实现
1)过滤器的分类
zuul中共有4个过滤器
- pre:在开始路由之前触发的过滤器
- routing: 在进行路由之时触发的过滤器
- post: 在完成路由以后触发的过滤器
- error: 在上述过滤器执行过程中出现了异常将会执行errorfilter
2)过滤器如何实现
编写ZuulFilter的子类
package com.qf.my.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@Component
public class MyZuulFilter extends ZuulFilter {
/**
* zuul过滤器有四个类型:
* pre
* routing
* post
* error
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 同一种过滤器类型的先后顺序
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 是否执行过滤
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 具体怎么过滤?
* 模拟用户是否已登陆,如果没有登陆则不允许访问。如果已登陆,则放行
* jwt token=>redis中获取用户信息=》zuul得知当前用户已登陆,可以放心/如果没拿到,那就不放行
* Authorization: Bearer eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB
* Cookie login_token
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//1.获取请求头中的数据-》获取request对象
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
//2.从request对象中获取Cookie 的数据
Cookie[] cookies = request.getCookies();
String loginToken = null;
if(cookies!=null){
for (Cookie cookie : cookies) {
if("login_token".equals(cookie.getName())){
loginToken = cookie.getValue();
break;
}
}
}
if(StringUtils.isNotBlank(loginToken)){
//拿到loginToken :去redis验证一下。如果ok: 放行
//放行
context.setSendZuulResponse(true);//false 不放行
context.setResponseStatusCode(200);
return null;
}
//不放行
context.setSendZuulResponse(false);
context.setResponseStatusCode(413);//权限不够
return null;
}
}
6.使用zuul网关实现后端服务的限流
使用redis来记录登陆次数
package com.qf.my.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* 计数器限流
*/
@Component
public class CountZuulFilter extends ZuulFilter {
@Autowired
private RedisTemplate redisTemplate;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
/**
* 维护一个计数:数值存放在哪里?——redis里面 zuul肯定要搭建集群的。
*
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
//1.获取redis中的访问数值
String countKey = "zuul:count";
/*
redis的非原子操作: 出现安全问题。
*/
/* int count = (int) redisTemplate.opsForValue().get(countKey);
//================
if(count>=10000){
//限流
}else{
//放行。然后让count+1
redisTemplate.opsForValue().set(countKey,count+1);
}*/
//原子操作(每访问一次就记录下来增加一次)
Long count = redisTemplate.opsForValue().increment(countKey);
if (count > 10) {
//限流
//不放行
context.setSendZuulResponse(false);
context.setResponseStatusCode(200);
context.setResponseBody("当前流量过大,请稍后重试");
return null;
}
//放行
context.setSendZuulResponse(true);
context.setResponseStatusCode(200);
return null;
}
}