搭建 Gateway

依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

新版的 gateway,摒弃了 ribbon 作为负载均衡,所以需要导入新的依赖

同时也是 解决lb失效的问题

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

关于Mac M1 启动报DNS,netty的错 “InvocationTargetException”

导入netty的依赖,即可解决

1
2
3
4
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>

配置文件(连载中)

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
server:
port: 9999
spring:
application:
name: LROOM-GATEWAY
cloud:
nacos:
discovery:
server-addr: xxx.xxx.xxx.xxx:8080
gateway:
routes:
- id: auth_router
# lb 是负载均衡 + 注册到 naco上的应用名字
# 也可以写 uri: http://127.0.0.1:8080 这样子的全路径
uri: lb://LROOM-AUTH-CENTER
# 断言用于过滤请求
# 不能不写哦
predicates:
- Path=/auth/token/**
# =1 就是把path 中第一个到 auth去掉
# 如果 =2 就是去掉 auth/token
filters:
- StripPrefix=1
# 这是我自己的自定义的 Token,读者不要写哦
- Token=true


# gateway 的跨域
globalcors:
corsConfigurations:
'[/**]':
allowCredentials: true
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"

启动两个服务看一下,结果

访问 localhost:9999/auth/token/select

模仿filters中stripeprefix 自定义 filter

可自定义很多东西,先学个皮毛

StripPrefixGatewayFilterFactory源码

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
public class StripPrefixGatewayFilterFactory
extends AbstractGatewayFilterFactory<StripPrefixGatewayFilterFactory.Config> {

/**
* Parts key.
*/
public static final String PARTS_KEY = "parts";

public StripPrefixGatewayFilterFactory() {
super(Config.class);
}

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(PARTS_KEY);
}

@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
addOriginalRequestUrl(exchange, request.getURI());
String path = request.getURI().getRawPath();
String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(path, "/")).skip(config.parts)
.collect(Collectors.joining("/"));
newPath += (newPath.length() > 1 && path.endsWith("/") ? "/" : "");
ServerHttpRequest newRequest = request.mutate().path(newPath).build();

exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());

return chain.filter(exchange.mutate().request(newRequest).build());
}

@Override
public String toString() {
return filterToStringCreator(StripPrefixGatewayFilterFactory.this).append("parts", config.getParts())
.toString();
}
};
}

public static class Config {

private int parts;

public int getParts() {
return parts;
}

public void setParts(int parts) {
this.parts = parts;
}

}

}

分析

  • 这个类继承 AbstractGatewayFilterFactory
  • 类名StripPrefixGatewayFilterFactory中去掉了 GatewayfileterFactory 就是过滤的条件名”StripPrefix“ (直接写名字也行,但是官网不推荐)

  • 有一个内部类 Config 的范型

为 StrpPrefix 提供 get 和 set 方法

  • 重写 apply 方法

写过滤的逻辑

  • 重写 shortcutFieldOrder 方法

将 stripPrefix 中的 value转化成数组

需求:写个 token 的filter,如果token = true ,则必须携带token,不然不放行

开始模仿

  • 新建一个 TokenGateWayFilterFactory 和 内部 config 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
@Slf4j
public class TokenGateWayFilterFactory extends AbstractGatewayFilterFactory<TokenGateWayFilterFactory.Config> {


public static class Config {

private boolean requireToken;

public boolean isRequireToken() {
return requireToken;
}

public void setRequireToken(boolean requireToken) {
this.requireToken = requireToken;
}
}

}
  • 重写和 stripPrefix 一样的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public List<String> shortcutFieldOrder() {
return List.of("requireToken");
}

public TokenGateWayFilterFactory() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
};
}
  • 写 apply的逻辑

