什么是网关
网关是微服务最边缘的服务,直接暴露给用户,用来做用户和微服务的桥梁
我们一般的微服务模块如下图所示:
当没有网关时:
- 我们需要将所有的微服务模块都在客户端中进行配置,缺点是当微服务的ip或者端口发生改变时,我们需要手动修改客户端的代码,并重启客户端服务
- 当某个微服务的并发量很大时,通常会进行分布式部署,进而需要实现负载均衡,如果不使用网关,则无法完成负载均衡的效果(这当然也不是绝对的,例如可以通过nginx进行负载均衡,这不在本文的讨论范围)
当使用网关时(通常需要结合注册中心完成,如Nacos,Eureka等):
- 客户端可以直接请求网关,然后由网关来访问相关的微服务
- 直接使用服务名称即可访问微服务,可以实现负载均衡
- 可以对一些统一性的服务进行划分,如:Token拦截,权限验证以及限流等操作,都可以通过网关进行统一操作,实现业务解耦,避免在每个服务中进行验证,提高系统性能
SpringCloud Gateway
什么是Gateway
Gateway的核心是一组过滤器,这些过滤器按照一定的先后顺序来执行过滤操作
三大核心概念
1.Filter过滤器
SpringCloud Gateway中的Filter主要分为两种类型,分别是Gateway Filter和Global Filter。这些Filter将会对请求和响应进行相应的修改和处理等。
- Gateway Filter:针对某一个路由(路径)的过滤器,如:对某个接口做限流操作,就可以通过该Filter实现
- Global Filter:针对全局的过滤器,如:实现全局的Token拦截,ip黑名单的处理
这里以全局过滤器为例,实现全局的Token拦截,限流操作
实现全局Token拦截功能
通常我们开发的大部分web系统都需要登录功能,在分布式中如果每个微服务都需要验证用户身份,那么将会极大地降低系统性能,重复性代码也会很高。然而在我们使用Gateway网关后,所有的请求都会经过Gateway,那么我们就可以将登陆验证的操作放在Gateway进行,如此一来,就可以有效地避免前面提到的问题。
因为我们要对所有的请求都进行token拦截验证,所以需要使用Gateway的Global Filter进行实现。
然后我们新建TokenCheckFilter.java
过滤器类,具体代码如下:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
* token校验
* 使用@Component注解将该过滤器注入到Spring容器中
*/
@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {
// 指定放行的路径列表 如果是这些路径则不检验token直接放行
public static final List<String> ALLOW_URL = Arrays.asList("/login-service/login", "/otherUrl", "/login");
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 步骤:
* 1.拿到请求的url
* 2.判断是否放行
* 3.拿到请求头
* 4.拿到token
* 5.校验
* 6.放行/拦截
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 获取请求的路径
String path = request.getURI().getPath();
// 如果请求的路径在放行路径列表中则直接放行,不做校验
if (ALLOW_URL.contains(path)){
return chain.filter(exchange);
}
// 否则,对token进行校验
HttpHeaders headers = request.getHeaders();
List<String> authorization = headers.get("Authorization");
if (!CollectionUtils.isEmpty(authorization)){
String token = authorization.get(0);
String real_token = token.replaceFirst("bearer ", "");
if (StringUtils.hasText(real_token) && redisTemplate.hasKey(real_token)){
return chain.filter(exchange);
}
}
// 如果token校验未通过,则返回相应的状态码
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("content-type", "application/json;charset=utf-8");
// 手动封装响应数据
HashMap<String, Object> map = new HashMap<>(4);
map.put("code", HttpStatus.UNAUTHORIZED.value());
map.put("msg", "未授权");
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes = new byte[0];
try {
bytes = objectMapper.writeValueAsBytes(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer wrap = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
}
@Override
public int getOrder() {
return 0;
}
}
借助redis实现限流功能
2. route路由
Gateway提供了两种实现路由的方式:(1)通过代码方式实现路由配置 (2)通过配置文件方式实现路由配置
(1)代码方式实现路由
通过官方给的Demo可以知道,只需要自定义一个Route的配置类并将其作为Bean组件注入到Springboot应用中即可。
根据官方提供的Demo,我们可以实现自定义的路由配置,一个简单的例子如下:
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RouteConfig {
/**
* 代码方式实现路由,和yml配置的路由不冲突
* @param builder
* @return
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("dologin_route", r -> r.path("/login")
.uri("http://localhost:8080"))
// 可以配置多个路由
// .route("host_route", r -> r.host("*.myhost.org")
// .uri("http://httpbin.org"))
.build();
}
}
然后我们就可以通过访问http://localhost:82/login实现对http://localhost:8080/login的访问,从而实现路由的转发
(2)配置文件方式实现路由
单接口路由转发配置如下:
server:
port: 81
# 应用名称
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: login-service-route # 路由id保持唯一即可
uri: http://localhost:8080
predicates: # 断言
- Path=/login # 匹配规则 只要路径中包含/login 就往http://localhost:8080转发 并且将路径带上
- id: login-service-route2 # 路由id保持唯一即可
uri: http://localhost:8082
predicates: # 断言
- Path=/service2 # 匹配规则 只要路径中包含/service2 就往http://localhost:8080转发 并且将路径带上
然而当某个uri中有多个服务时,我们不可能给每个路径都配置一个route规则,那么此时就可以使用如下配置进行多个路径接口的转发
server:
port: 81
# 应用名称
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
# 动态路由 当一个uri下有多个服务时使用
- id: login-service-route # 路由id保持唯一即可
uri: http://localhost:8080
predicates: # 断言
- Path=/login-service/** # 匹配规则 只要路径中包含/login-service/ 就往http://localhost:8080转发 并且将路径带上
此外,我们更多的一种情况是:某个微服务有多台服务器,那么我们就需要实现负载均衡的效果,这时我们就需要用到动态路由
(3)动态路由
如果要实现动态路由以及负载均衡,就需要使用注册发现中心,如:Nacos,Eureka等。
server:
port: 81
spring:
application:
name: gateway-nacos
cloud:
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
gateway:
routes:
- id: login-service-route # 路由id保持唯一即可
uri: lb://login-service # 负载均衡 login-service是nacos中注册的服务名称
predicates: # 断言
- Path=/login # 匹配规则 只要路径匹配导航了 就往uri转发 并且将路径带上
discovery:
locator:
enabled: true # 开启动态路由
3.Predicates断言
断言的意思就是一个Boolean表达式,如满足条件则执行相应的操作
SpringCloud Gateway提供了12个断言工厂,其中10个常用的断言如下:
- 基于Datetime类型的断言工厂,包括:
- AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期
- BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期
- BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内
- 基于Cookie的断言工厂:CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式,判断请求cookie是否具有给定名称且值与正则表达式匹配
- 基于Header的断言工厂:HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式,判断请求Header是否具有给定名称且值与正则表达式匹配
- 基于Host的断言工厂:HostRoutePredicateFactory:接收一个参数(一个host name数组),主机名模式,判断请求的Host是否满足匹配规则
- 基于Method请求方法的断言工厂:MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配
- 基于Path请求路径的断言工厂:PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则
- 基于Query请求参数的断言工厂:QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配
- 基于Remote的断言工厂:RemoteAddrRoutePredicateFactory:接收一组参数(至少一个参数),判断请求中的主机地址是否在给定的地址段中
- 基于路由权重的断言工厂:WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
举个栗子:
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
# 路由id保持唯一即可
- id: login-service-route
uri: http://localhost:8080
predicates:
# 请求路径 只要路径匹配导航了 就往uri转发 并且将路径带上
- Path=/loginService/**
# 请求方式匹配上了才允许访问接口
- Method=GET,POST
# 在给定时间之后允许访问接口
- After=2022-12-15T20:47:07.470+08:00[Asia/Shanghai]
# 在给定时间之前允许访问接口
- Before=2022-12-15T21:47:07.470+08:00[Asia/Shanghai]
# 在给定的时间段内允许访问接口
- Between=2022-12-15T21:47:07.470+08:00[Asia/Shanghai], 2022-12-15T22:47:07.470+08:00[Asia/Shanghai]
# 判断请求携带的cookie中是否具有给定名称的键,且值与正则表达式匹配
- Cookie=user_info, zhangsan.
# 判断请求的header中是否具有给定名称的键,且值与正则表达式匹配
- Header=X-Request-Id, \d+
# 判断请求的host是否满足匹配规则
- Host=**.somehost.org,**.anotherhost.org
# 判断请求中的主机地址是否在给定的地址段中
- RemoteAddr=192.168.1.1/24
# 判断请求携带的参数中是否具有给定名称的键,且值与正则表达式匹配
- Query=name, admin.
# 如果和其他路由属于同一个组,则按照响应的权重进行转发
- Weight=group1, 2
- id: login-service-route2
uri: http://localhost:8080
predicates:
- Path=/loginService/**
- Weight=group1, 8
评论区