From 400a561234088d3483390ae79965e200a5263576 Mon Sep 17 00:00:00 2001
From: qyx <565485304@qq.com>
Date: Mon, 12 Aug 2024 16:41:14 +0800
Subject: [PATCH] feat(master): SpringCLoud Gateway
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Gateway 相关的更新
---
.../README-SpringCloud-Gateway.md | 29 +++
.../dev-protocol-springcloud-gateway/pom.xml | 10 +
.../common/constant/CommonConstant.java | 19 ++
.../java/org/example/common/package-info.java | 1 +
.../example/common/util/TokenParseUtil.java | 63 +++++++
.../org/example/common/vo/CommonResponse.java | 36 ++++
.../java/org/example/common/vo/JwtToken.java | 17 ++
.../org/example/common/vo/LoginUserInfo.java | 20 ++
.../common/vo/UsernameAndPassword.java | 20 ++
.../org/example/conf/GatewayBeanConf.java | 17 ++
.../org/example/constant/GatewayConstant.java | 21 +++
.../filter/GlobalCacheRequestBodyFilter.java | 63 +++++++
.../filter/GlobalElapsedLogFilter.java | 40 ++++
.../filter/GlobalLoginOrRegisterFilter.java | 175 ++++++++++++++++++
.../filter/HeaderTokenGatewayFilter.java | 32 ++++
.../HeaderTokenGatewayFilterFactory.java | 16 ++
16 files changed, 579 insertions(+)
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/common/constant/CommonConstant.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/common/package-info.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/common/util/TokenParseUtil.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/common/vo/CommonResponse.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/common/vo/JwtToken.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/common/vo/LoginUserInfo.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/common/vo/UsernameAndPassword.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/conf/GatewayBeanConf.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/constant/GatewayConstant.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalCacheRequestBodyFilter.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalElapsedLogFilter.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalLoginOrRegisterFilter.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/HeaderTokenGatewayFilter.java
create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/factory/HeaderTokenGatewayFilterFactory.java
diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-gateway/README-SpringCloud-Gateway.md b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/README-SpringCloud-Gateway.md
index 340135d..ec6c21e 100644
--- a/dev-protocol-springcloud/dev-protocol-springcloud-gateway/README-SpringCloud-Gateway.md
+++ b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/README-SpringCloud-Gateway.md
@@ -36,6 +36,35 @@
---
## 5. 解读 SpringCloud Gateway Filter
+- SpringCloud Gateway Filter 的相关概念
+ - SpringCloud Gateway 基于过滤器实现,同 zuul 类似,有 pre 和 post 两种方式的filter,分别处理前置逻辑和后置逻辑
+ - 客户端的请求先经过 pre 类型的 filter,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过 post 类型的 fiter 处理,最后返回响应到客户端
+ - Filter 一共有两大类: 全局过滤器 和 局部过滤器
+- 搜索 GlobalFilter 就可以知道 Gateway 所支持的所有过滤器
+ - 添加前缀的局部过滤器: PrefixPathGatewayFilterFactory -> 局部过滤器返回均为 GatewayFilter
+ - 添加后缀过滤器(去掉前缀): StripPrefixGatewayFilterFactory
+---
+- SpringCloud Gateway Filter 的执行流程
+ - 过滤器有优先级之分,Order越大,优先级越低,越晚被执行
+ - 全局过滤器所有的请求都会执行
+ - 局部过滤器只有配置的请求才会执行
+---
+## 6. 局部过滤器 - 校验 Header 中的 Token
+- 实现一个局部过滤器, 和局部过滤器工厂
+ - [HeaderTokenGatewayFilter.java] + [HeaderTokenGatewayFilterFactory.java]
+
+---
+## 7. 缓存 HTTP 请求 Body 的全局过滤器
+- 缓存 HTTP 请求 Body 的全局过滤器
+ - [GlobalCacheRequestBodyFilter.java]
+---
+## 8. 登录、注册、鉴权全局过滤器
+- [GlobalLoginOrRegisterFilter.java]
+---
+## 9. 代码与文件两种方式配置网关路由
+
+## 10. 验证网关微服务功能可用性
+## 11. SpringCloud Gateway 微服务入口网关总结
\ No newline at end of file
diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-gateway/pom.xml b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/pom.xml
index c9360ad..d72f511 100644
--- a/dev-protocol-springcloud/dev-protocol-springcloud-gateway/pom.xml
+++ b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/pom.xml
@@ -18,6 +18,7 @@
8
8
UTF-8
+ 0.9.1
@@ -62,11 +63,20 @@
+
+ org.apache.commons
+ commons-lang3
+
com.alibaba
fastjson
1.2.51
+
+ io.jsonwebtoken
+ jjwt
+ ${jjwt.version}
+
DataBuffer
+ return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
+
+ // 确保数据缓冲区不被释放, 必须要 DataBufferUtils.retain
+ DataBufferUtils.retain(dataBuffer);
+ // defer、just 都是去创建数据源, 得到当前数据的副本
+ Flux cachedFlux = Flux.defer(() ->
+ Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
+ // 重新包装 ServerHttpRequest, 重写 getBody 方法, 能够返回请求数据
+ ServerHttpRequest mutatedRequest =
+ new ServerHttpRequestDecorator(exchange.getRequest()) {
+ @Override
+ public Flux getBody() {
+ return cachedFlux;
+ }
+ };
+ // 将包装之后的 ServerHttpRequest 向下继续传递
+ return chain.filter(exchange.mutate().request(mutatedRequest).build());
+ });
+ }
+
+ @Override
+ public int getOrder() {
+ return HIGHEST_PRECEDENCE + 1;
+ }
+}
diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalElapsedLogFilter.java b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalElapsedLogFilter.java
new file mode 100644
index 0000000..4539421
--- /dev/null
+++ b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalElapsedLogFilter.java
@@ -0,0 +1,40 @@
+package org.example.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.time.StopWatch;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 全局接口耗时日志过滤器
+ * */
+@Slf4j
+@Component
+public class GlobalElapsedLogFilter implements GlobalFilter, Ordered {
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+
+ // 前置逻辑
+ StopWatch sw = StopWatch.createStarted();
+ String uri = exchange.getRequest().getURI().getPath();
+
+ return chain.filter(exchange).then(
+ // 后置逻辑
+ Mono.fromRunnable(() ->
+ log.info("[{}] elapsed: [{}ms]",
+ uri, sw.getTime(TimeUnit.MILLISECONDS)))
+ );
+ }
+
+ @Override
+ public int getOrder() {
+ return HIGHEST_PRECEDENCE;
+ }
+}
diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalLoginOrRegisterFilter.java b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalLoginOrRegisterFilter.java
new file mode 100644
index 0000000..c502087
--- /dev/null
+++ b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/GlobalLoginOrRegisterFilter.java
@@ -0,0 +1,175 @@
+package org.example.filter;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.example.common.constant.CommonConstant;
+import org.example.common.util.TokenParseUtil;
+import org.example.common.vo.JwtToken;
+import org.example.common.vo.LoginUserInfo;
+import org.example.common.vo.UsernameAndPassword;
+import org.example.constant.GatewayConstant;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.server.ServerWebExchange;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * 全局登录鉴权过滤器
+ * */
+@Slf4j
+@Component
+public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {
+ /** 注册中心客户端, 可以从注册中心中获取服务实例信息 */
+ private final LoadBalancerClient loadBalancerClient;
+ private final RestTemplate restTemplate;
+
+ public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient,
+ RestTemplate restTemplate) {
+ this.loadBalancerClient = loadBalancerClient;
+ this.restTemplate = restTemplate;
+ }
+
+ /**
+ * 登录、注册、鉴权
+ * 1. 如果是登录或注册, 则去授权中心拿到 Token 并返回给客户端
+ * 2. 如果是访问其他的服务, 则鉴权, 没有权限返回 401
+ * */
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+
+ ServerHttpRequest request = exchange.getRequest();
+ ServerHttpResponse response = exchange.getResponse();
+
+ // 1. 如果是登录
+ if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
+ // 去授权中心拿 token
+ String token = getTokenFromAuthorityCenter(
+ request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT
+ );
+ // header 中不能设置 null
+ response.getHeaders().add(
+ CommonConstant.JWT_USER_INFO_KEY,
+ null == token ? "null" : token
+ );
+ response.setStatusCode(HttpStatus.OK);
+ return response.setComplete();
+ }
+
+ // 2. 如果是注册
+ if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
+ // 去授权中心拿 token: 先创建用户, 再返回 Token
+ String token = getTokenFromAuthorityCenter(
+ request,
+ GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT
+ );
+ response.getHeaders().add(
+ CommonConstant.JWT_USER_INFO_KEY,
+ null == token ? "null" : token
+ );
+ response.setStatusCode(HttpStatus.OK);
+ return response.setComplete();
+ }
+
+ // 3. 访问其他的服务, 则鉴权, 校验是否能够从 Token 中解析出用户信息
+ HttpHeaders headers = request.getHeaders();
+ String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
+ LoginUserInfo loginUserInfo = null;
+
+ try {
+ loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
+ } catch (Exception ex) {
+ log.error("parse user info from token error: [{}]", ex.getMessage(), ex);
+ }
+
+ // 获取不到登录用户信息, 返回 401
+ if (null == loginUserInfo) {
+ response.setStatusCode(HttpStatus.UNAUTHORIZED);
+ return response.setComplete();
+ }
+
+ // 解析通过, 则放行
+ return chain.filter(exchange);
+ }
+
+ @Override
+ public int getOrder() {
+ return HIGHEST_PRECEDENCE + 2;
+ }
+
+ /**
+ * 从授权中心获取 Token
+ * */
+ private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {
+
+ // service id 就是服务名字, 负载均衡
+ ServiceInstance serviceInstance = loadBalancerClient.choose(
+ CommonConstant.AUTHORITY_CENTER_SERVICE_ID
+ );
+ log.info("Nacos Client Info: [{}], [{}], [{}]",
+ serviceInstance.getServiceId(), serviceInstance.getInstanceId(),
+ JSON.toJSONString(serviceInstance.getMetadata()));
+
+ String requestUrl = String.format(
+ uriFormat, serviceInstance.getHost(), serviceInstance.getPort()
+ );
+ UsernameAndPassword requestBody = JSON.parseObject(
+ parseBodyFromRequest(request), UsernameAndPassword.class
+ );
+ log.info("login request url and body: [{}], [{}]", requestUrl, JSON.toJSONString(requestBody));
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ JwtToken token = restTemplate.postForObject(
+ requestUrl,
+ new HttpEntity<>(JSON.toJSONString(requestBody), headers),
+ JwtToken.class
+ );
+
+ if (null != token) {
+ return token.getToken();
+ }
+
+ return null;
+ }
+
+ /**
+ * 从 Post 请求中获取到请求数据
+ * */
+ private String parseBodyFromRequest(ServerHttpRequest request) {
+
+ // 获取请求体
+ Flux body = request.getBody();
+ // 原子引用
+ AtomicReference bodyRef = new AtomicReference<>();
+
+ // 订阅缓冲区去消费请求体中的数据
+ body.subscribe(buffer -> {
+ CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
+ // 一定要使用 DataBufferUtils.release 释放掉, 因为前面使用 DataBufferUtils.retain(dataBuffer); 保证不被释放, 这里不手动释放, 会出现内存泄露
+ DataBufferUtils.release(buffer);
+ bodyRef.set(charBuffer.toString());
+ });
+
+ // 获取 request body
+ return bodyRef.get();
+ }
+}
diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/HeaderTokenGatewayFilter.java b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/HeaderTokenGatewayFilter.java
new file mode 100644
index 0000000..4065750
--- /dev/null
+++ b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/HeaderTokenGatewayFilter.java
@@ -0,0 +1,32 @@
+package org.example.filter;
+
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+/**
+ * HTTP 请求头部携带 Token 验证过滤器
+ * */
+public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+
+ // 从 HTTP Header 中寻找 key 为 token, value 为 imooc 的键值对
+ String name = exchange.getRequest().getHeaders().getFirst("token");
+ if ("imooc".equals(name)) {
+ return chain.filter(exchange);
+ }
+
+ // 标记此次请求没有权限, 并结束这次请求
+ exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
+ return exchange.getResponse().setComplete();
+ }
+
+ @Override
+ public int getOrder() {
+ return HIGHEST_PRECEDENCE + 2;
+ }
+}
diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/factory/HeaderTokenGatewayFilterFactory.java b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/factory/HeaderTokenGatewayFilterFactory.java
new file mode 100644
index 0000000..deb3a0b
--- /dev/null
+++ b/dev-protocol-springcloud/dev-protocol-springcloud-gateway/src/main/java/org/example/filter/factory/HeaderTokenGatewayFilterFactory.java
@@ -0,0 +1,16 @@
+package org.example.filter.factory;
+
+import org.example.filter.HeaderTokenGatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+
+public class HeaderTokenGatewayFilterFactory extends AbstractGatewayFilterFactory