微服务网关 Spring Cloud Gateway 进阶

关于网关

网关是怎么演化来的

单体应用拆分成多个服务后,对外需要一个统一入口,解耦客户端与内部服务

网关的基本功能

  • 网关核心功能是路由转发,因此不要有耗时操作在网关上处理,让请求快速转发到后端服务上
  • 网关还能做统一的熔断、限流、认证、日志监控等

关于Spring Cloud Gateway

Spring Cloud Gateway是由Spring官方基于Spring5.0、Spring Boot2.0、Project Reactor等技术开发的网关,使用非阻塞API,Websockets得到支持,目的是代替原先版本中的Spring Cloud Netfilx Zuul,目前Netfilx已经开源了Zuul2.0,但Spring 没有考虑集成,而是推出了自己开发的Spring Cloud GateWay。这里需要注意一下gateway使用的netty+webflux实现,不要加入web依赖(不要引用webmvc),否则初始化会报错,需要加入webflux依赖

gateway与zuul的简单比较:gateway使用的是异步请求,zuul是同步请求,gateway的数据封装在ServerWebExchange里,zuul封装在RequestContext里,同步方便调式,可以把数据封装在ThreadLocal中传递。

Spring Cloud Gateway有三个核心概念:路由、断言、过滤器

网关作为所有请求流量的入口,在实际生产环境中为了保证高可靠和高可用,尽量避免重启,需要用到动态路由配置,在网关运行过程中更改路由配置

代码实践

需要用到3个项目:eureka-server、gateway、consumer-service

eureka-server

eureka-server 服务发现注册,供gateway转发请求时获取服务实例 ip+port

gateway

新建 gateway 网关项目,项目引用如下:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在主类上启用服务发现注册注解@EnableDiscoveryClient

配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
server:
port: 9999
spring:
profiles:
active: dev
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
# 服务名小写
lower-case-service-id: true
routes:
- id: consumer-service
# lb代表从注册中心获取服务,且已负载均衡方式转发
uri: lb://consumer-service
predicates:
- Path=/consumer/**
# 加上StripPrefix=1,否则转发到后端服务时会带上consumer前缀
filters:
- StripPrefix=1

# 注册中心
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://zy:zy123@localhost:10025/eureka/


# 暴露监控端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always

consumer-service

consumer-service 消费者服务,通过网关路由转发到消费者服务,并返回信息回去

项目引用如下:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

在主类上启用服务发现注册注解@EnableDiscoveryClient

在配置文件中添加配置:

1
2
3
4
5
server.port=9700
spring.application.name=consumer-service
eureka.instance.prefer-ip-address=true
# 配置eureka-server security的账户信息
eureka.client.serviceUrl.defaultZone=http://zy:zy123@localhost:10025/eureka/

新建 IndexController,添加一个 hello 方法,传入name参数,访问后返回 hi + name 字符串

1
2
3
4
5
6
7
8
@RestController
public class IndexController {

@RequestMapping("/hello")
public String hello(String name){
return "hi " + name;
}
}

运行并测试

分别启动3个项目,访问 http://localhost:10025 看eureka上gateway与consumer-service实例是否注册成功,分别在9700、9999端口

通过网关访问consumer-service的hello方法,http://localhost:9999/consumer/hello?name=joe ,如果成功返回,说明请求已经转发到consumer-service服务上了

以上完成了网关的基本代码,下面继续介绍一些常用的过滤器,通过过滤器实现统一认证鉴权、日志、安全等检验

GlobalFilter 全局过滤器

在网关项目中添加 GlobalFilter 全局过滤器,打印每次请求的url,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* 全局过滤器
* 所有请求都会执行
* 可拦截get、post等请求做逻辑处理
*/
@Component
public class RequestGlobalFilter implements GlobalFilter, Ordered {

//执行逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest= exchange.getRequest();
String uri = serverHttpRequest.getURI().toString();
System.out.println(" uri : " + uri); //打印每次请求的url
String method = serverHttpRequest.getMethodValue();
if ("POST".equals(method)) {
//从请求里获取Post请求体
String bodyStr = resolveBodyFromRequest(serverHttpRequest);
//TODO 得到Post请求的请求参数后,做你想做的事

//下面的将请求体再次封装写回到request里,传到下一级,否则,由于请求体已被消费,后续的服务将取不到值
URI uri = serverHttpRequest.getURI();
ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build();
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);

request = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
//封装request,传给下一级
return chain.filter(exchange.mutate().request(request).build());
} else if ("GET".equals(method)) {
Map requestQueryParams = serverHttpRequest.getQueryParams();
//TODO 得到Get请求的请求参数后,做你想做的事

return chain.filter(exchange);
}
return chain.filter(exchange);
}
/**
* 从Flux<DataBuffer>中获取字符串的方法
* @return 请求体
*/
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
//获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();

AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
//获取request body
return bodyRef.get();
}

private DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);

NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}

//执行顺序
@Override
public int getOrder() {
return 1;
}
}

重新运行网关项目,并访问 http://localhost:9999/consumer/hello?name=joe ,查看控制台,可看到 uri 日志被打印出来了

GatewayFilter 过滤器

在网关项目中添加 GatewayFilter 过滤器,我们给consumer-service 添加 token 认证过滤器,和全局过滤器不同的是,GatewayFilter需要在配置文件中指定那个服务使用此过滤器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 可对客户端header 中的 Authorization 信息进行认证
*/
@Component
public class TokenAuthenticationFilter extends AbstractGatewayFilterFactory {

private static final String Bearer_ = "Bearer ";

@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
ServerHttpResponse response = exchange.getResponse();
try {
//String token = exchange.getRequest().getQueryParams().getFirst("authToken");
//1.获取header中的Authorization
String header = request.getHeaders().getFirst("Authorization");
if (header == null || !header.startsWith(Bearer_)) {
throw new RuntimeException("请求头中Authorization信息为空");
}
//2.截取Authorization Bearer
String token = header.substring(7);
//可把token存到redis中,此时直接在redis中判断是否有此key,有则校验通过,否则校验失败
if (!StringUtils.isEmpty(token)) {
System.out.println("验证通过");
//3.有token,把token设置到header中,传递给后端服务
mutate.header("userDetails", token).build();
} else {
//4.token无效
System.out.println("token无效");
DataBuffer bodyDataBuffer = responseErrorInfo(response, HttpStatus.UNAUTHORIZED.toString(), "无效的请求");
return response.writeWith(Mono.just(bodyDataBuffer));
}
} catch (Exception e) {
//没有token
DataBuffer bodyDataBuffer = responseErrorInfo(response, HttpStatus.UNAUTHORIZED.toString(), e.getMessage());
return response.writeWith(Mono.just(bodyDataBuffer));
}
ServerHttpRequest build = mutate.build();
return chain.filter(exchange.mutate().request(build).build());
};
}

/**
* 自定义返回错误信息
* @param response
* @param status
* @param message
* @return
*/
public DataBuffer responseErrorInfo(ServerHttpResponse response, String status, String message) {
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");

response.setStatusCode(HttpStatus.UNAUTHORIZED);
Map<String,String> map = new HashMap<>();
map.put("status", status);
map.put("message", message);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(map.toString().getBytes());
return bodyDataBuffer;
}
}

在配置文件中指定consumer-service服务使用 TokenAuthenticationFilter ,配置如下:

1
2
3
4
5
6
7
8
9
routes:
- id: consumer-service
uri: lb://consumer-service
predicates:
- Path=/consumer/**
filters:
# 进行token过滤
- TokenAuthenticationFilter
- StripPrefix=1

运行项目,再次访问 http://localhost:9999/consumer/hello?name=joe

1
{message=请求头中Authorization信息为空, status=401}

跨域问题

前后端分离项目解决网关跨域问题,在网关主类中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(ctx);
}

HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = ctx.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
if (requestMethod != null) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "all");
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx);
};
}

参考:https://gitee.com/zhuyu1991/spring-cloud/tree/master/gateway

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2021 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :