Initial commit
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
FROM openjdk:8-jre
|
||||
MAINTAINER oneone oneonetech@163.com
|
||||
|
||||
|
||||
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \&& echo 'Asia/Shanghai' >/etc/timezone
|
||||
|
||||
# /tmp 目录作为容器数据卷目录,SpringBoot内嵌Tomcat容器默认使用/tmp作为工作目录,任何向 /tmp 中写入的信息不会记录进容器存储层,从而保证容器存储层的无状态化
|
||||
# 在宿主机的/var/lib/docker目录下创建一个临时文件并把它链接到容器中的/tmp目录
|
||||
VOLUME /tmp
|
||||
|
||||
# 复制jar至镜像
|
||||
ADD target/oneone-gateway.jar app.jar
|
||||
|
||||
ENTRYPOINT ["java", "-Xmx128m", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]
|
||||
|
||||
EXPOSE 9999
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>oneone-cloud</artifactId>
|
||||
<groupId>com.oneone.cloud</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>oneone-sys-gateway</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<!--Spring Cloud & Alibaba-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 注册中心-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 配置中心 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sentinel流量控制、熔断降级 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-nacos</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Api文档 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
|
||||
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 配置读取 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 公共依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.oneone.cloud</groupId>
|
||||
<artifactId>common-redis</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-webmvc-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<!-- 设置构建的 jar 包名 -->
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
<!-- 打包 -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.oneone.sys.gateway;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
public class SysGatewayApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SysGatewayApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.oneone.sys.gateway.config;
|
||||
|
||||
import com.oneone.common.redis.utils.LocalRedisTokenStore;
|
||||
import com.oneone.common.redis.utils.RedisUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author mice
|
||||
* @version 1.0
|
||||
* @date 2022-04-07 16:44
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisStoreConfig {
|
||||
|
||||
@Bean(value = "upmsRedisTokenStore")
|
||||
public LocalRedisTokenStore upmsRedisTokenStore(RedisUtils redisUtils){
|
||||
LocalRedisTokenStore sysRedisTokenStore = new LocalRedisTokenStore(redisUtils);
|
||||
sysRedisTokenStore.setPrefix("upms_token:");
|
||||
return sysRedisTokenStore;
|
||||
}
|
||||
|
||||
|
||||
@Bean(value = "appRedisTokenStore")
|
||||
public LocalRedisTokenStore appRedisTokenStore(RedisUtils redisUtils){
|
||||
LocalRedisTokenStore appRedisTokenStore = new LocalRedisTokenStore(redisUtils);
|
||||
appRedisTokenStore.setPrefix("app_token:");
|
||||
return appRedisTokenStore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.oneone.sys.gateway.cors;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.cors.reactive.CorsUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 跨域 Filter
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class CorsFilter implements WebFilter {
|
||||
|
||||
private static final String ALL = "*";
|
||||
private static final String MAX_AGE = "3600L";
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
// 非跨域请求,直接放行
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
if (!CorsUtils.isCorsRequest(request)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// 设置跨域响应头
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
headers.add("Access-Control-Allow-Origin", ALL);
|
||||
headers.add("Access-Control-Allow-Methods", ALL);
|
||||
headers.add("Access-Control-Allow-Headers", ALL);
|
||||
headers.add("Access-Control-Max-Age", MAX_AGE);
|
||||
if (request.getMethod() == HttpMethod.OPTIONS) {
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
return Mono.empty();
|
||||
}
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.oneone.sys.gateway.cors;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 解决 Spring Cloud Gateway 2.x 跨域时,出现重复 Origin 的 BUG
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
// 指定此过滤器位于 NettyWriteResponseFilter 之后
|
||||
// 即待处理完响应体后接着处理响应头
|
||||
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
return chain.filter(exchange).then(Mono.defer(() -> {
|
||||
exchange.getResponse().getHeaders().entrySet().stream()
|
||||
.filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
|
||||
.filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)))
|
||||
.forEach(kv -> kv.setValue(new ArrayList<String>() {{
|
||||
add(kv.getValue().get(0));
|
||||
}}));
|
||||
return chain.filter(exchange);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.oneone.sys.gateway.grey;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
|
||||
import com.oneone.common.util.collection.CollectionUtils;
|
||||
import com.oneone.sys.gateway.util.EnvUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.*;
|
||||
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 灰度 {@link GrayLoadBalancer} 实现类
|
||||
*
|
||||
* 根据请求的 header[version] 匹配,筛选满足 metadata[version] 相等的服务实例列表,然后随机 + 权重进行选择一个
|
||||
* 1. 假如请求的 header[version] 为空,则不进行筛选,所有服务实例都进行选择
|
||||
* 2. 如果 metadata[version] 都不相等,则不进行筛选,所有服务实例都进行选择
|
||||
*
|
||||
* 注意,考虑到实现的简易,它的权重是使用 Nacos 的 nacos.weight,所以随机 + 权重也是基于 {@link NacosBalancer} 筛选。
|
||||
* 也就是说,如果你不使用 Nacos 作为注册中心,需要微调一下筛选的实现逻辑
|
||||
*
|
||||
*
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
|
||||
|
||||
private static final String VERSION = "version";
|
||||
|
||||
/**
|
||||
* 用于获取 serviceId 对应的服务实例的列表
|
||||
*/
|
||||
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
|
||||
/**
|
||||
* 需要获取的服务实例名
|
||||
*
|
||||
* 暂时用于打印 logger 日志
|
||||
*/
|
||||
private final String serviceId;
|
||||
|
||||
@Override
|
||||
public Mono<Response<ServiceInstance>> choose(Request request) {
|
||||
// 获得 HttpHeaders 属性,实现从 header 中获取 version
|
||||
HttpHeaders headers = ((RequestDataContext) request.getContext()).getClientRequest().getHeaders();
|
||||
// 选择实例
|
||||
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
|
||||
return supplier.get(request).next().map(list -> getInstanceResponse(list, headers));
|
||||
}
|
||||
|
||||
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, HttpHeaders headers) {
|
||||
// 如果服务实例为空,则直接返回
|
||||
if (CollUtil.isEmpty(instances)) {
|
||||
log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
// 筛选满足 version 条件的实例列表
|
||||
String version = headers.getFirst(VERSION);
|
||||
List<ServiceInstance> chooseInstances;
|
||||
if (StrUtil.isEmpty(version)) {
|
||||
chooseInstances = instances;
|
||||
} else {
|
||||
chooseInstances = CollectionUtils.filterList(instances, instance -> version.equals(instance.getMetadata().get("version")));
|
||||
if (CollUtil.isEmpty(chooseInstances)) {
|
||||
log.warn("[getInstanceResponse][serviceId({}) 没有满足版本({})的服务实例列表,直接使用所有服务实例列表]", serviceId, version);
|
||||
chooseInstances = instances;
|
||||
}
|
||||
}
|
||||
|
||||
// 基于 tag 过滤实例列表
|
||||
chooseInstances = filterTagServiceInstances(chooseInstances, headers);
|
||||
|
||||
// 随机 + 权重获取实例列表 TODO 目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法
|
||||
return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 tag 请求头,过滤匹配 tag 的服务实例列表
|
||||
*
|
||||
* copy from EnvLoadBalancerClient
|
||||
*
|
||||
* @param instances 服务实例列表
|
||||
* @param headers 请求头
|
||||
* @return 服务实例列表
|
||||
*/
|
||||
private List<ServiceInstance> filterTagServiceInstances(List<ServiceInstance> instances, HttpHeaders headers) {
|
||||
// 情况一,没有 tag 时,直接返回
|
||||
String tag = EnvUtils.getTag(headers);
|
||||
if (StrUtil.isEmpty(tag)) {
|
||||
return instances;
|
||||
}
|
||||
|
||||
// 情况二,有 tag 时,使用 tag 匹配服务实例
|
||||
List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance)));
|
||||
if (CollUtil.isEmpty(chooseInstances)) {
|
||||
log.warn("[filterTagServiceInstances][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
|
||||
chooseInstances = instances;
|
||||
}
|
||||
return chooseInstances;
|
||||
}
|
||||
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package com.oneone.sys.gateway.grey;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.*;
|
||||
import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
|
||||
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
|
||||
import org.springframework.cloud.gateway.support.NotFoundException;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*;
|
||||
|
||||
/**
|
||||
* 支持灰度功能的 {@link ReactiveLoadBalancerClientFilter} 实现类
|
||||
*
|
||||
* 由于 {@link ReactiveLoadBalancerClientFilter#choose(Request, String, Set)} 是 private 方法,无法进行重写。
|
||||
* 因此,这里只好 copy 它所有的代码,手动重写 choose 方法
|
||||
*
|
||||
* 具体的使用与实现原理,可阅读如下两个文章:
|
||||
* 1. https://www.jianshu.com/p/6db15bc0be8f
|
||||
* 2. https://cloud.tencent.com/developer/article/1620795
|
||||
*
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
@SuppressWarnings({"JavadocReference", "rawtypes", "unchecked", "ConstantConditions"})
|
||||
public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private final LoadBalancerClientFactory clientFactory;
|
||||
|
||||
private final GatewayLoadBalancerProperties properties;
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
|
||||
// 修改 by 芋道源码:将 lb 替换成 grayLb,表示灰度负载均衡
|
||||
if (url == null || (!"grayLb".equals(url.getScheme()) && !"grayLb".equals(schemePrefix))) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
// preserve the original url
|
||||
addOriginalRequestUrl(exchange, url);
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
|
||||
}
|
||||
|
||||
URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
String serviceId = requestUri.getHost();
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
|
||||
.getSupportedLifecycleProcessors(clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
|
||||
RequestDataContext.class, ResponseData.class, ServiceInstance.class);
|
||||
DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
|
||||
new RequestDataContext(new RequestData(exchange.getRequest()), getHint(serviceId)));
|
||||
return choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext(response -> {
|
||||
|
||||
if (!response.hasServer()) {
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
|
||||
.onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, response)));
|
||||
throw NotFoundException.create(properties.isUse404(), "Unable to find instance for " + url.getHost());
|
||||
}
|
||||
|
||||
ServiceInstance retrievedInstance = response.getServer();
|
||||
|
||||
URI uri = exchange.getRequest().getURI();
|
||||
|
||||
// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
|
||||
// if the loadbalancer doesn't provide one.
|
||||
String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
|
||||
if (schemePrefix != null) {
|
||||
overrideScheme = url.getScheme();
|
||||
}
|
||||
|
||||
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance,
|
||||
overrideScheme);
|
||||
|
||||
URI requestUrl = reconstructURI(serviceInstance, uri);
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
|
||||
}
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
|
||||
exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, response));
|
||||
}).then(chain.filter(exchange))
|
||||
.doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
|
||||
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
|
||||
CompletionContext.Status.FAILED, throwable, lbRequest,
|
||||
exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR)))))
|
||||
.doOnSuccess(aVoid -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
|
||||
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
|
||||
CompletionContext.Status.SUCCESS, lbRequest,
|
||||
exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR),
|
||||
new ResponseData(exchange.getResponse(), new RequestData(exchange.getRequest()))))));
|
||||
}
|
||||
|
||||
protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
|
||||
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
|
||||
}
|
||||
|
||||
private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId,
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
|
||||
// 修改 by 芋道源码:直接创建 GrayLoadBalancer 对象
|
||||
GrayLoadBalancer loadBalancer = new GrayLoadBalancer(
|
||||
clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId);
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
|
||||
return loadBalancer.choose(lbRequest);
|
||||
}
|
||||
|
||||
private String getHint(String serviceId) {
|
||||
LoadBalancerProperties loadBalancerProperties = clientFactory.getProperties(serviceId);
|
||||
Map<String, String> hints = loadBalancerProperties.getHint();
|
||||
String defaultHint = hints.getOrDefault("default", "default");
|
||||
String hintPropertyValue = hints.get(serviceId);
|
||||
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
|
||||
}
|
||||
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package com.oneone.sys.gateway.security;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.oneone.common.base.LocalToken;
|
||||
import com.oneone.common.base.LocalUser;
|
||||
import com.oneone.common.constant.GlobalConstants;
|
||||
import com.oneone.common.constant.SecurityConstants;
|
||||
import com.oneone.common.redis.utils.LocalRedisTokenStore;
|
||||
import com.oneone.common.result.ResultCode;
|
||||
import com.oneone.sys.gateway.util.ResponseUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 安全拦截全局过滤器
|
||||
*
|
||||
* @author oneone
|
||||
* @date 2022/2/15
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@ConfigurationProperties(prefix = "security")
|
||||
public class SecurityGlobalFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private final LocalRedisTokenStore upmsRedisTokenStore;
|
||||
|
||||
private final LocalRedisTokenStore appRedisTokenStore;
|
||||
|
||||
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||
|
||||
@Setter
|
||||
private List<String> ignoreUrls;
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
if (request.getMethod() == HttpMethod.OPTIONS) { // 预检请求放行
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
String path = request.getURI().getPath();
|
||||
|
||||
Boolean skipAuth = false;
|
||||
if (whiteMatch(path)){
|
||||
skipAuth = true;
|
||||
}
|
||||
|
||||
String token = request.getHeaders().getFirst(SecurityConstants.AUTHORIZATION_KEY);
|
||||
if (StringUtils.isEmpty(token)){
|
||||
if(skipAuth){
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
String ip = getIpAddress(request);
|
||||
log.error("token为空,ip:{},path:{}",ip,path);
|
||||
return ResponseUtils.writeErrorInfo(response, ResultCode.TOKEN_INVALID_OR_EXPIRED);
|
||||
}
|
||||
|
||||
LocalToken localToken = null;
|
||||
if (path.startsWith(SecurityConstants.APP_URI_PREFIX)) {
|
||||
localToken = appRedisTokenStore.readAccessToken(StringUtils.removeFirst(token, "Bearer "));
|
||||
/* if (token.startsWith(SecurityConstants.OFFICIAL_ACCOUNT_TOKEN_PREFIX)) {
|
||||
localToken = officialAccountRedisTokenStore.readAccessToken(StringUtils.removeFirst(token, SecurityConstants.OFFICIAL_ACCOUNT_TOKEN_PREFIX));
|
||||
}*/
|
||||
} else {
|
||||
localToken = upmsRedisTokenStore.readAccessToken(StringUtils.removeFirst(token, "Bearer "));
|
||||
}
|
||||
if (localToken == null){
|
||||
if(skipAuth){
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
String ip = getIpAddress(request);
|
||||
log.error("token已过期:{},ip:{}",token,ip);
|
||||
return ResponseUtils.writeErrorInfo(response, ResultCode.TOKEN_INVALID_OR_EXPIRED);
|
||||
}
|
||||
|
||||
LocalUser localUser = new LocalUser();
|
||||
localUser.setUserId(localToken.getMemberId());
|
||||
//localUser.setRoles(localToken.getRoles());
|
||||
|
||||
request = exchange.getRequest().mutate()
|
||||
.header(GlobalConstants.USER_INFO, URLEncoder.encode(JSONObject.toJSONString(localUser), "UTF-8"))
|
||||
.build();
|
||||
exchange = exchange.mutate().request(request).build();
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
private boolean whiteMatch(String path){
|
||||
//long start = System.currentTimeMillis();
|
||||
for (String pattern : ignoreUrls) {
|
||||
if (PATH_MATCHER.match(pattern,path)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//log.error("匹配耗时:{}",System.currentTimeMillis()-start);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static String getIpAddress(ServerHttpRequest request) {
|
||||
HttpHeaders headers = request.getHeaders();
|
||||
String ip = headers.getFirst("x-forwarded-for");
|
||||
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
|
||||
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
|
||||
if (ip.indexOf(",") != -1) {
|
||||
ip = ip.split(",")[0];
|
||||
}
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = headers.getFirst("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = headers.getFirst("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = headers.getFirst("HTTP_CLIENT_IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = headers.getFirst("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddress().getAddress().getHostAddress();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.oneone.sys.gateway.util;
|
||||
|
||||
import cn.hutool.core.net.NetUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class EnvUtils {
|
||||
|
||||
private static final String HEADER_TAG = "tag";
|
||||
|
||||
public static final String HOST_NAME_VALUE = "${HOSTNAME}";
|
||||
|
||||
public static String getTag(HttpHeaders headers) {
|
||||
String tag = headers.getFirst(HEADER_TAG);
|
||||
// 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名
|
||||
// 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做
|
||||
return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag;
|
||||
}
|
||||
|
||||
public static String getTag(ServiceInstance instance) {
|
||||
return instance.getMetadata().get(HEADER_TAG);
|
||||
}
|
||||
|
||||
public static String getHostName() {
|
||||
return StrUtil.blankToDefault(NetUtil.getLocalHostName(), IdUtil.fastSimpleUUID());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.oneone.sys.gateway.util;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.oneone.common.result.Result;
|
||||
import com.oneone.common.result.ResultCode;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @Author oneone
|
||||
* @Date 2021-01-29 13:30
|
||||
*/
|
||||
public class ResponseUtils {
|
||||
|
||||
public static Mono<Void> writeErrorInfo(ServerHttpResponse response, ResultCode resultCode) {
|
||||
switch (resultCode) {
|
||||
case TOKEN_INVALID_OR_EXPIRED:
|
||||
response.setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
break;
|
||||
case TOKEN_ACCESS_FORBIDDEN:
|
||||
response.setStatusCode(HttpStatus.FORBIDDEN);
|
||||
break;
|
||||
default:
|
||||
response.setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
break;
|
||||
}
|
||||
String body = JSONUtil.toJsonStr(Result.failed(resultCode));
|
||||
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
|
||||
return response.writeWith(Mono.just(buffer))
|
||||
.doOnError(error -> DataBufferUtils.release(buffer));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
server:
|
||||
port: 8922
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
# 注册中心
|
||||
discovery:
|
||||
server-addr: http://192.168.100.151:8848
|
||||
namespace: d2010a44-0999-4b45-9af8-2a0f028a97c3
|
||||
# 配置中心
|
||||
config:
|
||||
server-addr: http://192.168.100.151:8848
|
||||
namespace: d2010a44-0999-4b45-9af8-2a0f028a97c3
|
||||
file-extension: yaml
|
||||
shared-configs[0]:
|
||||
data-id: oneone-common.yaml
|
||||
refresh: true
|
||||
@@ -0,0 +1,27 @@
|
||||
--- #################### 注册中心相关配置 ####################
|
||||
server:
|
||||
port: 8922
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: 192.168.100.19:8848
|
||||
discovery:
|
||||
namespace: d2010a44-0999-4b45-9af8-2a0f028a97c3 # 命名空间。这里使用 dev 开发环境
|
||||
metadata:
|
||||
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
|
||||
|
||||
--- #################### 配置中心相关配置 ####################
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
# Nacos Config 配置项,对应 NacosConfigProperties 配置属性类
|
||||
config:
|
||||
server-addr: 192.168.100.19:8848 # Nacos 服务器地址
|
||||
namespace: d2010a44-0999-4b45-9af8-2a0f028a97c3 # 命名空间 dev 的ID,不能直接使用 dev 名称。创建命名空间的时候需要指定ID为 dev,这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
name: ${spring.application.name} # 使用的 Nacos 配置集的 dataId,默认为 spring.application.name
|
||||
file-extension: yaml # 使用的 Nacos 配置集的 dataId 的文件拓展名,同时也是 Nacos 配置集的配置格式,默认为 properties
|
||||
shared-configs[0]:
|
||||
data-id: oneone-common.yaml
|
||||
refresh: true
|
||||
@@ -0,0 +1,18 @@
|
||||
server:
|
||||
port: 8911
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
# 注册中心
|
||||
discovery:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
#namespace: 8d720841-979d-4a4c-8e26-4dc7345d7b08
|
||||
# 配置中心
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
#namespace: 8d720841-979d-4a4c-8e26-4dc7345d7b08
|
||||
file-extension: yaml
|
||||
shared-configs[0]:
|
||||
data-id: oneone-common.yaml
|
||||
refresh: true
|
||||
@@ -0,0 +1,16 @@
|
||||
server:
|
||||
port: 8911
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
# 注册中心
|
||||
discovery:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
# 配置中心
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
file-extension: yaml
|
||||
shared-configs[0]:
|
||||
data-id: oneone-common.yaml
|
||||
refresh: true
|
||||
@@ -0,0 +1,9 @@
|
||||
spring:
|
||||
application:
|
||||
name: system-gateway
|
||||
profiles:
|
||||
active: dev
|
||||
|
||||
main:
|
||||
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
|
||||
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
|
||||
Reference in New Issue
Block a user