lixueqiang před 2 roky
rodič
revize
f7ab66340b
26 změnil soubory, kde provedl 958 přidání a 19 odebrání
  1. 1 0
      .gitignore
  2. 1 1
      kxs-admin-ui/.env.development
  3. 1 1
      kxs-admin-ui/src/views/login/component/password.vue
  4. 1 6
      kxs-admin-ui/vite.config.ts
  5. 11 6
      kxs-admin-ui/yarn.lock
  6. 0 2
      kxs-codegen/src/main/resources/application.yml
  7. 1 1
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/ErrorTypeEnum.java
  8. 22 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/AesDecodeException.java
  9. 13 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/GlobalBizExceptionHandler.java
  10. 0 2
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/ResourceAuthExceptionEntryPoint.java
  11. 70 0
      kxs-gateway/pom.xml
  12. 21 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/KxsGatewayApplication.java
  13. 38 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/config/AesCryptoConfig.java
  14. 41 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/config/GatewayConfigProperties.java
  15. 46 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/config/GatewayConfiguration.java
  16. 32 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/config/RateLimiterConfiguration.java
  17. 35 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/config/RouterFunctionConfiguration.java
  18. 121 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/filter/RequestDecoderFilter.java
  19. 128 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/filter/ValidateCodeGatewayFilter.java
  20. 59 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/handler/GlobalExceptionHandler.java
  21. 66 0
      kxs-gateway/src/main/java/com/kxs/gateway/api/handler/ImageCodeHandler.java
  22. 65 0
      kxs-gateway/src/main/resources/application.yml
  23. 77 0
      kxs-gateway/src/main/resources/logback-spring.xml
  24. 24 0
      kxs-system/kxs-system-biz/src/main/java/com/kxs/system/biz/KxsAdminApplication.java
  25. 64 0
      kxs-system/kxs-system-biz/src/main/resources/application.yml
  26. 20 0
      pom.xml

+ 1 - 0
.gitignore

@@ -40,6 +40,7 @@ target/
 
 ### vscode ###
 .vscode
+yarn.lock
 
 ### logs ###
 /logs/

+ 1 - 1
kxs-admin-ui/.env.development

@@ -8,6 +8,6 @@ VITE_OPEN = true
 ENV = 'development'
 
 # ADMIN 服务地址
-VITE_ADMIN_PROXY_PATH = http://127.0.0.1:8080
+VITE_ADMIN_PROXY_PATH = http://127.0.0.1:9999
 # 代码生成地址
 VITE_GEN_PROXY_PATH = http://127.0.0.1:5002

+ 1 - 1
kxs-admin-ui/src/views/login/component/password.vue

@@ -95,7 +95,7 @@ const imgSrc = ref('')
 //获取验证码图片
 const getVerifyCode = () => {
   state.ruleForm.randomStr = generateUUID()
-  imgSrc.value = `${import.meta.env.VITE_API_URL}/admin/code?randomStr=${state.ruleForm.randomStr}`
+  imgSrc.value = `${import.meta.env.VITE_API_URL}/code?randomStr=${state.ruleForm.randomStr}`
 }
 
 // 账号密码登录

+ 1 - 6
kxs-admin-ui/vite.config.ts

@@ -64,16 +64,11 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
 					changeOrigin: true, // 是否修改请求头中的 Origin 字段
 					rewrite: (path) => path.replace(/^\/api/, ''),
 				},
