一. 步骤
- 需求,现在有一个服务需要使用 Gateway 网关服务实现转发访问
- 创建目标服务,将目标服务注册到注册中心
- 此处以 Eureka 为注册中心,需要创建 Eureka 注册中心服务
- 创建 Gateway 网关服务,配置网关服务拦截目标服务路由,将网关服务注册到注册中心
- 根据需求在网关服务中创建GatewayFilter 过滤器,实现拦截过滤功能
二. 目标服务
- 目标服务 yml 配置文件,以指定服务名称,注册到 Eureka 注册中心
server:
port: 8001 #当前服务端口号
spring:
application:
name: cloud-payment-service #当前服务名称
datasource:
type: com.alibaba.druid.pool.DruidDataSource #当前数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/test01?serverTimezone=GMT%2B8 #连接的库
username: root
password:
#=================eureka相关配置======================
eureka:
client:
register-with-eureka: true #true 表示将当前服务注册到eureka注册中心
#true 表示是否在注册中心抓取已有的注册信息,集群环境时必须为true,配合ribbon进行负载
fetchRegistry: true
service-url:
#eureka 注册中心访问连接,集群环境多个注册地址
defaultZone: http://127.0.0.1:7001/eureka,http://127.0.0.1:7002/eureka
instance:
instance-id: payment8001 #配置当前服务向eureka注册中心注册时显示的服务器主机名称
prefer-ip-address: true #配置在开发人员直接访问eureka服务器时显示当前eureka上注册的服务的ip
lease-renewal-interval-in-seconds: 1 #指定定时向eureka注册中心发送代表当前服务心跳包的时间默认30秒
lease-expiration-duration-in-seconds: 2 # Eureka 接收到当前服务最后一次发送代表正常心跳包的等待时间,超过则将当前服务在 Eureka 上踢除
#=================eureka相关配置end======================
mybatis:
mapperLocations: classpath:mapper/*.xml #扫描mappper.xml */
type-aliases-package: com.test.dao #扫描对应mapper.xml 接口
- 目标服务需要通过 Gateway 网关转发访问的请求
@RestController
public class PaymentController {
@Autowired
private PaymentServerApi paymentServerApi;
@GetMapping(value = "/getRun")
public JsonResult getRun() {
System.out.println("执行查询所有数据8001");
return JsonResult.success();
}
@GetMapping(value = "/payment/getById/{id}")
public JsonResult getById(@PathVariable("id") Long id) {
return paymentServerApi.getPaymentById(id);
}
}
- 目标服务启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient //表示当前 PaymentMain8001 服务注册到Eureka中
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}
三. 创建 Gateway 网关服务
- pom 文件中引入 Gateway 依赖,注意点不可以引入 spring-boot-starter-web 依赖,如果引入了启动会报错
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- yml 文件配置拦截的目标服务,uri 中配置通过目标服务名称在注册中心获取目标服务调用地址,本地服务实现调用
server:
port: 9527
spring:
application:
name: cloud-gateway #当前服务名称
cloud:
gateway:
discovery:
locator:
enabled: true #开启网关通过注册中心动态创建路由功能,利用微服务进行路由
routes:
#分组路由,可以定义多个
- id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #转发到的真实服务器地址(该方式是写死的)
uri: lb://cloud-payment-service #目标服务在注册中心的名称
#配置断言条件(可以简单理解为通过网关转发请求访问上面 uri 配置的服务下的接口,
#执行下面 predicates 中配置的断言,可以同时配置多个,符合条件则放行
predicates:
- Path=/payment/getById/** #Path路径断言,通过网关访问服务时判断访问路径是否是path指定的,"**"表示模糊匹配*/
#分组路由"payment_route2"
- id: payment_route2
#uri: http://localhost:8001
uri: lb://cloud-payment-service
predicates:
- Path=/getRun
#After 断言在指定时间以后才可以通过网关访问uri设置的服务上的接口
#这个时间字符串可以通过 ZonedDateTime获取
- After=2020-05-01T02:46:36.560+08:00[Asia/Shanghai]
#配置过滤器
filters:
#Gateway内置GatewayFilter类型过滤器
#表示会对接收到的请求添加"X-Request-Foo"请求头,并且值为"Bar"
- AddRequestHeader=X-Request-Foo, Bar
#将当前 Gateway 服务注册到 Eureka注册中心
eureka:
client:
register-with-eureka: true #true 表示将当前服务注册到eureka注册中心
#true 表示是否在注册中心抓取已有的注册信息,集群环境时必须为true,配合ribbon进行负载
fetchRegistry: true
service-url:
#eureka 注册中心访问连接,集群环境多个注册地址
defaultZone: http://127.0.0.1:7001/eureka,http://127.0.0.1:7002/eureka
instance:
instance-id: gateway9527 #配置当前服务向eureka注册中心注册时显示的服务器主机名称
prefer-ip-address: true #配置在开发人员直接访问eureka服务器时显示当前eureka上注册的服务的ip
lease-renewal-interval-in-seconds: 1 #指定定时向eureka注册中心发送代表当前服务心跳包的时间默认30秒
# Eureka 接收到当前服务最后一次发送代表正常心跳包的等待时间,超过则将当前服务在 Eureka 上踢除
lease-expiration-duration-in-seconds: 2
hostname: cloud-gateway-service
#=================eureka相关配置end======================
一些其它的 predicates 断言与 filters 示例,如果需求中用到可以配置在yml 中
predicates:
#Before 断言在指定时间以前才可以通过网关访问uri设置的服务上的接口
- Before=2020-05-01T02:46:36.560+08:00[Asia/Shanghai]
#Between 指定时间范围,在该范围内才允许访问
- Between=2020-05-01T02:46:36.560+08:00[Asia/Shanghai],2021-05-01T02:46:36.560+08:00[Asia/Shanghai]
#Cookie 断言,匹配是否携带了key为username,值是否与"aaa"匹配
- Cookie=username,aaa
#Header 请求头断言,匹配请求头中是携带 X-Request-Id 数据,值是否与"\d+"正则匹配
- Header=X-Request-Id,\d+
#Host 断言,匹配访问时的host是否与指定的正则匹配
- Host=**.com
#Method 断言,匹配是否是指定的请求方式
- Method=GET
#Query 断言,匹配发送的请求是否包含名为 "valName" 的参数,并且值要与后面的"\d+"正则匹配
- Query=valName,\d+
#配置过滤器
filters:
#表示对请求添加名为"foo"的参数,值为"bar"
- AddRequestParameter=foo, bar
#表示请求执行完毕后添加名为"X-Response-Foo"响应头,值为"Bar"
- AddResponseHeader=X-Response-Foo, Bar
#保留原始请求的host头信息,而不是通过Gateway网关转发后的请求头
- PreserveHostHeader
#匹配ip
- RemoteAddr=127.0.0.1
#路径重写过滤器,假设上面配置的"predicates:- Path=/server-api/getRun",在请求服务时每次都需要携带"/server-api"路径
#通过此方式,重新路径,假设以前请求服务接口为"当前网关服务ip地址:当前网关服务端口号/server-api/getRun"
#使用路径重写过滤器后可以直接访问"当前网关服务ip地址:当前网关服务端口号/getRun",会自动在端口号后添加 RewritePath 指定的
#"/server-api",其中",/$\{segment} "正则表达式表示空
- RewritePath=/server-api/(?<segment>.*),/$\{segment}
- Gateway 网关服务启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain9527.class,args);
}
}
- 最终效果,此时启动注册中心,启动目标服务,启动 Gateway 网关服务,以前通过 “http://目标服务ip:目标服务端口号/访问路径” 直接访问接口,现在可以 “http://网关服务ip:网关服务端口号/目标服务访问路径” 通过网关服务转发的访问访问到目标服务,会自动通过网关服务中配置的目标服务名称在注册中心获取目标服务访问地址列表,负载访问
- 直接访问微服务接口
- 通过网关间接访问服务接口
三. 自定义 GatewayFilter 过滤器
- 增加需求,在发起请求访问目标服务的时候判断请求中是否携带了名为 “name” 的参数,如果没有则拒绝访问,在以前不使用网关服务的时候可以通过自定义 jvm 层面的过滤器拦截判断,每个单体目标服务中都要创建该过滤器,现在可以在网关服务中创建网关过滤器,通过网关过滤器统一对请求过滤
- 在网关服务中创建自定义过滤器,需要继承 GlobalFilter 与 Ordered 接口,重写 GlobalFilter 中的 filter() 方法,为过滤方法,方法中 return GatewayFilterChain 调用 filter() 表示当前过滤器拦截放行,执行下一个过滤,如果在该return 以前执行了非 GatewayFilterChain 调用 filter() 的return 表示拒绝访问
- 重写 Ordered 中的 getOrder() 方法,设置当前过滤器的访问优先级,返回 int 变量数值越小优先级越高
- 代码示例
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.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MyGatewayFilter implements GlobalFilter, Ordered {
//1.过滤方法,在方法中当 GatewayFilterChain 调用 filter() 时代表过滤放行,执行下一个过滤
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("自定过滤器执行");
//1.通过 ServerWebExchange 获取 Request,拿到用户发送请求时的所有信息
ServerHttpRequest request = exchange.getRequest();
//获取请求头
request.getHeaders();
//获取uri
request.getURI();
//获取请求体
request.getBody();
//2.此处以获取请求携带的参数
MultiValueMap<String, String> valueMap = request.getQueryParams();
//判断是否存有名字为"name"的参数
String nameVal = valueMap.getFirst("name");
if(null == nameVal){
System.out.println("过滤拦击返回执行");
//3.通过 ServerWebExchange 获取 Response,设置响应前台的数据
ServerHttpResponse response = exchange.getResponse();
//此处以没有"name"值时响应前台不被接受的响应码
response.setStatusCode(HttpStatus.NOT_ACCEPTABLE);
//4.响应前台拒绝访问
return response.setComplete();
}
//5.当 GatewayFilterChain 调用 filter() 方法时代表拦截放行向下执行下一个过滤器
return chain.filter(exchange);
}
//2.优先级,通过该方法返回的int值来设置当前过滤器加载执行的优先级,越小优先级越高
@Override
public int getOrder() {
return 0;
}
}
- 此时的效果,当用户通过网关服务间接的请求目标服务时,会自动执行网关服务中创建的这个自定义过滤器,获取请求中的参数,判断参数中是否携带了名为"name"的变量,如果为空则拒绝访问并相应前台拒绝访问的状态吗,如果不为空拦截放行执行下一个过滤器
四. 通过自定义 GatewayFilter 过滤器实现允许跨域问题
- 在Spring Boot 中如果设置一个请求允许跨域,可以通过一个 @CrossOrigin 注解修饰,但是每个请求上都要添加该注解
@RestController
@CrossOrigin //表示该类下的接口支持跨域请求
public class PaymentController {
@Autowired
private PaymentServerApi paymentServerApi;
@GetMapping(value = "/getRun")
public JsonResult getRun() {
System.out.println("执行查询所有数据8001");
return JsonResult.success();
}
}
- 方式二: 通过自定义 GatewayFilter 定义过滤器,拦截请求,统一设置请求允许跨域
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class CrossGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,"POST,GET,PUT,DELETE");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,"*");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}