1、结构调整
2、抽象工厂优化
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
<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>msgpush</artifactId>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>cn-msgpush</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>msgpush-gateway</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
<description>API 服务网关,基于 Spring Cloud Gateway 实现</description>
|
||||
<url>https://github.com/YunaiV/msgpush-cloud</url>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
@@ -57,6 +59,7 @@
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
@@ -90,5 +93,4 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.njcn.msgpush.gateway.filter.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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 解决 Spring Cloud Gateway 2.x 跨域时,出现重复 Origin 的 BUG
|
||||
*
|
||||
* 参考文档:<a href="https://blog.csdn.net/zimou5581/article/details/90043178" />
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@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(() -> {
|
||||
// https://gitee.com/zhijiantianya/msgpush-cloud/pulls/177/
|
||||
List<String> keysToModify = 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)))
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
keysToModify.forEach(key->{
|
||||
List<String> values = exchange.getResponse().getHeaders().get(key);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
exchange.getResponse().getHeaders().put(key, Collections.singletonList(values.get(0)));
|
||||
}
|
||||
});
|
||||
return chain.filter(exchange);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package com.njcn.msgpush.gateway.filter.grey;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
|
||||
import com.njcn.msgpush.framework.common.util.collection.CollectionUtils;
|
||||
import com.njcn.msgpush.gateway.util.EnvUtils;
|
||||
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
@@ -79,7 +79,7 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
|
||||
// 基于 tag 过滤实例列表
|
||||
chooseInstances = filterTagServiceInstances(chooseInstances, headers);
|
||||
|
||||
// 随机 + 权重获取实例列表 TODO 芋艿:目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法
|
||||
// 随机 + 权重获取实例列表 TODO 目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法
|
||||
return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Order
|
||||
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,表示灰度负载均衡
|
||||
// 修改:将 lb 替换成 grayLb,表示灰度负载均衡
|
||||
if (url == null || (!"grayLb".equals(url.getScheme()) && !"grayLb".equals(schemePrefix))) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Order
|
||||
|
||||
private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId,
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
|
||||
// 修改 by 灿能源码:直接创建 GrayLoadBalancer 对象
|
||||
// 修改:直接创建 GrayLoadBalancer 对象
|
||||
GrayLoadBalancer loadBalancer = new GrayLoadBalancer(
|
||||
clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId);
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
|
||||
|
||||
@@ -3,10 +3,11 @@ package com.njcn.msgpush.gateway.filter.logging;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
import com.njcn.msgpush.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.msgpush.gateway.util.SecurityFrameworkUtils;
|
||||
import com.njcn.msgpush.gateway.util.WebFrameworkUtils;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
@@ -37,7 +38,6 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -50,7 +50,7 @@ import static cn.hutool.core.date.DatePattern.NORM_DATETIME_MS_FORMATTER;
|
||||
*
|
||||
* 从功能上,它类似 msgpush-spring-boot-starter-web 的 ApiAccessLogFilter 过滤器
|
||||
*
|
||||
* TODO 芋艿:如果网关执行异常,不会记录访问日志,后续研究下 https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging
|
||||
* TODO 如果网关执行异常,不会记录访问日志,后续研究下 https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@@ -71,34 +71,34 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
|
||||
// log.info("[writeAccessLog][日志内容:{}]", JsonUtils.toJsonString(gatewayLog));
|
||||
|
||||
// 方式二:调用远程服务,记录到数据库中
|
||||
// TODO 芋艿:暂未实现
|
||||
// TODO 暂未实现
|
||||
|
||||
// 方式三:打印到控制台,方便排查错误
|
||||
try {
|
||||
Map<String, Object> values = MapUtil.newHashMap(15, true); // 手工拼接,保证排序;15 保证不用扩容
|
||||
values.put("userId", gatewayLog.getUserId());
|
||||
values.put("userType", gatewayLog.getUserType());
|
||||
values.put("routeId", gatewayLog.getRoute() != null ? gatewayLog.getRoute().getId() : null);
|
||||
values.put("schema", gatewayLog.getSchema());
|
||||
values.put("requestUrl", gatewayLog.getRequestUrl());
|
||||
values.put("queryParams", gatewayLog.getQueryParams().toSingleValueMap());
|
||||
values.put("requestBody", JsonUtils.isJson(gatewayLog.getRequestBody()) ? // 保证 body 的展示好看
|
||||
JSONUtil.parse(gatewayLog.getRequestBody()) : gatewayLog.getRequestBody());
|
||||
values.put("requestHeaders", JsonUtils.toJsonString(gatewayLog.getRequestHeaders().toSingleValueMap()));
|
||||
values.put("userIp", gatewayLog.getUserIp());
|
||||
values.put("responseBody", JsonUtils.isJson(gatewayLog.getResponseBody()) ? // 保证 body 的展示好看
|
||||
JSONUtil.parse(gatewayLog.getResponseBody()) : gatewayLog.getResponseBody());
|
||||
values.put("responseHeaders", gatewayLog.getResponseHeaders() != null ?
|
||||
JsonUtils.toJsonString(gatewayLog.getResponseHeaders().toSingleValueMap()) : null);
|
||||
values.put("httpStatus", gatewayLog.getHttpStatus());
|
||||
values.put("startTime", LocalDateTimeUtil.format(gatewayLog.getStartTime(), NORM_DATETIME_MS_FORMATTER));
|
||||
values.put("endTime", LocalDateTimeUtil.format(gatewayLog.getEndTime(), NORM_DATETIME_MS_FORMATTER));
|
||||
values.put("duration", gatewayLog.getDuration() != null ? gatewayLog.getDuration() + " ms" : null);
|
||||
log.info("[writeAccessLog][网关日志:{}]", JsonUtils.toJsonPrettyString(values));
|
||||
} catch (Exception e) {
|
||||
// 兜底处理,参见 https://gitee.com/zhijiantianya/msgpush-cloud/issues/IC9A70
|
||||
log.error("[writeAccessLog][打印网关日志时,发生异常]", e);
|
||||
}
|
||||
// try {
|
||||
// Map<String, Object> values = MapUtil.newHashMap(15, true); // 手工拼接,保证排序;15 保证不用扩容
|
||||
// values.put("userId", gatewayLog.getUserId());
|
||||
// values.put("userType", gatewayLog.getUserType());
|
||||
// values.put("routeId", gatewayLog.getRoute() != null ? gatewayLog.getRoute().getId() : null);
|
||||
// values.put("schema", gatewayLog.getSchema());
|
||||
// values.put("requestUrl", gatewayLog.getRequestUrl());
|
||||
// values.put("queryParams", gatewayLog.getQueryParams().toSingleValueMap());
|
||||
// values.put("requestBody", JsonUtils.isJson(gatewayLog.getRequestBody()) ? // 保证 body 的展示好看
|
||||
// JSONUtil.parse(gatewayLog.getRequestBody()) : gatewayLog.getRequestBody());
|
||||
// values.put("requestHeaders", JsonUtils.toJsonString(gatewayLog.getRequestHeaders().toSingleValueMap()));
|
||||
// values.put("userIp", gatewayLog.getUserIp());
|
||||
// values.put("responseBody", JsonUtils.isJson(gatewayLog.getResponseBody()) ? // 保证 body 的展示好看
|
||||
// JSONUtil.parse(gatewayLog.getResponseBody()) : gatewayLog.getResponseBody());
|
||||
// values.put("responseHeaders", gatewayLog.getResponseHeaders() != null ?
|
||||
// JsonUtils.toJsonString(gatewayLog.getResponseHeaders().toSingleValueMap()) : null);
|
||||
// values.put("httpStatus", gatewayLog.getHttpStatus());
|
||||
// values.put("startTime", LocalDateTimeUtil.format(gatewayLog.getStartTime(), NORM_DATETIME_MS_FORMATTER));
|
||||
// values.put("endTime", LocalDateTimeUtil.format(gatewayLog.getEndTime(), NORM_DATETIME_MS_FORMATTER));
|
||||
// values.put("duration", gatewayLog.getDuration() != null ? gatewayLog.getDuration() + " ms" : null);
|
||||
// log.info("[writeAccessLog][网关日志:{}]", JsonUtils.toJsonPrettyString(values));
|
||||
// } catch (Exception e) {
|
||||
// // 兜底处理,参见 https://gitee.com/zhijiantianya/msgpush-cloud/issues/IC9A70
|
||||
// log.error("[writeAccessLog][打印网关日志时,发生异常]", e);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,36 +6,13 @@ import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 登录用户信息
|
||||
*
|
||||
* copy from msgpush-spring-boot-starter-security 的 LoginUser 类
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@Data
|
||||
public class LoginUser {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 额外的用户信息
|
||||
*/
|
||||
private Map<String, String> info;
|
||||
|
||||
/**
|
||||
* 授权范围
|
||||
*/
|
||||
private List<String> scopes;
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
private LocalDateTime expiresTime;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package com.njcn.msgpush.gateway.filter.security;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.msgpush.framework.common.core.KeyValue;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.njcn.msgpush.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
|
||||
import com.njcn.msgpush.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
|
||||
import com.njcn.msgpush.framework.common.exception.ServiceException;
|
||||
import com.njcn.msgpush.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.njcn.msgpush.framework.common.pojo.CommonResult;
|
||||
import com.njcn.msgpush.framework.common.util.date.LocalDateTimeUtils;
|
||||
import com.njcn.msgpush.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.msgpush.gateway.util.SecurityFrameworkUtils;
|
||||
import com.njcn.msgpush.gateway.util.WebFrameworkUtils;
|
||||
import com.njcn.msgpush.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
|
||||
import com.njcn.msgpush.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.njcn.msgpush.module.system.enums.ErrorCodeConstants;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -28,96 +29,72 @@ import java.util.function.Function;
|
||||
|
||||
import static com.njcn.msgpush.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
|
||||
|
||||
/**
|
||||
* Token 过滤器,验证 token 的有效性
|
||||
* 1. 验证通过时,将 userId、userType通过 Header 转发给服务
|
||||
* 2. 验证不通过,还是会转发给服务。因为,接口是否需要登录的校验,还是交给服务自身处理
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@Component
|
||||
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||
|
||||
/**
|
||||
* CommonResult<OAuth2AccessTokenCheckRespDTO> 对应的 TypeReference 结果,用于解析 checkToken 的结果
|
||||
*/
|
||||
private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE
|
||||
= new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {
|
||||
};
|
||||
private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE =
|
||||
new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {};
|
||||
|
||||
/**
|
||||
* 空的 LoginUser 的结果
|
||||
* <p>
|
||||
* 用于解决如下问题:
|
||||
* 1. {@link #getLoginUser(ServerWebExchange, String)} 返回 Mono.empty() 时,会导致后续的 flatMap 无法进行处理的问题。
|
||||
* 2. {@link #buildUser(String)} 时,如果 Token 已经过期,返回 LOGIN_USER_EMPTY 对象,避免缓存无法刷新
|
||||
*/
|
||||
private static final LoginUser LOGIN_USER_EMPTY = new LoginUser();
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
/**
|
||||
* 登录用户的本地缓存
|
||||
* <p>
|
||||
* key2:访问令牌
|
||||
*/
|
||||
private final LoadingCache<String, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1),
|
||||
new CacheLoader<String, LoginUser>() {
|
||||
|
||||
@Override
|
||||
public LoginUser load(String token) {
|
||||
String body = checkAccessToken(token).block();
|
||||
return buildUser(body);
|
||||
return buildUser(body, token);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) {
|
||||
// Q:为什么不使用 OAuth2TokenApi 进行调用?
|
||||
// A1:Spring Cloud OpenFeign 官方未内置 Reactive 的支持 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#reactive-support
|
||||
// 因此,这里采用 WebClient,通过 lbFunction 实现负载均衡
|
||||
this.webClient = WebClient.builder().filter(lbFunction).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
// 移除 login-user 的请求头,避免伪造模拟
|
||||
exchange = SecurityFrameworkUtils.removeLoginUser(exchange);
|
||||
|
||||
// 情况一,如果没有 Token 令牌,则直接继续 filter
|
||||
String token = SecurityFrameworkUtils.obtainAuthorization(exchange);
|
||||
if (StrUtil.isEmpty(token)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// 情况二,如果有 Token 令牌,则解析对应 userId、userType等字段,并通过 通过 Header 转发给服务
|
||||
// 重要说明:defaultIfEmpty 作用,保证 Mono.empty() 情况,可以继续执行 `flatMap 的 chain.filter(exchange)` 逻辑,避免返回给前端空的 Response!!
|
||||
ServerWebExchange finalExchange = exchange;
|
||||
return getLoginUser(exchange, token).defaultIfEmpty(LOGIN_USER_EMPTY).flatMap(user -> {
|
||||
// 1. 无用户,直接 filter 继续请求
|
||||
if (user == LOGIN_USER_EMPTY || // 下面 expiresTime 的判断,为了解决 token 实际已经过期的情况
|
||||
user.getExpiresTime() == null || LocalDateTimeUtils.beforeNow(user.getExpiresTime())) {
|
||||
return chain.filter(finalExchange);
|
||||
}
|
||||
return getLoginUser(token)
|
||||
.flatMap(user -> {
|
||||
if (user.getExpiresTime() == null) {
|
||||
return chain.filter(finalExchange);
|
||||
}
|
||||
if (LocalDateTimeUtils.beforeNow(user.getExpiresTime())) {
|
||||
loginUserCache.invalidate(token);
|
||||
return WebFrameworkUtils.writeJSON(finalExchange,
|
||||
CommonResult.error(ErrorCodeConstants.OAUTH2_ACCESS_TOKEN_EXPIRE));
|
||||
}
|
||||
|
||||
// 2.1 有用户,则设置登录用户
|
||||
SecurityFrameworkUtils.setLoginUser(finalExchange, user);
|
||||
// 2.2 将 user 并设置到 login-user 的请求头,使用 json 存储值
|
||||
ServerWebExchange newExchange = finalExchange.mutate()
|
||||
.request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user)).build();
|
||||
return chain.filter(newExchange);
|
||||
});
|
||||
SecurityFrameworkUtils.setLoginUser(finalExchange, user);
|
||||
ServerWebExchange newExchange = finalExchange.mutate()
|
||||
.request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user))
|
||||
.build();
|
||||
return chain.filter(newExchange);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> chain.filter(finalExchange)))
|
||||
.onErrorResume(ServiceException.class, ex ->
|
||||
WebFrameworkUtils.writeJSON(finalExchange, CommonResult.error(ex)));
|
||||
}
|
||||
|
||||
private Mono<LoginUser> getLoginUser(ServerWebExchange exchange, String token) {
|
||||
private Mono<LoginUser> getLoginUser(String token) {
|
||||
LoginUser localUser = loginUserCache.getIfPresent(token);
|
||||
if (localUser != null) {
|
||||
return Mono.just(localUser);
|
||||
return localUser == LOGIN_USER_EMPTY ? Mono.empty() : Mono.just(localUser);
|
||||
}
|
||||
|
||||
// 缓存不存在,则请求远程服务
|
||||
return checkAccessToken(token).flatMap((Function<String, Mono<LoginUser>>) body -> {
|
||||
LoginUser remoteUser = buildUser(body);
|
||||
LoginUser remoteUser = buildUser(body, token);
|
||||
if (remoteUser != null) {
|
||||
// 非空,则进行缓存
|
||||
loginUserCache.put(token, remoteUser);
|
||||
return Mono.just(remoteUser);
|
||||
}
|
||||
@@ -128,33 +105,34 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||
private Mono<String> checkAccessToken(String token) {
|
||||
return webClient.get()
|
||||
.uri(OAuth2TokenCommonApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build())
|
||||
.retrieve().bodyToMono(String.class);
|
||||
.retrieve()
|
||||
.bodyToMono(String.class);
|
||||
}
|
||||
|
||||
private LoginUser buildUser(String body) {
|
||||
// 处理结果,结果不正确
|
||||
private LoginUser buildUser(String body, String token) {
|
||||
CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
if (result.isError()) {
|
||||
// 特殊情况:令牌已经过期(code = 401),需要返回 LOGIN_USER_EMPTY,避免 Token 一直因为缓存,被误判为有效
|
||||
if (Objects.equals(result.getCode(), HttpStatus.UNAUTHORIZED.value())) {
|
||||
if (Objects.equals(result.getCode(), GlobalErrorCodeConstants.UNAUTHORIZED.getCode())) {
|
||||
loginUserCache.put(token, LOGIN_USER_EMPTY);
|
||||
return LOGIN_USER_EMPTY;
|
||||
}
|
||||
return null;
|
||||
throw new ServiceException(result.getCode(), result.getMsg());
|
||||
}
|
||||
|
||||
// 创建登录用户
|
||||
OAuth2AccessTokenCheckRespDTO tokenInfo = result.getData();
|
||||
return new LoginUser().setId(tokenInfo.getUserId()).setUserType(tokenInfo.getUserType())
|
||||
.setInfo(tokenInfo.getUserInfo()) // 额外的用户信息
|
||||
return new LoginUser().setId(tokenInfo.getUserId())
|
||||
.setUserType(tokenInfo.getUserType())
|
||||
.setInfo(tokenInfo.getUserInfo())
|
||||
.setScopes(tokenInfo.getScopes())
|
||||
.setExpiresTime(tokenInfo.getExpiresTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return -100; // 和 Spring Security Filter 的顺序对齐
|
||||
return -100;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.resource.NoResourceFoundException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static com.njcn.msgpush.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
||||
import static com.njcn.msgpush.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_FOUND;
|
||||
|
||||
/**
|
||||
* Gateway 的全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
|
||||
@@ -27,6 +29,8 @@ import static com.njcn.msgpush.framework.common.exception.enums.GlobalErrorCodeC
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
|
||||
|
||||
private static final String CHROME_DEVTOOLS_RESOURCE_PATH = "/.well-known/appspecific/com.chrome.devtools.json";
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||
// 已经 commit,则直接返回异常
|
||||
@@ -37,7 +41,9 @@ public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
|
||||
|
||||
// 转换成 CommonResult
|
||||
CommonResult<?> result;
|
||||
if (ex instanceof ResponseStatusException) {
|
||||
if (ex instanceof NoResourceFoundException) {
|
||||
result = noResourceFoundExceptionHandler(exchange, (NoResourceFoundException) ex);
|
||||
} else if (ex instanceof ResponseStatusException) {
|
||||
result = responseStatusExceptionHandler(exchange, (ResponseStatusException) ex);
|
||||
} else {
|
||||
result = defaultExceptionHandler(exchange, ex);
|
||||
@@ -52,12 +58,30 @@ public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
|
||||
*/
|
||||
private CommonResult<?> responseStatusExceptionHandler(ServerWebExchange exchange,
|
||||
ResponseStatusException ex) {
|
||||
// TODO 芋艿:这里要精细化翻译,默认返回用户是看不懂的
|
||||
// TODO 这里要精细化翻译,默认返回用户是看不懂的
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
log.error("[responseStatusExceptionHandler][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex);
|
||||
return CommonResult.error(ex.getStatusCode().value(), ex.getReason());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WebFlux 静态资源不存在异常
|
||||
*/
|
||||
private CommonResult<?> noResourceFoundExceptionHandler(ServerWebExchange exchange,
|
||||
NoResourceFoundException ex) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String path = request.getPath().value();
|
||||
log.debug("[noResourceFoundExceptionHandler][uri({}/{}) 请求地址不存在]", request.getURI(), request.getMethod());
|
||||
return CommonResult.error(NOT_FOUND.getCode(), buildNoResourceMessage(path));
|
||||
}
|
||||
|
||||
private String buildNoResourceMessage(String path) {
|
||||
if (CHROME_DEVTOOLS_RESOURCE_PATH.equals(path)) {
|
||||
return "当前服务未提供浏览器调试探测资源";
|
||||
}
|
||||
return String.format("请求地址不存在:%s", path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统异常,兜底处理所有的一切
|
||||
*/
|
||||
@@ -66,7 +90,7 @@ public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
|
||||
Throwable ex) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
log.error("[defaultExceptionHandler][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex);
|
||||
// TODO 芋艿:是否要插入异常日志呢?
|
||||
// TODO 是否要插入异常日志呢?
|
||||
// 返回 ERROR CommonResult
|
||||
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package com.njcn.msgpush.gateway.jackson;
|
||||
|
||||
import com.njcn.msgpush.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.msgpush.framework.common.util.json.databind.NumberSerializer;
|
||||
import com.njcn.msgpush.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
|
||||
import com.njcn.msgpush.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
@@ -11,11 +7,15 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import com.njcn.msgpush.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.msgpush.framework.common.util.json.databind.NumberSerializer;
|
||||
import com.njcn.msgpush.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
|
||||
import com.njcn.msgpush.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.web.codec.CodecCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
|
||||
|
||||
@@ -2,73 +2,45 @@ package com.njcn.msgpush.gateway.util;
|
||||
|
||||
import cn.hutool.core.net.NetUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import com.njcn.msgpush.framework.common.util.json.JsonUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.gateway.route.Route;
|
||||
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Web 工具类
|
||||
*
|
||||
* copy from msgpush-spring-boot-starter-web 的 WebFrameworkUtils 类
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@Slf4j
|
||||
public class WebFrameworkUtils {
|
||||
|
||||
|
||||
private WebFrameworkUtils() {}
|
||||
|
||||
|
||||
/**
|
||||
* 返回 JSON 字符串
|
||||
*
|
||||
* @param exchange 响应
|
||||
* @param object 对象,会序列化成 JSON 字符串
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||
@SuppressWarnings("deprecation")
|
||||
public static Mono<Void> writeJSON(ServerWebExchange exchange, Object object) {
|
||||
// 设置 header
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
|
||||
// 设置 body
|
||||
return response.writeWith(Mono.fromSupplier(() -> {
|
||||
DataBufferFactory bufferFactory = response.bufferFactory();
|
||||
try {
|
||||
return bufferFactory.wrap(JsonUtils.toJsonByte(object));
|
||||
} catch (Exception ex) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
log.error("[writeJSON][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex);
|
||||
log.error("[writeJSON][uri({}/{}) error]", request.getURI(), request.getMethod(), ex);
|
||||
return bufferFactory.wrap(new byte[0]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得客户端 IP
|
||||
*
|
||||
* 参考 {@link ServletUtil} 的 getClientIP 方法
|
||||
*
|
||||
* @param exchange 请求
|
||||
* @param otherHeaderNames 其它 header 名字的数组
|
||||
* @return 客户端 IP
|
||||
*/
|
||||
public static String getClientIP(ServerWebExchange exchange, String... otherHeaderNames) {
|
||||
String[] headers = { "X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR" };
|
||||
String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP",
|
||||
"HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
|
||||
if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
|
||||
headers = ArrayUtil.addAll(headers, otherHeaderNames);
|
||||
}
|
||||
// 方式一,通过 header 获取
|
||||
String ip;
|
||||
for (String header : headers) {
|
||||
ip = exchange.getRequest().getHeaders().getFirst(header);
|
||||
@@ -76,8 +48,6 @@ public class WebFrameworkUtils {
|
||||
return NetUtil.getMultistageReverseProxyIp(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// 方式二,通过 remoteAddress 获取
|
||||
if (exchange.getRequest().getRemoteAddress() == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -85,12 +55,6 @@ public class WebFrameworkUtils {
|
||||
return NetUtil.getMultistageReverseProxyIp(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得请求匹配的 Route 路由
|
||||
*
|
||||
* @param exchange 请求
|
||||
* @return 路由
|
||||
*/
|
||||
public static Route getGatewayRoute(ServerWebExchange exchange) {
|
||||
return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
--- #################### 注册中心 + 配置中心相关配置 ####################
|
||||
|
||||
#################### 注册中心 + 配置中心相关配置 ####################
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
@@ -7,14 +6,13 @@ spring:
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
namespace: 7e15dd2b-2aa2-487d-9e80-1c01c7b9f742 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
config: # 【注册中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
namespace: 7e15dd2b-2aa2-487d-9e80-1c01c7b9f742 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
#################### 监控相关配置 ####################
|
||||
# Actuator 监控端点的配置项
|
||||
management:
|
||||
endpoints:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
--- #################### 注册中心 + 配置中心相关配置 ####################
|
||||
|
||||
#################### 注册中心 + 配置中心相关配置 ####################
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
@@ -7,14 +6,13 @@ spring:
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
namespace: 7e15dd2b-2aa2-487d-9e80-1c01c7b9f742 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
config: # 【注册中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
namespace: 7e15dd2b-2aa2-487d-9e80-1c01c7b9f742 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
#################### 监控相关配置 ####################
|
||||
# Actuator 监控端点的配置项
|
||||
management:
|
||||
endpoints:
|
||||
@@ -27,5 +25,5 @@ management:
|
||||
# 日志文件配置
|
||||
logging:
|
||||
level:
|
||||
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
|
||||
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR
|
||||
|
||||
|
||||
@@ -34,38 +34,22 @@ spring:
|
||||
routes:
|
||||
## system-server 服务
|
||||
- id: system-admin-api # 路由的编号
|
||||
uri: grayLb://system-server
|
||||
uri: grayLb://msgpush-system-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/admin-api/system/**
|
||||
filters:
|
||||
- RewritePath=/admin-api/system/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||
- id: system-app-api # 路由的编号
|
||||
uri: grayLb://system-server
|
||||
uri: grayLb://msgpush-system-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/app-api/system/**
|
||||
filters:
|
||||
- RewritePath=/app-api/system/v3/api-docs, /v3/api-docs
|
||||
## infra-server 服务
|
||||
- id: infra-admin-api # 路由的编号
|
||||
uri: grayLb://infra-server
|
||||
- id: system-websocket # 路由的编号(WebSocket)
|
||||
uri: grayLb://msgpush-system-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/admin-api/infra/**
|
||||
filters:
|
||||
- RewritePath=/admin-api/infra/v3/api-docs, /v3/api-docs
|
||||
- id: infra-app-api # 路由的编号
|
||||
uri: grayLb://infra-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/app-api/infra/**
|
||||
filters:
|
||||
- RewritePath=/app-api/infra/v3/api-docs, /v3/api-docs
|
||||
- id: infra-spring-boot-admin # 路由的编号(Spring Boot Admin)
|
||||
uri: grayLb://infra-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/admin/**
|
||||
- id: infra-websocket # 路由的编号(WebSocket)
|
||||
uri: grayLb://infra-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/infra/ws/**
|
||||
- Path=/system/ws/**
|
||||
## push-server 服务
|
||||
- id: push-admin-api # 路由的编号
|
||||
uri: grayLb://push-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
@@ -74,6 +58,8 @@ spring:
|
||||
- RewritePath=/admin-api/push/v3/api-docs, /v3/api-docs
|
||||
x-forwarded:
|
||||
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
||||
default-filters:
|
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin # 采用 https://github.com/spring-cloud/spring-cloud-gateway/pull/866 解决响应头重复问题
|
||||
|
||||
server:
|
||||
port: 48080
|
||||
@@ -88,11 +74,8 @@ knife4j:
|
||||
enabled: true
|
||||
routes:
|
||||
- name: system-server
|
||||
service-name: system-server
|
||||
service-name: msgpush-system-server
|
||||
url: /admin-api/system/v3/api-docs
|
||||
- name: infra-server
|
||||
service-name: infra-server
|
||||
url: /admin-api/infra/v3/api-docs
|
||||
- name: push-server
|
||||
service-name: push-server
|
||||
url: /admin-api/push/v3/api-docs
|
||||
@@ -101,4 +84,4 @@ knife4j:
|
||||
|
||||
msgpush:
|
||||
info:
|
||||
version: 1.0.0
|
||||
version: 1.0.0
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
灿能源码 http://www.iocoder.cn
|
||||
Application Version: ${msgpush.info.version}
|
||||
Spring Boot Version: ${spring-boot.version}
|
||||
|
||||
|
||||
@@ -34,17 +34,6 @@
|
||||
<appender-ref ref="FILE"/>
|
||||
</appender>
|
||||
|
||||
<!-- SkyWalking Appender:GRPC 日志收集,实现日志中心 -->
|
||||
<!--
|
||||
<appender name="SKYWALKING" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
|
||||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
|
||||
<pattern>[%tid] ${FILE_LOG_PATTERN}</pattern>
|
||||
</layout>
|
||||
</encoder>
|
||||
</appender>
|
||||
-->
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<!-- 本地环境下,如果不想【FILE】打印日志,可以注释掉本行 -->
|
||||
|
||||
Reference in New Issue
Block a user