-				'/v1/kxs/admin': {
+				'/v1/kxs': {
 					target: env.VITE_ADMIN_PROXY_PATH, // 目标服务器地址
 					ws: true, // 是否启用 WebSocket
 					changeOrigin: true
 				},
-				'/v1/kxs/gen': {
-					target: env.VITE_GEN_PROXY_PATH, // 目标服务器地址
-					ws: true, // 是否启用 WebSocket
-					changeOrigin: true
-				},
 				'^/ws/info/.*': {
 					target: env.VITE_ADMIN_PROXY_PATH, // 目标服务器地址
 					ws: true, // 是否启用 WebSocket

+ 11 - 6
kxs-admin-ui/yarn.lock

@@ -44,9 +44,9 @@
   "resolved" "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz"
   "version" "2.1.0"
 
-"@esbuild/win32-x64@0.18.11":
-  "integrity" "sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA=="
-  "resolved" "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz"
+"@esbuild/darwin-arm64@0.18.11":
+  "integrity" "sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w=="
+  "resolved" "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz"
   "version" "0.18.11"
 
 "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0":
@@ -235,9 +235,9 @@
     "estree-walker" "^2.0.2"
     "picomatch" "^2.3.1"
 
-"@swc/core-win32-x64-msvc@1.3.68":
-  "integrity" "sha512-9HZVtLQUgK8r/yXQdwe0VBexbIcrY6+fBROhs7AAPWdewpaUeLkwQEJk6TbYr9CQuHw26FFGg6SjwAiqXF+kgQ=="
-  "resolved" "https://registry.npmmirror.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.68.tgz"
+"@swc/core-darwin-arm64@1.3.68":
+  "integrity" "sha512-Z5pNxeuP2NxpOHTzDQkJs0wAPLnTlglZnR3WjObijwvdwT/kw1Y5EPDKM/BVSIeG40SPMkDLBbI0aj0qyXzrBA=="
+  "resolved" "https://registry.npmmirror.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.68.tgz"
   "version" "1.3.68"
 
 "@swc/core@^1.3.10":
@@ -1433,6 +1433,11 @@
   "resolved" "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz"
   "version" "1.0.0"
 
+"fsevents@~2.3.2":
+  "integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="
+  "resolved" "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz"
+  "version" "2.3.2"
+
 "function-bind@^1.1.1":
   "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
   "resolved" "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz"

+ 0 - 2
kxs-codegen/src/main/resources/application.yml

@@ -1,7 +1,5 @@
 server:
   port: 5002
-  servlet:
-    context-path: /v1/kxs/gen
 
 # 配置文件加密根密码
 jasypt:

+ 1 - 1
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/ErrorTypeEnum.java

@@ -12,7 +12,7 @@ import lombok.RequiredArgsConstructor;
 @Getter
 @RequiredArgsConstructor
 public enum ErrorTypeEnum {
-
+    DECRYPT_ERROR(-1, "解密异常"),
     TOKEN_EXPIRED(-1, "令牌已过期"),
     TOKEN_EMPTY(-1, "令牌不存在"),
     CLIENT_NULL(-1, "客户端查询异常,请检查数据库链接"),

+ 22 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/AesDecodeException.java

@@ -0,0 +1,22 @@
+
+package com.kxs.common.core.exception;
+
+
+import java.io.Serial;
+
+/**
+ * 解密异常
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+public class AesDecodeException extends RuntimeException {
+
+	@Serial
+	private static final long serialVersionUID = -7285211528095468156L;
+
+	public AesDecodeException(String msg) {
+		super(msg);
+	}
+
+}

+ 13 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/GlobalBizExceptionHandler.java

@@ -124,4 +124,17 @@ public class GlobalBizExceptionHandler {
         return R.failed(exception.getMessage());
     }
 
+    /**
+     * aes 解密 Exception
+     *
+     * @param exception 异常
+     * @return R
+     */
+    @ExceptionHandler({AesDecodeException.class })
+    @ResponseStatus(HttpStatus.OK)
+    public R<Object> bindAesDecodeException(AesDecodeException exception) {
+        log.error("解密异常:{}", exception.getMessage(),exception);
+        return R.failed(exception.getMessage());
+    }
+
 }

+ 0 - 2
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/ResourceAuthExceptionEntryPoint.java

@@ -8,8 +8,6 @@ import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
-import org.springframework.context.MessageSource;
-import org.springframework.context.i18n.LocaleContextHolder;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.InsufficientAuthenticationException;
 import org.springframework.security.core.AuthenticationException;

+ 70 - 0
kxs-gateway/pom.xml

@@ -0,0 +1,70 @@
+<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/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs</artifactId>
+        <version>1.1.0</version>
+    </parent>
+    <artifactId>kxs-gateway</artifactId>
+    <packaging>jar</packaging>
+
+    <description>网关模块</description>
+
+    <!--gateway 网关依赖,内置webflux 依赖-->
+    <dependencies>
+
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-gateway</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-captcha</artifactId>
+        </dependency>
+        <!-- LB 扩展 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+        <!--caffeine 替换LB 默认缓存实现-->
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+        <!--结局mac下报错日志,可忽略-->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-resolver-dns-native-macos</artifactId>
+            <version>4.1.95.Final</version>
+            <classifier>osx-aarch_64</classifier>
+        </dependency>
+        <!-- 工具包依赖 -->
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-crypto</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-http</artifactId>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+    </build>
+</project>

+ 21 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/KxsGatewayApplication.java

@@ -0,0 +1,21 @@
+package com.kxs.gateway.api;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+/**
+ * 网关
+ *
+ * @author Lxq
+ * @date 2023-11-11
+ */
+@EnableDiscoveryClient
+@SpringBootApplication
+public class KxsGatewayApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(KxsGatewayApplication.class, args);
+	}
+
+}

