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 { + @Override + public GatewayFilter apply(Object config) { + return new HeaderTokenGatewayFilter(); + + } +}