搭建 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 uri: lb://LROOM-AUTH-CENTER predicates: - Path=/auth/token/** filters: - StripPrefix=1 - Token=true
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> {
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; }
}
}
|
分析
为 StrpPrefix 提供 get 和 set 方法
写过滤的逻辑
将 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; } }
}
|
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) { }; }
|
完整代码
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; } }
}
|
只需要在配置文件中添加

结果演示
- 不携带 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; } }));
} }
|
看看结果

- 携带了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
- 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) { this.webClient = webClientBuilder.baseUrl(ServiceName.AUTH_SERVICE).build(); }
public Mono<UserContext> getUserInfo() { return webClient.get() .uri("/api/user/info") .retrieve() .bodyToMono(UserContext.class); }
}
|
- 发送请求
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向下游服务传递数据
- 从上游解析
token 中的用户信息
,传递到下游服务使用。避免每次都去 auth 服务获取用户信息。