+ 38 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/config/AesCryptoConfig.java

@@ -0,0 +1,38 @@
+package com.kxs.gateway.api.config;
+
+import cn.hutool.crypto.symmetric.AES;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 对称加密配置类
+ *
+ * @author Lxq
+ * @date 2023-11-03
+ */
+@Configuration
+@RequiredArgsConstructor
+public class AesCryptoConfig {
+
+    private final GatewayConfigProperties configProperties;
+
+
+
+    /**
+     * <p>
+     *     aes加密配置
+     *     此处设在配置类可以避免每次请求都去创建加密类,以此节约性能
+     * </p>
+     *
+     * @return {@link AES}
+     */
+    @Bean
+    public AES cryptoAes(){
+        return new AES(configProperties.getEncodeMode(), configProperties.getEncodePadding(),
+                // 密钥,可以自定义
+                configProperties.getEncodeKey().getBytes(),
+                // iv加盐,按照实际需求添加
+                configProperties.getEncodeIv().getBytes());
+    }
+}

+ 41 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/config/GatewayConfigProperties.java

@@ -0,0 +1,41 @@
+package com.kxs.gateway.api.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+
+import java.util.List;
+
+/**
+ * @author lengleng
+ * @date 2020/10/4
+ * <p>
+ * 网关配置文件
+ */
+@Data
+@RefreshScope
+@ConfigurationProperties("gateway")
+public class GatewayConfigProperties {
+
+	/**
+	 * 网关解密 秘钥 {@link com.kxs.gateway.api.filter.DecodeRequestGlobalFilter}
+	 */
+	@Value("${gateway.aes.encode-key}")
+	private String encodeKey;
+
+	@Value("${gateway.aes.encode-iv}")
+	private String encodeIv;
+
+	@Value("${gateway.aes.encode-mode}")
+	private String encodeMode;
+
+	@Value("${gateway.aes.encode-padding}")
+	private String encodePadding;
+
+	/**
+	 * 网关不需要校验验证码的客户端 {@link com.kxs.gateway.api.filter.ValidateCodeGatewayFilter}
+	 */
+	private List<String> ignoreClients;
+
+}

+ 46 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/config/GatewayConfiguration.java

@@ -0,0 +1,46 @@
+package com.kxs.gateway.api.config;
+
+import cn.hutool.crypto.symmetric.AES;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.kxs.gateway.api.filter.RequestDecoderFilter;
+import com.kxs.gateway.api.filter.ValidateCodeGatewayFilter;
+import com.kxs.gateway.api.handler.GlobalExceptionHandler;
+import com.kxs.gateway.api.handler.ImageCodeHandler;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.RedisTemplate;
+
+
+/**
+ * 网关配置
+ *
+ * @author Lxq
+ * @date 2023-11-11
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(GatewayConfigProperties.class)
+public class GatewayConfiguration {
+
+	@Bean
+	public RequestDecoderFilter requestDecoderFilter(AES cryptoAes) {
+		return new RequestDecoderFilter(cryptoAes);
+	}
+
+	@Bean
+	public ValidateCodeGatewayFilter validateCodeGatewayFilter(GatewayConfigProperties configProperties,
+															   ObjectMapper objectMapper, RedisTemplate<String, Object> redisTemplate) {
+		return new ValidateCodeGatewayFilter(configProperties, objectMapper, redisTemplate);
+	}
+
+	@Bean
+	public GlobalExceptionHandler globalExceptionHandler(ObjectMapper objectMapper) {
+		return new GlobalExceptionHandler(objectMapper);
+	}
+
+	@Bean
+	public ImageCodeHandler imageCodeHandler(RedisTemplate<String, Object>  redisTemplate) {
+		return new ImageCodeHandler(redisTemplate);
+	}
+
+}

+ 32 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/config/RateLimiterConfiguration.java

@@ -0,0 +1,32 @@
+package com.kxs.gateway.api.config;
+
+import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import reactor.core.publisher.Mono;
+
+import java.util.Objects;
+
+/**
+ * 限流配置
+ *
+ * @author Lxq
+ * @date 2023-11-11
+ */
+@Configuration(proxyBeanMethods = false)
+public class RateLimiterConfiguration {
+
+	/**
+	 * Remote addr key resolver key resolver.
+	 *
+	 * @link {<a href="https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-requestratelimiter-gatewayfilter-factory">...</a>}
+	 */
+	@Bean
+	public KeyResolver remoteAddrKeyResolver() {
+		return exchange -> Mono
+			.just(Objects.requireNonNull(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()))
+				.getAddress()
+				.getHostAddress());
+	}
+
+}

+ 35 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/config/RouterFunctionConfiguration.java

@@ -0,0 +1,35 @@
+package com.kxs.gateway.api.config;
+
+import com.kxs.gateway.api.handler.ImageCodeHandler;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.server.RequestPredicates;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.RouterFunctions;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+
+/**
+ * 验证码路由器
+ * webflux响应式编程
+ *
+ * @author 没秃顶的码农
+ * @date 2023/11/04
+ */
+@Slf4j
+@Configuration(proxyBeanMethods = false)
+@RequiredArgsConstructor
+public class RouterFunctionConfiguration {
+
+
+	private final ImageCodeHandler imageCodeHandler;
+
+	@Bean
+	public RouterFunction<ServerResponse> routerFunction() {
+		return RouterFunctions.route(
+				RequestPredicates.path("/v1/kxs/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), imageCodeHandler);
+	}
+}

+ 121 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/filter/RequestDecoderFilter.java

@@ -0,0 +1,121 @@
+package com.kxs.gateway.api.filter;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.crypto.symmetric.AES;
+import com.kxs.common.core.constant.enums.ErrorTypeEnum;
+import com.kxs.common.core.exception.AesDecodeException;
+import com.kxs.common.core.exception.CheckedException;
+import com.kxs.common.core.exception.ValidateCodeException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
+import org.springframework.cloud.gateway.support.BodyInserterContext;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ReactiveHttpOutputMessage;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
+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.lang.reflect.Field;
+import java.net.URI;
+
+/**
+ * @author lengleng
+ * @date 2019 /2/1 密码解密工具类
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class RequestDecoderFilter extends AbstractGatewayFilterFactory<Object> {
+
+	private final AES cryptoAes;
+
+	@Override
+	public GatewayFilter apply(Object config) {
+		return (exchange, chain) -> {
+			ServerHttpRequest request = exchange.getRequest();
+			//请求方法
+			HttpMethod method = request.getMethod();
+
+			if (method == HttpMethod.GET) {
+				//1 修改请求参数,并获取请求参数
+				try {
+					updateRequestParam(exchange);
+				} catch (Exception e) {
+					throw new CheckedException("解密异常");
+				}
+			}
+
+			if (method != HttpMethod.POST) {
+				return chain.filter(exchange);
+			}
+			//2 获取请求体,修改请求体
+			ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
+
+			Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
+				//解密请求体
+				try {
+					log.info("Post请求:{},待解密请求参数:{}", request.getURI().getPath(), body);
+					String encrypt = cryptoAes.decryptStr(body);
+
+					return Mono.just(encrypt);
+
+				} catch (Exception e) {
+					log.error(ErrorTypeEnum.DECRYPT_ERROR.getDescription(), e);
+					return Mono.error(new AesDecodeException(ErrorTypeEnum.DECRYPT_ERROR.getDescription()));
+				}
+			});
+
+			//3 创建BodyInserter修改请求体
+			BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
+			HttpHeaders headers = new HttpHeaders();
+			headers.putAll(exchange.getRequest().getHeaders());
+			headers.remove(HttpHeaders.CONTENT_LENGTH);
+			//4 创建CachedBodyOutputMessage并且把请求param加入
+			CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
+			return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
+				ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
+					@Override
+					public Flux<DataBuffer> getBody() {
+						return outputMessage.getBody();
+					}
+				};
+				return chain.filter(exchange.mutate().request(decorator).build());
+			}));
+		};
+	}
+
+	/**
+	 * 修改前端传的参数
+	 */
+	private void updateRequestParam(ServerWebExchange exchange) throws NoSuchFieldException, IllegalAccessException {
+		ServerHttpRequest request = exchange.getRequest();
+		//请求链接
+		URI uri = request.getURI();
+		//请求参数
+		String value = request.getQueryParams().getFirst("value");
+		log.info("Get请求:{},待解密请求参数:{}", uri.getPath(), value);
+		//判断是否有加密的参数 这里的约定是 param
+		if (CharSequenceUtil.isNotBlank(value)) {
+			//解密请求参数
+			String decryPtoData = cryptoAes.decryptStr(value);
+			log.info("Get请求:{},解密之后参数:{}", uri.getPath(), decryPtoData);
+			//使用反射强行拿出 URI 的 query
+			Field targetQuery = uri.getClass().getDeclaredField("value");
+			//授权
+			targetQuery.setAccessible(true);
+			//重新设置参数
+			targetQuery.set(uri, decryPtoData);
+		}
+	}
+
+}

+ 128 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/filter/ValidateCodeGatewayFilter.java

@@ -0,0 +1,128 @@
+package com.kxs.gateway.api.filter;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.kxs.common.core.constant.CacheConstants;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.core.constant.enums.ErrorTypeEnum;
+import com.kxs.common.core.exception.ValidateCodeException;
+import com.kxs.common.core.util.R;
+import com.kxs.common.core.util.WebUtils;
+import com.kxs.gateway.api.config.GatewayConfigProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+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.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import reactor.core.publisher.Mono;
+
+import java.util.Objects;
+
+
+/**
+ * 验证码网关过滤器
+ *
+ * @author Lxq
+ * @date 2023-11-11
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory<Object> {
+
+	private final GatewayConfigProperties configProperties;
+
+	private final ObjectMapper objectMapper;
+
+	private final RedisTemplate<String, Object> redisTemplate;
+
+	/**
+	 * 应用网关过滤器
+	 * @param config 配置对象
+	 * @return 网关过滤器
+	 */
+	@Override
+	public GatewayFilter apply(Object config) {
+		return (exchange, chain) -> {
+			ServerHttpRequest request = exchange.getRequest();
+			boolean isAuthToken = CharSequenceUtil.containsAnyIgnoreCase(request.getURI().getPath(),
+					SecurityConstants.OAUTH_TOKEN_URL);
+
+			if (!isAuthToken || isRefreshTokenRequest(request)) {
+				return chain.filter(exchange);
+			}
+
+			boolean isIgnoreClient = configProperties.getIgnoreClients().contains(WebUtils.getClientId(request));
+			try {
+				if (!isIgnoreClient) {
+					checkCode(request);
+				}
+			}
+			catch (Exception e) {
+				ServerHttpResponse response = exchange.getResponse();
+				response.setStatusCode(HttpStatus.PRECONDITION_REQUIRED);
+				response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+
+				final String errMsg = e.getMessage();
+				return response.writeWith(Mono.create(monoSink -> {
+					try {
+						byte[] bytes = objectMapper.writeValueAsBytes(R.failed(errMsg));
+						DataBuffer dataBuffer = response.bufferFactory().wrap(bytes);
+
+						monoSink.success(dataBuffer);
+					}
+					catch (JsonProcessingException jsonProcessingException) {
+						log.error("对象输出异常", jsonProcessingException);
+						monoSink.error(jsonProcessingException);
+					}
+				}));
+			}
+
+			return chain.filter(exchange);
+		};
+	}
+
+	/**
+	 * 判断是否为刷新令牌请求
+	 * @param request HTTP请求
+	 * @return 是否为刷新令牌请求
+	 */
+	private boolean isRefreshTokenRequest(ServerHttpRequest request) {
+		return CharSequenceUtil.equals(SecurityConstants.REFRESH_TOKEN, request.getQueryParams().getFirst("grant_type"));
+	}
+
+	/**
+	 * 检查验证码
+	 * @param request HTTP请求
+	 * @throws ValidateCodeException 验证码异常
+	 */
+	@SneakyThrows
+	private void checkCode(ServerHttpRequest request) {
+		String code = request.getQueryParams().getFirst("code");
+
+		if (CharSequenceUtil.isBlank(code)) {
+			throw new ValidateCodeException(ErrorTypeEnum.VERIFICATION_CODE_IS_EMPTY.getDescription());
+		}
+
+		String randomStr = CharSequenceUtil.blankToDefault(request.getQueryParams().getFirst("randomStr"),
+				request.getQueryParams().getFirst(SecurityConstants.SMS_PARAMETER_NAME));
+
+		String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
+
+		Object codeObj = redisTemplate.opsForValue().get(key);
+
+		if (ObjectUtil.isEmpty(codeObj) || !Objects.equals(code, codeObj)) {
+			throw new ValidateCodeException(ErrorTypeEnum.VERIFICATION_CODE_IS_ILLEGAL.getDescription());
+		}
+
+		redisTemplate.delete(key);
+	}
+
+}

