如下图所示:
这样的架构,会存在着诸多的问题:
针对上面的这些问题,我们可以借助API网关来解决
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等
添加上API网关之后,系统的架构图变成了如下所示:
现在的整体架构应该是这样了:
由上图我们可以总结一下:
路由转发
接收外界请求,通过网关的路由转发,转发到后端的服务上
过滤器
对于我们来说比较常用的功能有鉴权、限流、路由、监控、日志记录等功能
在业界比较流行的网关,有下面这些
基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用
问题:
版本说明:
Zuul1.0和Zuul2.0官网地址:https://github.com/Netflix/zuul/wiki
Zuul1.0的生命周期:
Zuul2.0的生命周期:
两者相比,主要有两点区别:
两者的应用场景不同:
Zuul 1 (阻塞)的应用场景:
Zuul 2(非阻塞)的应用场景:
注意点:
目前版本:https://spring.io/projects/spring-cloud-gateway#learn,如下图所示:
优点:
缺点:
需求:通过浏览器访问api网关,然后通过网关将请求转发到用户服务
创建一个子模块,名字叫:springcloudalibaba-gateway-server-1040,它是一个微服务,然后pom.xml导入下面依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringCloudAlibabaDemoartifactId>
<groupId>cn.wujiangbogroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>springcloudalibaba-gateway-server-1040artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
dependencies>
project>
注意:gateway网关服务中不需要引入【spring-boot-starter-web】依赖,否则会报如下错误,因为【spring-cloud-starter-gateway】依赖中已经包含这个web包了:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'routeDefinitionRouteLocator' defined in class path resource [org/springframework/cloud/gateway/config/GatewayAutoConfiguration.class]: Unsatisfied dependency expressed through method 'routeDefinitionRouteLocator' parameter 4; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.convert.ConversionService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=webFluxConversionService)}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:769) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:509) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1320) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1159) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:847) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877) ~[spring-context-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:744) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:391) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1204) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at cn.wujiangbo.GatewayApp1030.main(GatewayApp1030.java:17) [classes/:na]
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.convert.ConversionService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=webFluxConversionService)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1662) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1221) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1175) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
... 19 common frames omitted
package cn.wujiangbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*
* @author 波波老师(微信 : javabobo0513)
*/
@SpringBootApplication
public class GatewayApp1040 {
public static void main(String[] args){
SpringApplication.run(GatewayApp1040.class, args);
}
}
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
uri: http://localhost:1010 # 请求要转发到的地址
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
UserController类如下:
package cn.wujiangbo.controller;
import cn.wujiangbo.dto.User;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户服务相关api接口
*
* @author 波波老师(微信 : javabobo0513)
*/
@RestController
@RequestMapping("/user")
public class UserController {
//获取配置文件中的值
@Value("${server.port}")
private String port;
@GetMapping("/getUserById/{id}")
//限流降级
@SentinelResource(value="getUserById", blockHandler="exceptionHandler", fallback = "getUserByIdFallback")
public User getUserById(@PathVariable Long id){
return new User(id,"王天霸", "我是王天霸,你好吗?port=" + port);
}
// 限流与阻塞处理 : 参数要和 被降级的方法参数一样
public User exceptionHandler(@PathVariable("userId") Long userId, BlockException ex) {
ex.printStackTrace();
return new User(-1L,"null","抱歉,Sentinel-限流");
}
// 熔断降级,参数和返回值与源方法一致
public User getUserByIdFallback(@PathVariable("userId") Long userId){
return new User(userId,"null", "抱歉,Sentinel-熔断");
}
}
启动用户服务和网关服务,浏览器访问网关服务:http://localhost:1040/user-server/user/getUserById/13,页面结果:
达到预期效果,测试成功
上面yml文件中我们看到了,uri的转发目标地址是硬编码写死的,那万一用户服务做了集群的话,这里就不好办了,这样很不优雅
所以接下来我们从注册中心获取此转发目标地址:
pom.xml添加nacos依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringCloudAlibabaDemoartifactId>
<groupId>cn.wujiangbogroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>springcloudalibaba-gateway-server-1040artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloud groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
dependencies>
project>
启动类上需要添加@EnableDiscoveryClient注解,如下:
package cn.wujiangbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 启动类
*
* @author 波波老师(微信 : javabobo0513)
*/
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApp1040 {
public static void main(String[] args){
SpringApplication.run(GatewayApp1040.class, args);
}
}
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
discovery:
locator:
enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
# uri: http://localhost:1010 # 请求要转发到的地址
uri: lb://user-server # lb指的是是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
启动用户服务和网关服务,浏览器访问网关服务:http://localhost:1040/user-server/user/getUserById/13,页面结果:
达到预期效果,测试成功
去掉关于路由的配置:
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
discovery:
locator:
enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
启动用户服务和网关服务,浏览器访问网关服务:http://localhost:1040/user-server/user/getUserById/13,页面结果:
达到预期效果,测试成功,只要按照网关地址/微服务/接口的格式去访问,就可以得到成功响应
路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体
主要定义了下面的几个信息:
执行流程大体如下:
路由断言工厂分为下面两种:
内置路由断言工厂具体有下面这些:
此类型的断言根据时间做判断,主要有三个:
案例:
# 当前的请求必须要在下方指定的时间之后
- After=2022-12-20T17:42:47.789-07:00[America/Denver]
# 当前的请求必须在下方指定的时间之前
- Before=2022-12-20T17:42:47.789-07:00[America/Denver]
# 当前的请求必须在下方指定的时间段之内
- Between=2022-12-20T17:42:47.789-07:00[America/Denver],2022-12-25T17:42:47.789-07:00[America/Denver]
这个时间格式是带区域的,以后如果忘记了可以使用ZonedDateTime.now()
来输出
RemoteAddrRoutePredicateFactory,接收一个IP地址段,判断请求主机地址是否在地址段中
案例:
-RemoteAddr=192.168.1.1/24
CookieRoutePredicateFactory,判断请求Cookie中必须某个key对应的value必须为指定的值,接收两个参数,cookie 名字和一个正则表达式
案例:
# cookie中 TestToken 的值必须为colin.wjb 第二个参数的值可以使用正则表达式
- Cookie=TestToken,colin.wjb
HeaderRoutePredicateFactory,接收两个参数,标题名称和正则表达式
判断请求Header是否具有给定名称且值与正则表达式匹配
案例:
#这个路由规则匹配Header中包含X-Request-Id并且值为纯数字的请求
-Header=X-Request-Id, \d+
HostRoutePredicateFactory,接收一个参数,主机名模式
判断请求的Host是否满足匹配规则
案例:
#这个路由规则匹配Header中必须包含.ybz.com
-Host=**.ybz.com
MethodRoutePredicateFactory,接收一个参数,判断请求类型是否跟指定的类型匹配
案例:
##限制请求方式为GET或者POST
-Method=GET,POST
PathRoutePredicateFactory,接收一个参数,判断请求的URI部分是否满足路径规则
案例:
# 当请求路径满足Path指定的规则时,才进行路由转发
- Path=/user-server/**
QueryRoutePredicateFactory ,接收两个参数,请求param和正则表达式
判断请求参数是否具有给定名称且值与正则表达式匹配
案例:
-Query=abc,def.
解读:abc请求参数名称,def. 是abc的值,是一个正则表达式,在正则表达式中点(.)表示匹配任意一个字符,所以当请求参数abc=defaaa或abc=defbbb能满足断言条件
WeightRoutePredicateFactory,接收一个[组名,权重],然后对于同一个组内的路由按照权重转发
案例:
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weight.high.org
predicates:
- Weight=group1,8 #80%
- id: weight_low
uri: https://weight.low.org
predicates:
- Weight=group1,2 #20%
解读:该路由会将约 80% 的流量转发到:weight.high.org,将约 20% 的流量转发到:weight.low.org
接下来我们验证几个内置断言的使用,yml改成这样:
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
# discovery:
# locator:
# enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
uri: http://127.0.0.1:1010
# uri: lb://user-server # lb指的是是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
# - Before=2021-11-28T00:00:00.000+08:00 #限制请求时间在 2022-11-28 之前
- Method=POST #限制请求方式为POST
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉第1层路径
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
logging:
level:
org.springframework.cloud.gateway: debug
特别坑的地方:
predicates下面用哪些断言工厂配置(比如Before、Method等)时,不能使用enabled:true的配置,否则会失效
好,我们开始测试
启动用户服务和网关服务,浏览器访问:http://localhost:1040/user-server/user/getUserById/13,页面结果:
为什么会报这个错呢?
控制台打印如下:
2022-10-20 15:19:51.034 DEBUG 13784 --- [ctor-http-nio-3] o.s.c.g.r.RouteDefinitionRouteLocator : RouteDefinition user_route applying {_genkey_0=/user-server/**} to Path
2022-10-20 15:19:51.035 DEBUG 13784 --- [ctor-http-nio-3] o.s.c.g.r.RouteDefinitionRouteLocator : RouteDefinition user_route applying {_genkey_0=POST} to Method
2022-10-20 15:19:51.035 DEBUG 13784 --- [ctor-http-nio-3] o.s.c.g.r.RouteDefinitionRouteLocator : RouteDefinition user_route applying filter {_genkey_0=1} to StripPrefix
2022-10-20 15:19:51.036 DEBUG 13784 --- [ctor-http-nio-3] o.s.c.g.r.RouteDefinitionRouteLocator : RouteDefinition matched: user_route
提示很明显,因为断言配置了:- Method=POST,含义是只有POST请求才会转发,其他请求不处理,通过浏览器直接访问,属于GET请求,所以不会转发
AbstractRoutePredicateFactory
类,重写apply()
方法的逻辑apply()
方法中可以通过exchange.getRequest()
拿到ServerHttpReqeust
对象,从而可以获取到请求参数、请求方式、请求头等信息编码注意事项:
RoutePredicateFactory
结尾AbstractRoutePredicateFactory
shortcutFieldOrder()
方法中,它会把yaml配置文件中写的值,映射到静态内部类Config中的属性需求是:前端请求时传参age字段的值,该值必须在我yml中设置的范围之中,否则就断言失败
先自定义一个断言工厂,实现断言方法
package cn.wujiangbo.predicates;
import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 自定义断言工厂
*
* @author 波波老师(微信 : javabobo0513)
*/
@Component
//泛型 用于接收一个配置类,配置类用于接收中配置文件中的配置
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//用于从配置文件中获取参数值,赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
//断言,如果返回true就是断言匹配成功,返回false就是匹配失败
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new Predicate<ServerWebExchange>(){
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//从serverWebExchange获取传入的参数
String ageString = serverWebExchange.getRequest().getQueryParams().getFirst("age");
if(StringUtils.hasLength(ageString)){
//如果前端传值的话
int age = Integer.parseInt(ageString);
//大于最小值且小于最大值,那就成功返回true
return age > config.getMinAge() && age < config.getMaxAge();
}
return true;
}
};
}
//创建一个静态内部类,成员变量的set/get方法必须要
@Data
public static class Config{
private int minAge;//最小年龄
private int maxAge;//最大年龄
}
}
然后在yml中设置age字段值的范围,如下:
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
# discovery:
# locator:
# enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
uri: http://127.0.0.1:1010
# uri: lb://user-server # lb指的是是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
# - Before=2022-11-28T00:00:00.000+08:00 #限制请求时间在 2022-11-28 之前
# - Method=POST,GET #限制请求方式为POST或者GET
- Age=18,30 # 限制年龄只有在18到30岁之间的人能访问
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉第1层路径
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
logging:
level:
org.springframework.cloud.gateway: debug
开始测试:
浏览器分别访问下面两个地址:
掌握下面三个知识点:
在Gateway中,Filter的生命周期只有两个:“pre” 和 “post”
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter
局部过滤器是针对单个路由的过滤器
官方文档,详细介绍了很多的过滤器工厂,这里就拿一些常用的记录
添加请求头
# 添加一个请求头X-Request-red,值为blue
- AddRequestHeader=X-Request-red, blue
微服务接口中获取请求头数据,代码如下:
@RequestMapping("/filter")
public String filter(@RequestHeader("X-Request-red") String red){
return red;
}
添加请求参数
# 添加一个请求参数red,值为blue
- AddRequestParameter=red, blue
微服务接口中获取请求头数据,代码如下:
@RequestMapping("/filter")
public String filter(@RequestParam("red") String red){
return red;
}
为匹配的路由统一添加前缀
# 添加前缀,对应微服务需要配置context-path
- PrefixPath=/colin
微服务的yaml配置文件中需要添加context-path
server:
port: 1010
servlet:
context-path: /colin
添加响应头
- AddResponseHeader=X-Response-Red, Blue
截断原始请求的路径
# 使用数字表示要截断路径的数量
- StripPrefix=2
重定向
# 重定向到百度
- RedirectTo=302, https://www.baidu.com/
在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器
具体如下:
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名及值 |
AddResponseHeader | 添加响应头 | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的header名称及去重策略 |
Hystrix | 为路由引入Hystrixd 的断路器保护 | HystrixCommand的名称 |
FallbackHeaders | 为fallbackUri的请求头添加具体的异常信息 | Header的名称 |
PrefixPath | 为原始的请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求天一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver rateLimiter statusCode denyEmptyKey emptyKeyStatus |
RedirectTo | 重定向到指定的URL | http状态码及重定向的URL |
RemoveRequestHeader | 删除某个请求头 | header名称 |
RemoveResponseHeader | 删除某个响应头 | header名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则 |
RewriteResponseHeader | 重写响应头 | Header名称、值的正则表达式、重写后的值 |
SaveSession | 在转发请求之前,强制执行WebSession::save 操作 | 无 |
SecureHeaders | 为原始响应添加一系列安全作用的响应头 | 无,支持修改这些安全响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetRequestHeader | 修改请求头 | Header的名称及值 |
SetResponseHeader | 修改响应头 | Header的名称及值 |
SetStatus | 修改响应状态码 | http状态码,可以是数字/字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断路径的数量 |
Retry | 针对不同的响应进行重试 | retries statuses methods series |
RequestSize | 设置允许接收最大请求包的大小。如果超过则返回413 | 请求包大小,单位为字节 默认为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
注意事项:
AbstractNameValueGatewayFilterFactory
抽象类GatewayFilterFactory
结尾我们来写一个试试,需求:灵活配置是否需要在过滤器中校验token
我们先来写一个自定义的Filter,如下:
package cn.wujiangbo.filter;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
/**
* 自定义过滤器工厂
*
* @author 波波老师(微信 : javabobo0513)
*/
@Component
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config> {
private static final String AUTHORIZE_TOKEN = "token";
public AuthorizeGatewayFilterFactory() {
super(Config.class);
}
//用于从配置文件中获取参数值,赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("tokenEnabled");
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter(){
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!config.tokenEnabled) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(AUTHORIZE_TOKEN);
if (token == null) {
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
if (StringUtils.isEmpty(token)) {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "token is Empty"));
}
/**
* 此时需要根据 token 查询Redis,看是否可以查到用户权限信息了
* 我这里做测试,没有引入Redis,所以模拟从Redis查询到数据了
*/
String authTokenFromRedis = "user:zhangsan,age=21";
if (StringUtils.isEmpty(authTokenFromRedis)) {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "token is error"));
}
//授权信息不为空,那就放行(真实项目中不会这么简单的,这里是测试)
return chain.filter(exchange);
}
};
}
//创建一个静态内部类,成员变量的set/get方法必须要
@Data
public static class Config {
// 控制是否开启认证
private Boolean tokenEnabled;
}
}
在application.yml配置使用:
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
# discovery:
# locator:
# enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
uri: http://127.0.0.1:1010
# uri: lb://user-server # lb指的是是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
# - Before=2022-11-28T00:00:00.000+08:00 #限制请求时间在 2022-11-28 之前
# - Method=POST,GET #限制请求方式为POST或者GET
- Age=18,30 # 限制年龄只有在18到30岁之间的人能访问
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉第1层路径
# 关键在下面一句,值为true则开启认证,false则不开启
- Authorize=true
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
logging:
level:
org.springframework.cloud.gateway: debug
开始测试,浏览器分别访问下面两个地址:
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验
开发中的鉴权逻辑如下:
下图是大概流程:
如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验
检验的标准就是请求中是否携带token凭证以及token的正确性
下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”,则不转发路由,否则执行正常的逻辑,代码如下:
package cn.wujiangbo.filter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 自定义全局过滤器
*
* @author 波波老师(微信 : javabobo0513)
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判断逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst("token");
if (StringUtils.isBlank(token)) {
System.out.println("鉴权失败");
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication Failed"));
}
//调用chain.filter继续向下游执行
return chain.filter(exchange);
}
//顺序,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
开始测试:
浏览器访问下面地址:
用postman测试,在请求头中传一个token的话,就可以正常访问了,如下:
前后端分离的项目,前端访问后台服务肯定是有跨域的问题的,SpringCloud Gateway提供了处理跨域的功能
通过yml配置文件的方式:
spring:
cloud:
gateway:
# 跨域的配置
globalcors:
cors-configurations:
# /**代表允许跨域访问的资源
'[/**]':
# 下面就是跨域允许的来源,在开发环境就可以直接设置一个*表示所有都可以进行访问
# 一旦开启了跨域,我们只会针对已知的来源允许跨域,可以写一个域名
allowedOrigins: "*"
# 设置允许的请求方式
allowedMethods:
- GET
- POST
通过配置类的方式:
package cn.wujiangbo.config;
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;
/**
* 跨域处理
*
* @author 波波老师(微信 : javabobo0513)
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter(){
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*"); // 允许的请求方式 POST、GET
config.addAllowedOrigin("*"); // 允许的来源
config.addAllowedHeader("*"); // 允许的请求头参数
// 允许访问的资源
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流
网关服务导入下面依赖:
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-spring-cloud-gateway-adapterartifactId>
dependency>
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可
代码如下:
package cn.wujiangbo.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* 限流配置类
*
* @author 波波老师(微信 : javabobo0513)
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
//初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
/**
* 下面设置含义:
* 2秒内最多允许3个请求访问 user_route 服务
* 请求超过了这个阈值的话,就返回下面 initBlockHandlers方法 定义的异常信息
*/
new GatewayFlowRule("user_route") //资源名称,对应路由id
.setCount(3) // 限流阈值
.setIntervalSec(2) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler
sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange
serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", "-1");
map.put("message", "接口被限流了(网关限流)");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
启动用户服务和网关服务,页面访问:http://localhost:1040/user-server/user/getUserById/13,第一次肯定是可以正常返回信息的,刷新频率快一点,保证2秒内请求次数多余3次就会出现下面提示: