1、结构调整

2、抽象工厂优化
This commit is contained in:
2026-03-31 19:35:21 +08:00
parent 87757b352c
commit ebdbdbeb41
667 changed files with 1240 additions and 50173 deletions

View File

@@ -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>

View File

@@ -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);
}));
}
}

View File

@@ -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));
}

View File

@@ -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));

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 进行调用?
// A1Spring 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;
}
}

View File

@@ -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());
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,3 @@
灿能源码 http://www.iocoder.cn
Application Version: ${msgpush.info.version}
Spring Boot Version: ${spring-boot.version}

View File

@@ -34,17 +34,6 @@
<appender-ref ref="FILE"/>
</appender>
<!-- SkyWalking AppenderGRPC 日志收集,实现日志中心 -->
<!--
<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】打印日志可以注释掉本行 -->