+ 59 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/handler/GlobalExceptionHandler.java

@@ -0,0 +1,59 @@
+
+package com.kxs.gateway.api.handler;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.kxs.common.core.util.R;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
+import org.springframework.core.annotation.Order;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
+import reactor.core.publisher.Mono;
+
+/**
+ * 网关异常通用处理器,只作用在webflux 环境下 , 优先级低于 {@link ResponseStatusExceptionHandler} 执行
+ *
+ * @author 冷酱
+ * @date 2020/5/26
+ */
+@Slf4j
+@Order(-1)
+@RequiredArgsConstructor
+public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
+
+	private final ObjectMapper objectMapper;
+
+	@Override
+	public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
+		ServerHttpResponse response = exchange.getResponse();
+
+		if (response.isCommitted()) {
+			return Mono.error(ex);
+		}
+
+		// header set
+		response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+		if (ex instanceof ResponseStatusException) {
+			response.setStatusCode(((ResponseStatusException) ex).getStatusCode());
+		}
+
+		return response.writeWith(Mono.fromSupplier(() -> {
+			DataBufferFactory bufferFactory = response.bufferFactory();
+			try {
+				log.warn("Error Spring Cloud Gateway : {} {}", exchange.getRequest().getPath(), ex.getMessage());
+				return bufferFactory.wrap(objectMapper.writeValueAsBytes(R.failed(ex.getMessage())));
+			}
+			catch (JsonProcessingException e) {
+				log.error("Error writing response", ex);
+				return bufferFactory.wrap(new byte[0]);
+			}
+		}));
+	}
+
+}

+ 66 - 0
kxs-gateway/src/main/java/com/kxs/gateway/api/handler/ImageCodeHandler.java

