excludeUrls) {
+ this.excludeUrls = excludeUrls;
+ }
+}
diff --git a/src/main/java/com/muyu/gateway/filter/AccessLogFilter.java b/src/main/java/com/muyu/gateway/filter/AccessLogFilter.java
new file mode 100644
index 0000000..9ceec12
--- /dev/null
+++ b/src/main/java/com/muyu/gateway/filter/AccessLogFilter.java
@@ -0,0 +1,226 @@
+package com.muyu.gateway.filter;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import com.alibaba.nacos.common.utils.StringUtils;
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.gateway.model.AccessLog;
+import com.muyu.gateway.utils.WebFrameworkUtils;
+import lombok.extern.log4j.Log4j2;
+import org.reactivestreams.Publisher;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
+import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
+import org.springframework.cloud.gateway.support.BodyInserterContext;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ReactiveHttpOutputMessage;
+import org.springframework.http.codec.HttpMessageReader;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.BodyInserter;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.server.HandlerStrategies;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+/**
+ * 网关的访问日志过滤器
+ *
+ *
+ * TODO 如果网关执行异常,不会记录访问日志,后续研究下 https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging
+ */
+@Log4j2
+@Component
+public class AccessLogFilter implements GlobalFilter, Ordered {
+
+ private final List> messageReaders = HandlerStrategies.withDefaults().messageReaders();
+
+ /**
+ * 打印日志
+ *
+ * @param gatewayLog 网关日志
+ */
+ private void writeAccessLog(AccessLog gatewayLog) {
+ log.info("[网关日志:{}]", gatewayLog.toString());
+ }
+
+ @Override
+ public int getOrder() {
+ return -99;
+ }
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ // 将 Request 中可以直接获取到的参数,设置到网关日志
+ ServerHttpRequest request = exchange.getRequest();
+ // TODO traceId
+ AccessLog accessLog = AccessLog.builder()
+ .userId(request.getHeaders().getFirst(SecurityConstants.DETAILS_USER_ID))
+ .route(WebFrameworkUtils.getGatewayRoute(exchange))
+ .schema(request.getURI().getScheme())
+ .requestMethod(request.getMethod().name())
+ .requestUrl(request.getURI().getRawPath())
+ .queryParams(request.getQueryParams())
+ .requestHeaders(request.getHeaders())
+ .startTime(LocalDateTime.now())
+ .userIp(WebFrameworkUtils.getClientIP(exchange))
+ .build();
+
+ // 继续 filter 过滤
+ MediaType mediaType = request.getHeaders().getContentType();
+ return MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)
+ ?
+ filterWithRequestBody(exchange, chain, accessLog)
+ :
+ filterWithoutRequestBody(exchange, chain, accessLog);
+ }
+
+ private Mono filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
+ // 包装 Response,用于记录 Response Body
+ ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
+ return chain.filter(exchange.mutate().response(decoratedResponse).build())
+ .then(Mono.fromRunnable(() -> writeAccessLog(accessLog))); // 打印日志
+ }
+
+ /**
+ * 参考 {@link ModifyRequestBodyGatewayFilterFactory} 实现
+ *
+ * 差别主要在于使用 modifiedBody 来读取 Request Body 数据
+ */
+ private Mono filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog) {
+ // 设置 Request Body 读取时,设置到网关日志
+ ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
+ Mono modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
+ gatewayLog.setRequestBody(body);
+ return Mono.just(body);
+ });
+
+ // 创建 BodyInserter 对象
+ BodyInserter, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
+ // 创建 CachedBodyOutputMessage 对象
+ HttpHeaders headers = new HttpHeaders();
+ headers.putAll(exchange.getRequest().getHeaders());
+ // the new content type will be computed by bodyInserter
+ // and then set in the request decorator
+ headers.remove(HttpHeaders.CONTENT_LENGTH); // 移除
+ CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
+ // 通过 BodyInserter 将 Request Body 写入到 CachedBodyOutputMessage 中
+ return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
+ // 包装 Request,用于缓存 Request Body
+ ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
+ // 包装 Response,用于记录 Response Body
+ ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
+ // 记录普通的
+ return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
+ .then(Mono.fromRunnable(() -> writeAccessLog(gatewayLog))); // 打印日志
+
+ }));
+ }
+
+ /**
+ * 记录响应日志
+ * 通过 DataBufferFactory 解决响应体分段传输问题。
+ */
+ private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog gatewayLog) {
+ ServerHttpResponse response = exchange.getResponse();
+ return new ServerHttpResponseDecorator(response) {
+
+ @Override
+ public Mono writeWith(Publisher extends DataBuffer> body) {
+ if (body instanceof Flux) {
+ DataBufferFactory bufferFactory = response.bufferFactory();
+ // 计算执行时间
+ gatewayLog.setEndTime(LocalDateTime.now());
+ gatewayLog.setDuration((int) (LocalDateTimeUtil.between(gatewayLog.getStartTime(),
+ gatewayLog.getEndTime()).toMillis()));
+ // 设置其它字段
+// gatewayLog.setUserId(SecurityFrameworkUtils.getLoginUserId(exchange));
+ gatewayLog.setResponseHeaders(response.getHeaders());
+ gatewayLog.setHttpStatus(response.getStatusCode());
+
+ // 获取响应类型,如果是 json 就打印
+ String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
+ if (StringUtils.isNotBlank(originalResponseContentType)
+ && originalResponseContentType.contains("application/json")) {
+ Flux extends DataBuffer> fluxBody = Flux.from(body);
+ return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
+ // 设置 response body 到网关日志
+ byte[] content = readContent(dataBuffers);
+ String responseResult = new String(content, StandardCharsets.UTF_8);
+ gatewayLog.setResponseBody(responseResult);
+
+ // 响应
+ return bufferFactory.wrap(content);
+ }));
+ }
+ }
+ // if body is not a flux. never got there.
+ return super.writeWith(body);
+ }
+ };
+ }
+
+ // ========== 参考 ModifyRequestBodyGatewayFilterFactory 中的方法 ==========
+
+ /**
+ * 请求装饰器,支持重新计算 headers、body 缓存
+ *
+ * @param exchange 请求
+ * @param headers 请求头
+ * @param outputMessage body 缓存
+ * @return 请求装饰器
+ */
+ private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
+ return new ServerHttpRequestDecorator(exchange.getRequest()) {
+
+ @Override
+ public HttpHeaders getHeaders() {
+ long contentLength = headers.getContentLength();
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.putAll(super.getHeaders());
+ if (contentLength > 0) {
+ httpHeaders.setContentLength(contentLength);
+ } else {
+ httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
+ }
+ return httpHeaders;
+ }
+
+ @Override
+ public Flux getBody() {
+ return outputMessage.getBody();
+ }
+ };
+ }
+
+ // ========== 参考 ModifyResponseBodyGatewayFilterFactory 中的方法 ==========
+
+ private byte[] readContent(List extends DataBuffer> dataBuffers) {
+ // 合并多个流集合,解决返回体分段传输
+ DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
+ DataBuffer join = dataBufferFactory.join(dataBuffers);
+ byte[] content = new byte[join.readableByteCount()];
+ join.read(content);
+ // 释放掉内存
+ DataBufferUtils.release(join);
+ return content;
+ }
+
+}
diff --git a/src/main/java/com/muyu/gateway/filter/AuthFilter.java b/src/main/java/com/muyu/gateway/filter/AuthFilter.java
new file mode 100644
index 0000000..47e073f
--- /dev/null
+++ b/src/main/java/com/muyu/gateway/filter/AuthFilter.java
@@ -0,0 +1,120 @@
+package com.muyu.gateway.filter;
+
+import com.muyu.common.core.constant.CacheConstants;
+import com.muyu.common.core.constant.HttpStatus;
+import com.muyu.common.core.constant.SecurityConstants;
+import com.muyu.common.core.constant.TokenConstants;
+import com.muyu.common.core.utils.JwtUtils;
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.common.redis.service.RedisService;
+import com.muyu.gateway.config.properties.IgnoreWhiteProperties;
+import io.jsonwebtoken.Claims;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+/**
+ * 网关鉴权
+ *
+ * @author muyu
+ */
+@Component
+public class AuthFilter implements GlobalFilter, Ordered {
+ private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
+
+ // 排除过滤的 uri 地址,nacos自行添加
+ @Autowired
+ private IgnoreWhiteProperties ignoreWhite;
+
+ @Autowired
+ private RedisService redisService;
+
+
+ @Override
+ public Mono filter (ServerWebExchange exchange, GatewayFilterChain chain) {
+ ServerHttpRequest request = exchange.getRequest();
+ ServerHttpRequest.Builder mutate = request.mutate();
+
+ String url = request.getURI().getPath();
+ // 跳过不需要验证的路径
+ if (StringUtils.matches(url, ignoreWhite.getWhites())) {
+ return chain.filter(exchange);
+ }
+ String token = getToken(request);
+ if (StringUtils.isEmpty(token)) {
+ return unauthorizedResponse(exchange, "令牌不能为空");
+ }
+ Claims claims = JwtUtils.parseToken(token);
+ if (claims == null) {
+ return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
+ }
+ String userkey = JwtUtils.getUserKey(claims);
+ boolean islogin = redisService.hasKey(getTokenKey(userkey));
+ if (!islogin) {
+ return unauthorizedResponse(exchange, "登录状态已过期");
+ }
+ String userid = JwtUtils.getUserId(claims);
+ String username = JwtUtils.getUserName(claims);
+ if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
+ return unauthorizedResponse(exchange, "令牌验证失败");
+ }
+
+ // 设置用户信息到请求
+ addHeader(mutate, SecurityConstants.USER_KEY, userkey);
+ addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
+ addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
+ // 内部请求来源参数清除
+ removeHeader(mutate, SecurityConstants.FROM_SOURCE);
+ return chain.filter(exchange.mutate().request(mutate.build()).build());
+ }
+
+ private void addHeader (ServerHttpRequest.Builder mutate, String name, Object value) {
+ if (value == null) {
+ return;
+ }
+ String valueStr = value.toString();
+ String valueEncode = ServletUtils.urlEncode(valueStr);
+ mutate.header(name, valueEncode);
+ }
+
+ private void removeHeader (ServerHttpRequest.Builder mutate, String name) {
+ mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
+ }
+
+ private Mono unauthorizedResponse (ServerWebExchange exchange, String msg) {
+ log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
+ return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
+ }
+
+ /**
+ * 获取缓存key
+ */
+ private String getTokenKey (String token) {
+ return CacheConstants.LOGIN_TOKEN_KEY + token;
+ }
+
+ /**
+ * 获取请求token
+ */
+ private String getToken (ServerHttpRequest request) {
+ String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
+ // 如果前端设置了令牌前缀,则裁剪掉前缀
+ if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
+ token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
+ }
+ return token;
+ }
+
+ @Override
+ public int getOrder () {
+ return -200;
+ }
+}
diff --git a/src/main/java/com/muyu/gateway/filter/BlackListUrlFilter.java b/src/main/java/com/muyu/gateway/filter/BlackListUrlFilter.java
new file mode 100644
index 0000000..0096d4c
--- /dev/null
+++ b/src/main/java/com/muyu/gateway/filter/BlackListUrlFilter.java
@@ -0,0 +1,58 @@
+package com.muyu.gateway.filter;
+
+import com.muyu.common.core.utils.ServletUtils;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * 黑名单过滤器
+ *
+ * @author muyu
+ */
+@Component
+public class BlackListUrlFilter extends AbstractGatewayFilterFactory {
+ public BlackListUrlFilter () {
+ super(Config.class);
+ }
+
+ @Override
+ public GatewayFilter apply (Config config) {
+ return (exchange, chain) -> {
+
+ String url = exchange.getRequest().getURI().getPath();
+ if (config.matchBlacklist(url)) {
+ return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
+ }
+
+ return chain.filter(exchange);
+ };
+ }
+
+ public static class Config {
+ private List blacklistUrl;
+
+ private List blacklistUrlPattern = new ArrayList<>();
+
+ public boolean matchBlacklist (String url) {
+ return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
+ }
+
+ public List getBlacklistUrl () {
+ return blacklistUrl;
+ }
+
+ public void setBlacklistUrl (List blacklistUrl) {
+ this.blacklistUrl = blacklistUrl;
+ this.blacklistUrlPattern.clear();
+ this.blacklistUrl.forEach(url -> {
+ this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
+ });
+ }
+ }
+
+}
diff --git a/src/main/java/com/muyu/gateway/filter/CacheRequestFilter.java b/src/main/java/com/muyu/gateway/filter/CacheRequestFilter.java
new file mode 100644
index 0000000..3a09564
--- /dev/null
+++ b/src/main/java/com/muyu/gateway/filter/CacheRequestFilter.java
@@ -0,0 +1,75 @@
+package com.muyu.gateway.filter;
+
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.http.HttpMethod;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 获取body请求数据(解决流不能重复读取问题)
+ *
+ * @author muyu
+ */
+@Component
+public class CacheRequestFilter extends AbstractGatewayFilterFactory {
+ public CacheRequestFilter () {
+ super(Config.class);
+ }
+
+ @Override
+ public String name () {
+ return "CacheRequestFilter";
+ }
+
+ @Override
+ public GatewayFilter apply (Config config) {
+ CacheRequestGatewayFilter cacheRequestGatewayFilter = new CacheRequestGatewayFilter();
+ Integer order = config.getOrder();
+ if (order == null) {
+ return cacheRequestGatewayFilter;
+ }
+ return new OrderedGatewayFilter(cacheRequestGatewayFilter, order);
+ }
+
+ @Override
+ public List shortcutFieldOrder () {
+ return Collections.singletonList("order");
+ }
+
+ public static class CacheRequestGatewayFilter implements GatewayFilter {
+ @Override
+ public Mono filter (ServerWebExchange exchange, GatewayFilterChain chain) {
+ // GET DELETE 不过滤
+ HttpMethod method = exchange.getRequest().getMethod();
+ if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) {
+ return chain.filter(exchange);
+ }
+ return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
+ if (serverHttpRequest == exchange.getRequest()) {
+ return chain.filter(exchange);
+ }
+ return chain.filter(exchange.mutate().request(serverHttpRequest).build());
+ });
+ }
+ }
+
+ static class Config {
+ private Integer order;
+
+ public Integer getOrder () {
+ return order;
+ }
+
+ public void setOrder (Integer order) {
+ this.order = order;
+ }
+ }
+}
diff --git a/src/main/java/com/muyu/gateway/filter/ValidateCodeFilter.java b/src/main/java/com/muyu/gateway/filter/ValidateCodeFilter.java
new file mode 100644
index 0000000..c19c944
--- /dev/null
+++ b/src/main/java/com/muyu/gateway/filter/ValidateCodeFilter.java
@@ -0,0 +1,69 @@
+package com.muyu.gateway.filter;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.muyu.common.core.utils.ServletUtils;
+import com.muyu.common.core.utils.StringUtils;
+import com.muyu.gateway.config.properties.CaptchaProperties;
+import com.muyu.gateway.service.ValidateCodeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * 验证码过滤器
+ *
+ * @author muyu
+ */
+@Component
+public class ValidateCodeFilter extends AbstractGatewayFilterFactory