完整代码

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
@Component
@Slf4j
public class TokenGateWayFilterFactory extends AbstractGatewayFilterFactory<TokenGateWayFilterFactory.Config> {

final
RedisUtil redisUtil;

public static final String TOKEN_KEY = "token";
private String token;


@Override
public List<String> shortcutFieldOrder() {
return List.of("requireToken");
}

public TokenGateWayFilterFactory(RedisUtil redisUtil) {
super(Config.class);
this.redisUtil = redisUtil;
}

@Override
public GatewayFilter apply(Config config) {

log.info("Enter Token GatewayFilter");
return (exchange, chain) -> {
if (config.isRequireToken()) {

String token = Optional
.ofNullable(exchange.getRequest().getQueryParams().get(TOKEN_KEY).get(0))
.orElseThrow(() -> new IllegalTokenException("Without Token"));
log.info("Token: " + token);

if (!validateToken(TOKEN_PREFIX_REDIS + token)) throw new IllegalTokenException("Invalid Token");

}

return chain.filter(exchange);
};
}

private Boolean validateToken(String token) {
return redisUtil.has(token);
}

public static class Config {

private boolean requireToken;

public boolean isRequireToken() {
return requireToken;
}

public void setRequireToken(boolean requireToken) {
this.requireToken = requireToken;
}
}

}

只需要在配置文件中添加

结果演示

  1. 不携带 token

不携带,报错了,但是和预期到不一样

所以,我们就需要一个全局的异常处理机制。

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
@Order(-1) // 代表过滤器的顺序,越小优先级越高
@Slf4j
@Configuration
public class GlobalExceptionConfiguration implements ErrorWebExceptionHandler {


@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {

ApiResult result = new ApiResult();
ServerHttpResponse response = exchange.getResponse();
// 多个异常处理时
if (response.isCommitted()) {
return Mono.error(ex);
}
// 设置响应头类型
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 处理异常信息
if (ex instanceof BizException) {
BizException bizEx = (BizException) ex;
result.setCode(bizEx.getErrorCode());
result.setMsg(bizEx.getErrorMsg());
}
return response.writeWith(Mono.fromSupplier(() -> {

DataBufferFactory dataBufferFactory = response.bufferFactory();
ObjectMapper objectMapper = new ObjectMapper();
try {
return dataBufferFactory.wrap(objectMapper.writeValueAsBytes(result));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}));

}
}

看看结果

  1. 携带了token,但是不正确

上面的全局异常,只是针对 gateway的异常的检查,gateway并不推荐做异常处理,它就是单一职责,做流量的转发而已。

出现上面的情况,是因为,在别的服务中,定义了异常的处理机制,gateway只是原封不动的将它返回了。

Gateway 要用全局异常处理,需要用到 GobalFilter, 而不是ErrorWebExceptionHandler。

Gateway中请求其它服务

  • openfeign不能用,不支持,别问为什么
  • 使用WebClient 调用别的服务

配置 webclient

1
2
3
4
5
6
7
8
9
@Configuration
public class LroomGatewayConfiguration {

@Bean
@LoadBalanced // 开启才能 通过服务名称调用
public WebClient.Builder webClient(){
return WebClient.builder();
}
}

Example

  1. Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class LroomAuthService {


private final WebClient webClient;

public LroomAuthService(WebClient.Builder webClientBuilder) {
// 设置基础Url
this.webClient = webClientBuilder.baseUrl(ServiceName.AUTH_SERVICE).build();
}

// 返回条用连接的 Mono对象
public Mono<UserContext> getUserInfo() {
return webClient.get()
.uri("/api/user/info")
.retrieve()
.bodyToMono(UserContext.class);
}

}
  1. 发送请求
1
2
3
4
5
6
7
8
9
@Autowried
LroomAuthService authService

Mono<UserContext> userContextMono = authService.getUserInfo();

userContextMono.subscribe(userContext -> {
String userName = userContext.getUserName();
System.out.println(userName);
});

tips: 不要用 block(), 因为webflux是异步的,需要使用 subscribe,否则调用失败

Gateway向下游服务传递数据

  1. 从上游解析 token 中的用户信息,传递到下游服务使用。避免每次都去 auth 服务获取用户信息。