@@ -0,0 +1,66 @@
+package com.kxs.gateway.api.handler;
+
+import cn.hutool.captcha.CaptchaUtil;
+import cn.hutool.captcha.LineCaptcha;
+import cn.hutool.captcha.generator.RandomGenerator;
+import com.kxs.common.core.constant.CacheConstants;
+import com.kxs.common.core.constant.SecurityConstants;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.util.FastByteArrayOutputStream;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.server.HandlerFunction;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * 验证码生成逻辑处理类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/11/03
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class ImageCodeHandler implements HandlerFunction<ServerResponse> {
+
+	private static final Integer DEFAULT_IMAGE_WIDTH = 100;
+
+	private static final Integer DEFAULT_IMAGE_HEIGHT = 40;
+
+	private final RedisTemplate<String, Object> redisTemplate;
+
+	@Override
+	public Mono<ServerResponse> handle(ServerRequest serverRequest) {
+
+		// 保存验证码信息
+		Optional<String> randomStr = serverRequest.queryParam("randomStr");
+
+		//生成验证码
+		RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
+		LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT);
+		lineCaptcha.setGenerator(randomGenerator);
+
+		redisTemplate.setKeySerializer(new StringRedisSerializer());
+		randomStr.ifPresent(s -> redisTemplate.opsForValue()
+				.set(CacheConstants.DEFAULT_CODE_KEY + s, lineCaptcha.getCode(), SecurityConstants.CODE_TIME, TimeUnit.SECONDS));
+
+		// 转换流信息写出
+		FastByteArrayOutputStream os = new FastByteArrayOutputStream();
+		lineCaptcha.write(os);
+		return ServerResponse.status(HttpStatus.OK)
+				.contentType(MediaType.IMAGE_JPEG)
+				.body(BodyInserters.fromResource(new ByteArrayResource(os.toByteArray())));
+
+	}
+
+}

+ 65 - 0
kxs-gateway/src/main/resources/application.yml

@@ -0,0 +1,65 @@
+
+server:
+  port: 9999
+spring:
+  cloud:
+    gateway:
+      routes:
+        #ADMIN 模块
+        - id: kxs-system-biz
+          uri: lb://kxs-system-biz
+          predicates:
+            - Path=/v1/kxs/admin/**
+          filters:
+            # 过滤前缀
+            - StripPrefix=3
+            # 限流配置
+            - name: RequestRateLimiter
+              args:
+                key-resolver: '#{@remoteAddrKeyResolver}'
+                redis-rate-limiter.replenishRate: 100
+                redis-rate-limiter.burstCapacity: 200
+            # 验证码处理
+#            - ValidateCodeGatewayFilter
+            # 解密过滤器
+            - RequestDecoderFilter
+        # 代码生成模块
+        - id: kxs-codegen
+          uri: lb://kxs-codegen:5002
+          predicates:
+            - Path=/v1/kxs/gen/**
+          filters:
+            # 过滤前缀
+            - StripPrefix=3
+        # 定时任务成模块
+        - id: sky-quartz
+          uri: lb://sky-quartz
+          predicates:
+            - Path=/job/**
+        # 固定路由转发配置 无修改
+        - id: openapi
+          uri: lb://sky-gateway
+          predicates:
+            - Path=/v3/api-docs/**
+          filters:
+            - RewritePath=/v3/api-docs/(?<path>.*), /$\{path}/$\{path}/v3/api-docs
+
+
+    discovery:
+      client:
+        simple:
+          instances:
+            kxs-system-biz: #负载均衡
+              - uri: http://127.0.0.1:8080
+            kxs-codegen:
+              - uri: http://127.0.0.1:5002
+
+gateway:
+  aes:
+    encode-key: 'CBTU1dD4Kd5pyiGWTsI10jRQ3SvKusSV'
+    encode-iv: 'DYgjCEIMVrj2W9xN'
+    encode-mode: 'CBC'
+    encode-padding: 'PKCS7Padding'
+  #不校验验证码的客户端
+  ignore-clients:
+    - sky

+ 77 - 0
kxs-gateway/src/main/resources/logback-spring.xml

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2020 sky4cloud Authors. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<configuration debug="false" scan="false">
+	<springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue=""/>
+	<property name="log.path" value="logs/${spring.application.name}"/>
+	<!-- 彩色日志格式 -->
+	<property name="CONSOLE_LOG_PATTERN"
+			  value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+	<!-- 彩色日志依赖的渲染类 -->
+	<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
+	<conversionRule conversionWord="wex"
+					converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
+	<conversionRule conversionWord="wEx"
+					converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
+	<!-- Console log output -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- Log file debug output -->
+	<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/debug.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+			<maxFileSize>50MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- Log file error output -->
+	<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/error.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+			<maxFileSize>50MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<level>ERROR</level>
+		</filter>
+	</appender>
+
+	<!--nacos 心跳 INFO 屏蔽-->
+	<logger name="com.alibaba.nacos" level="OFF">
+		<appender-ref ref="error"/>
+	</logger>
+
+	<!-- Level: FATAL 0  ERROR 3  WARN 4  INFO 6  DEBUG 7 -->
+	<root level="INFO">
+		<appender-ref ref="console"/>
+		<appender-ref ref="debug"/>
+		<appender-ref ref="error"/>
+	</root>
+</configuration>

+ 24 - 0
kxs-system/kxs-system-biz/src/main/java/com/kxs/system/biz/KxsAdminApplication.java

@@ -0,0 +1,24 @@
+package com.kxs.system.biz;
+
+import com.kxs.common.security.annotation.EnableAuthResourceServer;
+import com.kxs.common.security.annotation.EnableLoginResourceServer;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2022年06月21日
+ * <p>
+ * 用户统一管理系统
+ */
+@SpringBootApplication(scanBasePackages = {"com.kxs.common.**", "com.kxs.system.**"})
+@EnableLoginResourceServer
+@EnableAuthResourceServer
+public class KxsAdminApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(KxsAdminApplication.class, args);
+	}
+
+}

