上面的架构,会存在着诸多的问题:
客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
认证复杂,每个服务都需要独立认证。
存在跨域请求,在一定场景下处理相对复杂。
网关可以做什么?
- 路由转发。
- 身份认证。
- 统一跨域解决。
- 黑白名单ip
- 敏感词
- 限流
nginx:它可以当网关
zuul:早期的微服务就是使用的该组件作为网关,但是它的底层使用的servlet。它的效率非常慢。而且它是netflix的产品。 netflix预计产品zuul2, 但是zuul2夭折。
springcloud gateway:它是spring公司出品的网关。它的效率是zuul的1.6倍。
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
其实网关 它也是一个微服务,那么我们也可以创建网关微服务。
引入spring-cloud-starter-gateway
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
dependencies>
(2)创建主启动类
@SpringBootApplication
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class,args);
}
}
(3)修改配置文件
# 配置路由
spring:
cloud:
gateway:
routes:
- id: shop-product #路由的唯一标识。如果没有给定默认按照UUID生成
uri: http://localhost:8001 #真实转发的地址
predicates: # 断言 如果断言满足要求,则转发到uri指定的真实地址.
- Path=/product/** # 如果客户的请求路径以product开头,则满足该断言要求,则转发的uri真实地址。
- id: shop-order
uri: http://localhost:9001
predicates:
- Path=/order/**
(4)启动gateway
(5)演示
上面配置文件有没有需要改进的?
- 我们真实转发的地址,万一搭建是一个集群。 我们观察到gateway本身也是一个微服务,是否可以从注册中心拉取相关的微服务,然后访问该服务呢。
(1)引入nacos注册中心的依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
(2)修改配置文件
测试:
# 配置路由
spring:
cloud:
gateway:
routes:
- id: shop-product #路由的唯一标识。如果没有给定默认按照UUID生成
uri: lb://shop-product #真实转发的地址 lb: ---loadbalanced
predicates: # 断言 如果断言满足要求,则转发到uri指定的真实地址.
- Path=/product/** # 如果客户的请求路径以product开头,则满足该断言要求,则转发的uri真实地址。
- id: shop-order
uri: lb://shop-order
predicates:
- Path=/order/**
思考: 如果这时增加新的微服务, 需要修改网关的路由配置。
改为自动路由发现。
(1)修改gateway的配置文件
(2)访问网关
l 基于Datetime类型的断言工厂
此类型的断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
l 基于远程地址的断言工厂
RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
-RemoteAddr=192.168.1.1/24
l 基于Cookie的断言工厂
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求
cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
l 基于Header的断言工厂
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否
具有给定名称且值与正则表达式匹配。 key value
-Header=X-Request-Id, \d+
l 基于Host的断言工厂
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
l 基于Method请求方法的断言工厂
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
l 基于Path请求路径的断言工厂
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}基于Query请求参数的断言工厂
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具
有给定名称且值与正则表达式匹配。
-Query=baz, ba.
l 基于路由权重的断言工厂
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
-id: weight_route1 uri: host1 predicates:
-Path=/product/**
-Weight=group3, 1
-id: weight_route2 uri: host2 predicates:
-Path=/product/**
-Weight= group3, 9
如果上面的内置断言无法满足需求 可以自定义断言。【了解】
案例: 年龄必须在18~65之间才能访问我指定的微服务。
自定义断言类
package com.aaa.predicate;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import javax.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* @program: qy156-shop-parent
* @description:
* @author: 闫克起2
* @create: 2022-11-21 16:27
**/
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("minAge", "maxAge");
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return (serverWebExchange)->{
ServerHttpRequest request = serverWebExchange.getRequest();
//获取传递的年龄
String age = request.getHeaders().getFirst("age");
if(StringUtils.hasText(age)){
int a = Integer.parseInt(age);
if(a>=config.getMinAge()&&a<=config.getMaxAge()){
return true;
}
}
return false;
};
}
@Validated
public static class Config {
@NotNull
private int minAge;
@NotNull
private int maxAge;
public int getMinAge() {
return minAge;
}
public void setMinAge(int minAge) {
this.minAge = minAge;
}
public int getMaxAge() {
return maxAge;
}
public void setMaxAge(int maxAge) {
this.maxAge = maxAge;
}
}
}
小结:
gateway:网关,路由转发ribbon: 实现负载均衡
openfeign: 完成服务之间的调用。
nacos: 注册中心
为请求到达微服务前可以添加相应的请求设置, 响应后为响应结果添加一些设置。
gateway内部含有很多种过滤。
https://www.cnblogs.com/zhaoxiangjun/p/13042189.html
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
Hystrix | 为路由引入Hystrix的断路器保护HystrixCommand的名称 | |
FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url |
RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | 默认就会启用,可以通过配置指定仅删除哪些Header |
RemoveRequestHeader | 为原始请求删除某个Header | Header名称 |
RemoveResponseHeader | 为原始响应删除某个Header | Header名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达式 |
RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正则表达式,重写后的值 |
SaveSession | 在转发请求之前,强制执行WebSession::save操作 | 无 |
secureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 |
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、methods、series |
RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large | 请求包大小,单位为字节,默认值为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
Default | 为所有路由添加过滤器 | 过滤器工厂名称及值 |
Tips:每个过滤器工厂都对应一个实现类,并且这些类的名称必须以GatewayFilterFactory结尾,这是Spring Cloud Gateway的一个约定,例如AddRequestHeader对应的实现类为AddRequestHeaderGatewayFilterFactory。
举例: StripPrefix 用于截断原始请求的路径。
测试:
例子: 设置响应的状态码2500
例子: 认证过滤。
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的认证校验。
开发中的鉴权逻辑:
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
认证通过,将用户信息进行加密形成token[jwt],返回给客户端,作为登录凭证
以后每次请求,客户端都携带认证的token [携带请求头]
服务端对token进行解密,判断是否有效。
package com.aaa.filter;
import com.alibaba.fastjson.JSON;
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.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.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* @program: qy156-shop-parent
* @description:
* @author: 闫克起2
* @create: 2022-11-22 15:07
**/
@Component
public class LoginFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//判断请求路径是否为放行。
String path = request.getPath().toString();
if("/login".equals(path)){
return chain.filter(exchange);//放行
}
//获取请求头的token值。
String token = request.getHeaders().getFirst("token");
if(StringUtils.hasText(token)){
//校验token是否有效
if("admin".equals(token)){
return chain.filter(exchange);//放行
}
}
//3.1设置状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//3.2封装返回数据
Map<String, Object> map = new HashMap<>();
map.put("msg", "未登录");
map.put("code", "NOTLOGING");
//3.3作JSON转换
byte[] bytes = JSON.toJSONString(map).getBytes(StandardCharsets.UTF_8);
//3.4调用bufferFactory方法,生成DataBuffer对象
DataBuffer buffer = response.bufferFactory().wrap(bytes);
//4.调用Mono中的just方法,返回要写给前端的JSON数据
return response.writeWith(Mono.just(buffer));
}
//优先级 值越小优先级越高
@Override
public int getOrder() {
return 0;
}
}
第一种通过配置文件
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
default-filters:
- DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
第二种写一个配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}