赞
踩
spring cloud netflix zuul、spring cloud gateway是最常见的微服务网关,通过网关,我们可以在请求到达后端指定服务之前/后端服务处理完业务响应数据之后对响应进行对请求/响应进行处理。
比如常见的参数校验、接口鉴权等等,在后端服务的拦截器和过滤器能做的事在网关都可以做。
网关的主要功能是请求的转发以及负载均衡,和nginx的功能类似,只是底层实现不同。
这篇文章就详细介绍一下spring cloud gateway的使用,包括了各种断言及过滤器的相关配置,帮助初学者更好的了解gateway的使用。
目录
在讲解之前,先搭建起一个网关服务,通过Springboot整合spring cloud gateway搭建一个微服务网关的案例。
创建一个springboot项目,并命名为springboot-gateway
- <?xml version="1.0" encoding="UTF-8"?>
- <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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.2.6.RELEASE</version>
- <relativePath />
- </parent>
-
- <groupId>cn.edu.sgu.www</groupId>
- <artifactId>springboot-gateway</artifactId>
- <version>0.0.1-SNAPSHOT</version>
-
- <properties>
- <java.version>1.8</java.version>
- <lombok.version>1.18.22</lombok.version>
- <spring-cloud.version>2.2.6.RELEASE</spring-cloud.version>
- </properties>
-
- <dependencies>
- <!--lombok-->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>${lombok.version}</version>
- </dependency>
-
- <!--gateway网关-->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-gateway</artifactId>
- <version>${spring-cloud.version}</version>
- </dependency>
-
- <!--网关负载均衡-->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-loadbalancer</artifactId>
- <version>${spring-cloud.version}</version>
- </dependency>
-
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
参数说明:- StripPrefix=2表示删除前面两级路径(如:/api/mhxysy),比如http://localhost:9091/api/mhxysy/user/login对应服务器端的地址为http://localhost:8080/user/login
server: port: 9091 spring: application: name: gateway cloud: gateway: enabled: true routes: - id: mhxysy uri: http://localhost:8080 predicates: - Path=/api/mhxysy/** filters: - StripPrefix=2 logging: file: name: D:/log/gateway.log level: springfox: error cn.edu.sgu.www.gateway: debug
这基本上是最简单的配置,配置了服务名为gateway,然后配置了一个路由,是在idea里启动的一个后端项目。
在学习gateway的配置之前,需要了解gateway中的路由这个概念,路由一般又路由ID、目标URL、一组断言和一组过滤器组成。
比如上面我们配置的一个路由,routes中可以配置多个路由。
- spring:
- cloud:
- gateway:
- routes:
- - id: mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- filters:
- - StripPrefix=2
其中uri称为目标地址,也就是我们需要转发请求到uri后面配置的路径,当我们访问ip地址:端口号/api/mhxysy/**时,请求将会被转发到http://localhost:8080/**
断言,其实就是一组条件,熟悉java.util.function.Predicate的应该对这个概念有一定的了解,其实就是设置了一些条件,通过matches()方法的返回值来判断是否满足设置的条件。
gateway里的断言predicates指的是路由断言工厂,在predicates里可以配置各种条件,只有全部条件都满足,请求才能被转发。
接下来介绍gateway里都有哪些断言工厂。
配置在指定时间之后才能转发请求,后面指定的值的格式和LocalDateTime很像,只是多了毫秒和时区信息。
2023-09-08T08:31:59.789+08:00[Asia/Shanghai]
前半部分:2023-09-08T08:31:59这是一个LocalDateTime的字符串格式,789表示的是789毫秒,1秒=1000毫秒。
后半部分:+08:00[Asia/Shanghai]表示东八区的亚洲/上海。
- 东时区用+表示,如+6:00表示东6区
- 西时区用-表示,如-5:00表示西5区
spring: application: name: gateway cloud: gateway: enabled: true routes: - id: gateway-mhxysy uri: http://localhost:8080 predicates: - Path=/api/mhxysy/** - After=2023-09-08T08:31:59.789+08:00[Asia/Shanghai] filters: - StripPrefix=2 server: port: 9091
比如当前是2023年9月8日早上8:30分,此时访问服务的/chongwu/selectAll接口返回404
过了那个时间再访问,成功返回查询结果
限制在指定时间之前才能访问,比如上面的配置改成Before就访问不到了,因为已经过了那个时间点
spring: application: name: gateway cloud: gateway: enabled: true routes: - id: gateway-mhxysy uri: http://localhost:8080 predicates: - Path=/api/mhxysy/** - Before=2023-09-08T08:31:59.789+08:00[Asia/Shanghai] filters: - StripPrefix=2 server: port: 9091
配置只能在指定时间段访问,上面的时间配置后移一天,依旧能得到返回结果。
spring: application: name: gateway cloud: gateway: enabled: true routes: - id: gateway-mhxysy uri: http://localhost:8080 predicates: - Path=/api/mhxysy/** - Between=2023-09-08T08:31:59.789+08:00[Asia/Shanghai],2023-09-09T08:31:59.789+08:00[Asia/Shanghai] filters: - StripPrefix=2 server: port: 9091
限制只有请求中携带了指定的cookie才能访问,比如配置需要携带MHXYSY_JSESSIONID。
因为在mhxysy这个服务的配置文件中设置了session的名称为MHXYSY_JSESSIONID,默认是JSESSIONID,登录之后,浏览器每次请求都会携带MHXYSY_JSESSIONID的cookie。
- package cn.edu.sgu.www.mhxysy.config;
-
- import org.springframework.boot.web.servlet.ServletContextInitializer;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
- import javax.servlet.ServletContext;
-
- /**
- * springmvc配置类
- * @author heyunlin
- * @version 1.0
- */
- @Configuration
- public class SpringMvcConfig implements WebMvcConfigurer {
-
- /**
- * 设置SESSION_ID
- * @return ServletContextInitializer
- */
- @Bean
- public ServletContextInitializer servletContextInitializer() {
- return new ServletContextInitializer() {
- @Override
- public void onStartup(ServletContext servletContext) {
- servletContext.getSessionCookieConfig().setName("MHXYSY_JSESSIONID");
- }
- };
- }
-
- }
session值通过浏览器中查看得到
然后配置cookie
spring: application: name: gateway cloud: gateway: enabled: true routes: - id: gateway-mhxysy uri: http://localhost:8080 predicates: - Path=/api/mhxysy/** - Cookie=MHXYSY_JSESSIONID,831B175D25150131A3F3017116369CAE filters: - StripPrefix=2 server: port: 9091
把上面的配置中的cookie名称改为默认的JSESSIONID之后,请求返回404。
只有携带指定请求头的请求会被转发,在这里配置需要携带token的请求头
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- - Header=token,mhxy1218
- filters:
- - StripPrefix=2
- server:
- port: 9091
为了方便测试,使用postman来测试效果
不携带请求头时
携带请求头,正常响应,这里返回401,是因为没有通过接口鉴权,浏览器登录了,所以没有返回401
这个是指定域名访问,如果不是指定的域名,将访问失败,这个不好测试,就跳过了。
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- - Host=taobao.com
- filters:
- - StripPrefix=2
- server:
- port: 9091
配置请求方式,只有规定的请求方式才会被转发。
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- - Method=POST
- filters:
- - StripPrefix=2
- server:
- port: 9091
这时候通过get请求访问会404(post请求返回401是因为没有登录,鉴权失败了)。
post
get
这是基础的断言,只有将指定的路径转发到目标URL,本篇文章的http://localhost:9091/api/mhxysy/chongwu/selectAll会被转发到http://localhost:8080/chongwu/selectAll
限制需要携带指定参数的请求才能正常转发
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- - Query=acess,true
- filters:
- - StripPrefix=2
- server:
- port: 9091
上面配置了要携带参数?acess=true
未携带参数时
RemoteAddr用于指定IP地址,只有配置的IP地址才能访问。
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- - RemoteAddr=176.176.4.127
- filters:
- - StripPrefix=2
- server:
- port: 9091
上面配置了本机的IP地址,所以通过localhost访问时返回404
这个用于配置权重,同一个分组中,权重配置的越大,请求时被选择访问的概率越高。这个不方便演示,跳过。
网关的过滤器,网关的处理流程由一组过滤器组成的过滤器链组成,是责任链设计模式的典型应用。
为了演示过滤器效果,在mhxysy的服务添加一个切面类,通过日志在控制台打印请求信息。
- package cn.edu.sgu.www.mhxysy.aop;
-
- import cn.edu.sgu.www.mhxysy.util.UserUtils;
- import lombok.extern.slf4j.Slf4j;
- import org.aspectj.lang.JoinPoint;
- import org.aspectj.lang.annotation.After;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Before;
- import org.aspectj.lang.annotation.Pointcut;
- import org.springframework.stereotype.Component;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Enumeration;
-
- @Slf4j
- @Aspect
- @Component
- public class AuthenticationAop {
-
- @Pointcut("execution(public * cn.edu.sgu.www.mhxysy.controller..*(..))")
- public void requestAspect(){}
-
- @Before(value = "requestAspect()")
- public void before(JoinPoint joinPoint) throws Throwable {
-
- synchronized (AuthenticationAop.class) {
- log.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~进入方法AuthenticationAop.before()");
-
- HttpServletRequest request = UserUtils.getRequest();
- String requestURI = request.getRequestURI();
-
- //打印请求信息
- log.debug("request_header:{}", request.getHeader("request_header"));
- log.info("请求ip:{}", request.getRemoteAddr());
- log.info("请求地址:{}", requestURI);
- log.info("请求方式:{}", request.getMethod());
- log.info("请求类方法:{}", joinPoint.getSignature());
- log.info("请求类方法参数:{}", Arrays.toString(joinPoint.getArgs()));
-
- log.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~AuthenticationAop.before()执行完成");
- }
- }
-
- @After(value = "requestAspect()")
- public void after() throws Throwable {
- HttpServletResponse response = UserUtils.getResponse();
-
- log.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~AuthenticationAop.after()开始执行");
-
- // 得到所有响应头的名称
- Collection<String> headerNames = response.getHeaderNames();
-
- for (String headerName : headerNames) {
- String header = response.getHeader(headerName);
-
- log.debug("响应头 => {}:{}", headerName, header);
- }
-
- log.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~AuthenticationAop.after()执行完成");
- }
-
- }
接下来介绍gateway里的几种常用的过滤器,因为过滤器实在是太多了,只讲解4种,其他的过滤器使用类似,感兴趣的可以通过文章末尾的spring cloud gateway链接进一步学习。
这个过滤器的作用是在请求转发之前为当前请求添加请求头。
然后给请求添加一个请求头
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- filters:
- - StripPrefix=2
- - AddRequestHeader=request_header, mhxy1218
- server:
- port: 9091
通过postman发起一次请求,mhxysy服务后台成功打印出请求头request_header的值
这个过滤器的作用是给请求添加参数,相当于在请求后面添加?参数名=参数值。
因为之前的方法没有定义参数,这里改一个接口,这次用/chongwu/selectById这个接口。
如下图,给请求添加参数?id=CW20230727095358
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- filters:
- - StripPrefix=2
- # - AddRequestParameter=id, CW20230727095358
- server:
- port: 9091
没有添加id参数时,因为ID查询条件通过@RequestParam注解设置为必填,会发生异常。
取消上面的- AddRequestParameter=id, CW20230727095358注释之后,成功查询到了指定ID的宠物数据
这个过滤器的作用是,服务端返回请求之后,在网关返回数据给客户端之前为响应添加响应头。
在这里添加一个响应头name,值为heyunlin
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- filters:
- - StripPrefix=2
- - AddResponseHeader=name, heyunlin
- server:
- port: 9091
然后我们观察一下效果,返回的响应中确实有一个自定义的响应头name
这个过滤器的作用是删除响应头,例如上面的请求中,总是会返回一个响应头Date,在这里配置移除这个响应头。
- spring:
- application:
- name: gateway
- cloud:
- gateway:
- enabled: true
- routes:
- - id: gateway-mhxysy
- uri: http://localhost:8080
- predicates:
- - Path=/api/mhxysy/**
- filters:
- - StripPrefix=2
- - RemoveResponseHeader=Date
- server:
- port: 9091
在postman里查看响应信息,确实已经没有了Date这个响应头。
项目的前端封装了ajax请求,在每个请求的路径前加上网关里配置的对应路由,控制通过每个请求都走网关。
- let base = "http://localhost:9091/api/mhxysy";
-
- /**
- * 封装的ajax get请求
- * @param url 请求url
- * @param params 请求参数
- * @param success 成功回调函数
- * @param error 失败回调函数
- */
- function get(url, params, success, error) {
- $.ajax({
- type: "GET",
- url: base + url,
- data: params,
- cache: false,
- async: true,
- dataType: 'json',
- processData: true,
- success: success,
- error: error
- });
- }
-
- /**
- * 封装的ajax post请求
- * @param url 请求url
- * @param params 请求参数
- * @param success 成功回调函数
- * @param error 失败回调函数
- */
- function post(url, params, success, error) {
- $.ajax({
- type: "POST",
- url: base + url,
- data: params,
- async: true,
- cache: false,
- dataType: 'json',
- processData: true,
- success: success,
- error: error
- });
- }
-
- let error = (res) => {
- console.log(res);
-
- if (res && res.responseJSON) {
- let response = res.responseJSON;
-
- if (res.status && res.status === 404) {
- $.messager.alert("提示", "路径" + response.path + "不存在。", "error");
- } else {
- $.messager.alert("提示", response.message, "error");
- }
- }
- }
-
- /**
- * 文件上传
- * @param url 上传路径
- * @param data 提交数据
- * @param success 成功回调
- * @param error 失败回调
- */
- function ajaxUpload(url, data, success, error) {
- $.ajax({
- url: base + url,
- data: data,
- cache: false,
- async: true,
- type: "POST",
- dataType: 'json',
- processData: false,
- contentType: false,
- success: success,
- error: error
- });
- }
启动gateway和mhxysy两个服务,在测试的控制器接口上加上自定义的匿名注解跳过鉴权(因为存在跨域session丢失问题,暂时没有解决)
- @RestController
- @Api(value = "宠物控制器类", tags = "宠物控制器类")
- @RequestMapping(path = "/chongwu", produces = "application/json; charset=utf-8")
- public class ChongwuController {
- private final ChongwuService service;
-
- @Autowired
- public ChongwuController(ChongwuService service) {
- this.service = service;
- }
-
- /**
- * 查询全部宠物
- * @return List<Chongwu>
- */
- @AnonymityAccess
- @ApiOperation("通过ID修改宠物信息")
- @RequestMapping(path = "/selectAll", method = RequestMethod.GET)
- public List<Chongwu> selectAll() {
- return service.selectAll();
- }
-
- }
浏览器访问如下地址,成功返回查询结果
localhost:9091/api/mhxysy/chongwu/selectAll
访问以上地址,请求会被转发到 localhost:8080/chongwu/selectAll ,如图
通过网关转发请求
直接访问
如果像上面一样,每个应用都要写ip:端口号,也太麻烦了。gateway支持动态路由,通过配置动态路由,可以实现负载均衡的功能,只需要把上面的配置稍微修改一下。
server: port: 9091 spring: application: name: gateway cloud: gateway: enabled: true # 开启自动路由 discovery: locator: enabled: true routes: - id: gateway-mhxysy uri: lb://mhxysy predicates: - Path=/api/mhxysy/** filters: - StripPrefix=2 logging: level: springfox: error cn.edu.sgu.www.gateway: debug
上面的配置文件对比前面的配置文件,其实就多了一个开启动态路由的配置
- # 开启自动路由
- discovery:
- locator:
- enabled: true
然后断言改成了以下格式,其中lb表示load balance(负载均衡),后面加上我们注册到注册中心的服务名,也就是服务提供者的的spring.application.name配置的值
lb://服务名称
好了,文章就分享到这里了,代码已开源,可按需获取,看完不要忘了点赞+收藏哦~
gateway网关服务项目https://gitee.com/he-yunlin/springboot-gateway.git
如果想要更深入学习gateway,可以访问Spring官网Spring Cloud Gateway
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。