+ 64 - 0
kxs-system/kxs-system-biz/src/main/resources/application.yml

@@ -0,0 +1,64 @@
+server:
+  port: 8080
+
+spring:
+  profiles:
+    active: dev
+  application:
+    name: kxs-admin
+  cache:
+    type: REDIS
+    redis:
+      time-to-live: 1h # 设置过期时间为 1 小时
+  data:
+    redis:
+      host: localhost
+      repositories:
+        enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度\
+  datasource:
+    type: com.zaxxer.hikari.HikariDataSource
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: root
+    password: root
+    url: jdbc:mysql://localhost:3306/kxs?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
+
+  # Servlet 配置
+  servlet:
+    # 文件上传相关配置项
+    multipart:
+      max-file-size: 16MB # 单个文件大小
+      max-request-size: 32MB # 设置总上传的文件大小
+
+--- #################### 接口文档配置 ####################
+
+springdoc:
+  api-docs:
+    enabled: true
+    path: /v3/api-docs
+  swagger-ui:
+    enabled: true
+    path: /swagger-ui
+  default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档
+
+knife4j:
+  enable: true
+  setting:
+    language: zh_cn
+
+# mybaits-plus配置
+mybatis-plus:
+  mapper-locations: classpath*:/mapper/*.xml
+  global-config:
+    banner: false # 关闭控制台的 Banner 打印
+    db-config:
+      id-type: auto
+      table-underline: true
+      logic-delete-value: 1  # 逻辑已删除值(默认为 1)
+      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
+  type-handlers-package: com.kxs.common.mybatis.handler
+  configuration:
+    map-underscore-to-camel-case: true
+    shrink-whitespaces-in-sql: true
+
+
+

+ 20 - 0
pom.xml

@@ -12,6 +12,8 @@
 
     <properties>
         <spring-boot.version>3.1.5</spring-boot.version>
+        <spring-cloud.version>2022.0.4</spring-cloud.version>
+        <spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <maven.compiler.source>17</maven.compiler.source>
         <maven.compiler.target>17</maven.compiler.target>
@@ -70,10 +72,12 @@
         <module>kxs-app</module>
         <module>kxs-system</module>
         <module>kxs-codegen</module>
+        <module>kxs-gateway</module>
     </modules>
 
     <dependencyManagement>
         <dependencies>
+
             <!-- spring boot 依赖 -->
             <dependency>
                 <groupId>org.springframework.boot</groupId>
@@ -82,6 +86,22 @@
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
+            <!-- spring cloud 依赖 -->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- spring cloud alibaba 依赖 -->
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
             <!--web 模块-->
             <dependency>
                 <groupId>org.springframework.boot</groupId>