浏览代码

工程迁移

by su 3 年之前
当前提交
9b1b79c0d0

+ 131 - 0
pom.xml

@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.3.12.RELEASE</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>com.fdkankan</groupId>
+    <artifactId>4dkankan-gateway</artifactId>
+    <version>2.0.0</version>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <spring.cloud-version>Hoxton.SR8</spring.cloud-version>
+    </properties>
+
+    <dependencies>
+
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.boot</groupId>-->
+<!--            <artifactId>spring-boot-starter-web</artifactId>-->
+<!--        </dependency>-->
+
+        <dependency>
+            <groupId>com.fdkankan</groupId>
+            <artifactId>4dkankan-common-utils</artifactId>
+            <version>2.0.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fdkankan</groupId>
+            <artifactId>4dkankan-utils-redis</artifactId>
+            <version>2.0.0</version>
+        </dependency>
+
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <scope>runtime</scope>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-gateway</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-mongodb</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <!--nacos 客户端 配置中心-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <dependencyManagement>
+
+        <dependencies>
+
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>2.2.7.RELEASE</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring.cloud-version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 31 - 0
src/main/java/com/fdkankan/gateway/GatewayApplication.java

@@ -0,0 +1,31 @@
+package com.fdkankan.gateway;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+@EnableDiscoveryClient
+//@Controller
+@RefreshScope
+@ComponentScan(basePackages = {"com.fdkankan.redis","com.fdkankan.gateway"})
+public class GatewayApplication {
+
+    public static void main(String[] args) {
+        ConfigurableApplicationContext context = SpringApplication.run(GatewayApplication.class, args);
+            String test = context.getEnvironment().getProperty("foo.bar");
+            System.out.println(test);
+    }
+
+
+//    @RequestMapping("/test/test")
+//    @ResponseBody
+//    public String test(){
+//        return fooBar;
+//    }
+
+}

+ 83 - 0
src/main/java/com/fdkankan/gateway/config/CustomErrorWebFluxAutoConfiguration.java

@@ -0,0 +1,83 @@
+package com.fdkankan.gateway.config;
+
+import com.fdkankan.gateway.exception.JsonErrorWebExceptionHandler;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.autoconfigure.condition.SearchStrategy;
+import org.springframework.boot.autoconfigure.web.ResourceProperties;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
+import org.springframework.boot.web.reactive.error.ErrorAttributes;
+import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.codec.ServerCodecConfigurer;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+import org.springframework.web.reactive.result.view.ViewResolver;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 网关全局异常配置
+ */
+@Configuration
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
+@ConditionalOnClass(WebFluxConfigurer.class)
+@AutoConfigureBefore(WebFluxAutoConfiguration.class)
+@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
+public class CustomErrorWebFluxAutoConfiguration {
+
+    private final ServerProperties serverProperties;
+
+    private final ApplicationContext applicationContext;
+
+    private final ResourceProperties resourceProperties;
+
+    private final List<ViewResolver> viewResolvers;
+
+    private final ServerCodecConfigurer serverCodecConfigurer;
+
+    public CustomErrorWebFluxAutoConfiguration(ServerProperties serverProperties,
+                                               ResourceProperties resourceProperties,
+                                               ObjectProvider<ViewResolver> viewResolversProvider,
+                                               ServerCodecConfigurer serverCodecConfigurer,
+                                               ApplicationContext applicationContext) {
+        this.serverProperties = serverProperties;
+        this.applicationContext = applicationContext;
+        this.resourceProperties = resourceProperties;
+        this.viewResolvers = viewResolversProvider.orderedStream()
+                .collect(Collectors.toList());
+        this.serverCodecConfigurer = serverCodecConfigurer;
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class,
+            search = SearchStrategy.CURRENT)
+    @Order(-1)
+    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
+        // TODO 这里完成自定义ErrorWebExceptionHandler实现逻辑
+        JsonErrorWebExceptionHandler exceptionHandler = new JsonErrorWebExceptionHandler(
+                errorAttributes,
+                resourceProperties,
+                this.serverProperties.getError(),
+                applicationContext);
+        exceptionHandler.setViewResolvers(this.viewResolvers);
+        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
+        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
+        return exceptionHandler;
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
+    public DefaultErrorAttributes errorAttributes() {
+        return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
+    }
+}

+ 111 - 0
src/main/java/com/fdkankan/gateway/config/DynamicRouteServiceImpl.java

@@ -0,0 +1,111 @@
+package com.fdkankan.gateway.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
+import org.springframework.cloud.gateway.route.RouteDefinition;
+import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
+import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.ApplicationEventPublisherAware;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * 动态更新路由网关service
+ * 1)实现一个Spring提供的事件推送接口ApplicationEventPublisherAware
+ * 2)提供动态路由的基础方法,可通过获取bean操作该类的方法。该类提供新增路由、更新路由、删除路由,然后实现发布的功能。
+ */
+@Slf4j
+@Service
+public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
+    @Autowired
+    private RouteDefinitionWriter routeDefinitionWriter;
+    @Autowired
+    private RouteDefinitionLocator routeDefinitionLocator;
+
+    /**
+     * 发布事件
+     */
+    @Autowired
+    private ApplicationEventPublisher publisher;
+
+    @Override
+    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
+        this.publisher = applicationEventPublisher;
+    }
+
+    /**
+     * 删除路由
+     * @param id
+     * @return
+     */
+    public String delete(String id) {
+        try {
+            log.info("gateway delete route id {}",id);
+            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
+            this.publisher.publishEvent(new RefreshRoutesEvent(this));
+            return "delete success";
+        } catch (Exception e) {
+            return "delete fail";
+        }
+    }
+
+    /**
+     * 更新路由
+     * @param definitions
+     * @return
+     */
+    public String updateList(List<RouteDefinition> definitions) {
+        log.info("gateway update route {}",definitions);
+        // 删除缓存routerDefinition
+        List<RouteDefinition> routeDefinitionsExits =  routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
+        if (!CollectionUtils.isEmpty(routeDefinitionsExits)) {
+            routeDefinitionsExits.forEach(routeDefinition -> {
+                log.info("delete routeDefinition:{}", routeDefinition);
+                delete(routeDefinition.getId());
+            });
+        }
+        definitions.forEach(definition -> {
+            updateById(definition);
+        });
+        return "success";
+    }
+
+    /**
+     * 更新路由
+     * @param definition
+     * @return
+     */
+    public String updateById(RouteDefinition definition) {
+        try {
+            log.info("gateway update route {}",definition);
+            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
+        } catch (Exception e) {
+            return "update fail,not find route  routeId: "+definition.getId();
+        }
+        try {
+            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
+            this.publisher.publishEvent(new RefreshRoutesEvent(this));
+            return "success";
+        } catch (Exception e) {
+            return "update route fail";
+        }
+    }
+
+    /**
+     * 增加路由
+     * @param definition
+     * @return
+     */
+    public String add(RouteDefinition definition) {
+        log.info("gateway add route {}",definition);
+        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
+        this.publisher.publishEvent(new RefreshRoutesEvent(this));
+        return "success";
+    }
+}
+

+ 98 - 0
src/main/java/com/fdkankan/gateway/config/DynamicRouteServiceImplByNacos.java

@@ -0,0 +1,98 @@
+package com.fdkankan.gateway.config;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.nacos.api.NacosFactory;
+import com.alibaba.nacos.api.config.ConfigService;
+import com.alibaba.nacos.api.config.listener.Listener;
+import com.alibaba.nacos.api.exception.NacosException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.route.RouteDefinition;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.Executor;
+
+/**
+ *
+ * 通过nacos下发动态路由配置,监听Nacos中gateway-route配置
+ *
+ */
+@Component
+@Slf4j
+@DependsOn({"nacosConfig"}) // 依赖于gatewayConfig bean
+public class DynamicRouteServiceImplByNacos {
+
+    @Autowired
+    private DynamicRouteServiceImpl dynamicRouteService;
+
+    private ConfigService configService;
+
+    @PostConstruct
+    public void init() {
+        log.info("gateway route init...");
+        try{
+            configService = initConfigService();
+            if(configService == null){
+                log.warn("initConfigService fail");
+                return;
+            }
+            String configInfo = configService.getConfig(NacosConfig.NACOS_ROUTE_DATA_ID, NacosConfig.NACOS_ROUTE_GROUP, NacosConfig.DEFAULT_TIMEOUT);
+            log.info("获取网关当前配置:\r\n{}",configInfo);
+            List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
+            for(RouteDefinition definition : definitionList){
+                log.info("update route : {}",definition.toString());
+                dynamicRouteService.add(definition);
+            }
+        } catch (Exception e) {
+            log.error("初始化网关路由时发生错误",e);
+        }
+        dynamicRouteByNacosListener(NacosConfig.NACOS_ROUTE_DATA_ID, NacosConfig.NACOS_ROUTE_GROUP);
+    }
+
+    /**
+     * 监听Nacos下发的动态路由配置
+     * @param dataId
+     * @param group
+     */
+    public void dynamicRouteByNacosListener (String dataId, String group){
+        try {
+            configService.addListener(dataId, group, new Listener()  {
+                @Override
+                public void receiveConfigInfo(String configInfo) {
+                    log.info("进行网关更新:\n\r{}",configInfo);
+                    List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
+                    log.info("update route : {}",definitionList.toString());
+                    dynamicRouteService.updateList(definitionList);
+                }
+                @Override
+                public Executor getExecutor() {
+                    log.info("getExecutor\n\r");
+                    return null;
+                }
+            });
+        } catch (NacosException e) {
+            log.error("从nacos接收动态路由配置出错!!!",e);
+        }
+    }
+
+    /**
+     * 初始化网关路由 nacos config
+     * @return
+     */
+    private ConfigService initConfigService(){
+        try{
+            Properties properties = new Properties();
+            properties.setProperty("serverAddr", NacosConfig.NACOS_SERVER_ADDR);
+            properties.setProperty("namespace", NacosConfig.NACOS_NAMESPACE);
+            return configService= NacosFactory.createConfigService(properties);
+        } catch (Exception e) {
+            log.error("初始化网关路由时发生错误",e);
+            return null;
+        }
+    }
+}
+

+ 39 - 0
src/main/java/com/fdkankan/gateway/config/NacosConfig.java

@@ -0,0 +1,39 @@
+package com.fdkankan.gateway.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class NacosConfig {
+
+    public static final long DEFAULT_TIMEOUT = 30000;
+
+    public static String NACOS_SERVER_ADDR;
+
+    public static String NACOS_NAMESPACE;
+
+    public static String NACOS_ROUTE_DATA_ID;
+
+    public static String NACOS_ROUTE_GROUP;
+
+    @Value("${spring.cloud.nacos.config.server-addr}")
+    public void setNacosServerAddr(String nacosServerAddr){
+        NACOS_SERVER_ADDR = nacosServerAddr;
+    }
+
+    @Value("${spring.cloud.nacos.config.namespace}")
+    public void setNacosNamespace(String nacosNamespace){
+        NACOS_NAMESPACE = nacosNamespace;
+    }
+
+    @Value("${nacos.gateway.route.config.data-id}")
+    public void setNacosRouteDataId(String nacosRouteDataId){
+        NACOS_ROUTE_DATA_ID = nacosRouteDataId;
+    }
+
+    @Value("${nacos.gateway.route.config.group}")
+    public void setNacosRouteGroup(String nacosRouteGroup){
+        NACOS_ROUTE_GROUP = nacosRouteGroup;
+    }
+
+}

+ 117 - 0
src/main/java/com/fdkankan/gateway/dto/TmDeveloperDto.java

@@ -0,0 +1,117 @@
+package com.fdkankan.gateway.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * <p>
+ * 开发者信息表
+ * </p>
+ *
+ * @author ${author}
+ * @since 2021-12-23
+ */
+@Data
+public class TmDeveloperDto implements Serializable {
+
+    private static final long serialVersionUID=1L;
+
+    /**
+     * 开发者id
+     */
+    private String developerId;
+
+    /**
+     * 开发者应用id
+     */
+    private String appId;
+
+    /**
+     * 开发者所属公司统一社会信用代码
+     */
+    private String companyCreditCode;
+
+    /**
+     * 开发者应用密钥
+     */
+    private String appSecret;
+
+    /**
+     * 模式
+     */
+    private String state;
+
+    /**
+     * 使用的微信小程序的app id
+     */
+    private String wxAppId;
+
+    /**
+     * 开发者所属公司
+     */
+    private String company;
+
+    /**
+     * 开发者所属公司固话号码
+     */
+    private String tel;
+
+    /**
+     * 开发者所属公司法人名字
+     */
+    private String legalPersonName;
+
+    /**
+     * 开发者所属公司法人手机号码
+     */
+    private String legalPersonPhone;
+
+    /**
+     * 是否可用: 2审批通过 1 待审批 0 不可用
+     */
+    private Integer enable;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 最新修改时间
+     */
+    private Date lastModifyDatetime;
+
+    /**
+     * openApi回调开发者接口的url
+     */
+    private String callBackUrl;
+
+    /**
+     * 管理者手机号
+     */
+    private String adminPhone;
+
+    /**
+     * 管理者邮箱
+     */
+    private String adminEmail;
+
+    /**
+     * 微信小程序校验文件链接
+     */
+    private String wxMiniProgramFile;
+
+    /**
+     * 所属领域
+     */
+    private String domainType;
+
+    /**
+     * 是否需要将返回参数拼到url上:Y 需要 N不需要
+     */
+    private String respondToUrl;
+
+
+}

+ 15 - 0
src/main/java/com/fdkankan/gateway/dto/User.java

@@ -0,0 +1,15 @@
+package com.fdkankan.gateway.dto;
+
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@ToString
+public class User {
+
+    private String id;
+    private String name;
+    private Integer age;
+    private String sex;
+
+}

+ 60 - 0
src/main/java/com/fdkankan/gateway/exception/JsonErrorWebExceptionHandler.java

@@ -0,0 +1,60 @@
+package com.fdkankan.gateway.exception;
+
+
+import com.fdkankan.common.constant.ServerCode;
+import com.fdkankan.common.exception.BusinessException;
+import org.springframework.boot.autoconfigure.web.ErrorProperties;
+import org.springframework.boot.autoconfigure.web.ResourceProperties;
+import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
+import org.springframework.boot.web.error.ErrorAttributeOptions;
+import org.springframework.boot.web.reactive.error.ErrorAttributes;
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.reactive.function.server.*;
+
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+public class JsonErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
+
+    public JsonErrorWebExceptionHandler(ErrorAttributes errorAttributes,
+                                        ResourceProperties resourceProperties,
+                                        ErrorProperties errorProperties,
+                                        ApplicationContext applicationContext) {
+        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
+    }
+
+    @Override
+    protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
+
+        // 这里其实可以根据异常类型进行定制化逻辑
+        Throwable error = super.getError(request);
+        Map<String, Object> errorAttributes = new HashMap<>(8);
+
+        if(error instanceof BusinessException){
+            errorAttributes.put("code", ((BusinessException) error).getCode());
+            errorAttributes.put("message", ((BusinessException) error).getMessage());
+        }else{
+            errorAttributes.put("code", ServerCode.SYSTEM_ERROR.code());
+            errorAttributes.put("message", ServerCode.SYSTEM_ERROR.message());
+        }
+        errorAttributes.put("method", request.methodName());
+        errorAttributes.put("path", request.path());
+        errorAttributes.put("timestamp", Calendar.getInstance().getTimeInMillis());
+        return errorAttributes;
+    }
+
+    @Override
+    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
+        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
+    }
+
+    @Override
+    protected int getHttpStatus(Map<String, Object> errorAttributes) {
+        // 这里其实可以根据errorAttributes里面的属性定制HTTP响应码
+        return HttpStatus.INTERNAL_SERVER_ERROR.value();
+    }
+
+
+}

+ 24 - 0
src/main/java/com/fdkankan/gateway/factory/AuthFilterGatewayFilterFactory.java

@@ -0,0 +1,24 @@
+package com.fdkankan.gateway.factory;
+
+import com.fdkankan.gateway.filter.AppAuthFilter;
+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.stereotype.Component;
+
+/**
+ * 第三方系统访问过滤器
+ */
+@Component
+public class AuthFilterGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
+
+    @Autowired
+    private AppAuthFilter appAuthFilter;
+
+    @Override
+    public GatewayFilter apply(Object filter) {
+        return appAuthFilter;
+    }
+}
+
+

+ 25 - 0
src/main/java/com/fdkankan/gateway/factory/OfficialTokenFilterGatewayFilterFactory.java

@@ -0,0 +1,25 @@
+package com.fdkankan.gateway.factory;
+
+import com.fdkankan.gateway.filter.AppAuthFilter;
+import com.fdkankan.gateway.filter.OfficialTokenFilter;
+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.stereotype.Component;
+
+/**
+ * 第三方系统访问过滤器
+ */
+@Component
+public class OfficialTokenFilterGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
+
+    @Autowired
+    private OfficialTokenFilter officialTokenFilter;
+
+    @Override
+    public GatewayFilter apply(Object filter) {
+        return officialTokenFilter;
+    }
+}
+
+

+ 254 - 0
src/main/java/com/fdkankan/gateway/filter/AccessLogFilter.java

@@ -0,0 +1,254 @@
+package com.fdkankan.gateway.filter;
+
+import cn.hutool.core.util.StrUtil;
+import com.fdkankan.gateway.log.GatewayLog;
+import com.fdkankan.gateway.util.WebUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.reactivestreams.Publisher;
+import org.springframework.beans.factory.annotation.Autowired;
+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.route.Route;
+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.data.mongodb.core.MongoTemplate;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+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.util.MultiValueMap;
+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.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 请求、响应过滤器
+ */
+@Slf4j
+@Component
+public class AccessLogFilter  implements GlobalFilter, Ordered {
+
+    private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
+
+    @Autowired
+    private MongoTemplate mongoTemplate;
+
+    @Override
+    public int getOrder() {
+        return -100;
+    }
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+
+        ServerHttpRequest request = exchange.getRequest();
+        // 请求路径
+        String requestPath = request.getPath().pathWithinApplication().value();
+
+        Route route = getGatewayRoute(exchange);
+
+
+        String ipAddress = WebUtil.getIpAddress(request);
+
+        GatewayLog gatewayLog = new GatewayLog();
+        gatewayLog.setSchema(request.getURI().getScheme());
+        gatewayLog.setRequestMethod(request.getMethodValue());
+        gatewayLog.setRequestPath(requestPath);
+        gatewayLog.setTargetServer(route.getId());
+        gatewayLog.setRequestTime(new Date());
+        gatewayLog.setIp(ipAddress);
+
+        MediaType mediaType = request.getHeaders().getContentType();
+
+        if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){
+            return writeBodyLog(exchange, chain, gatewayLog);
+        }else{
+            return writeBasicLog(exchange, chain, gatewayLog);
+        }
+    }
+
+    private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) {
+        StringBuilder builder = new StringBuilder();
+        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
+        for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
+            builder.append(entry.getKey()).append("=").append(StrUtil.join(",", entry.getValue()));
+        }
+        accessLog.setRequestBody(builder.toString());
+
+        //获取响应体
+        ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
+
+        return chain.filter(exchange.mutate().response(decoratedResponse).build())
+                .then(Mono.fromRunnable(() -> {
+                    // 打印日志
+                    writeAccessLog(accessLog);
+                }));
+    }
+
+
+    /**
+     * 参考: org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
+     * @param exchange
+     * @param chain
+     * @param gatewayLog
+     * @return
+     */
+    @SuppressWarnings("unchecked")
+    private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
+        ServerRequest serverRequest = ServerRequest.create(exchange,messageReaders);
+
+        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
+                .flatMap(body ->{
+                    gatewayLog.setRequestBody(body);
+                    return Mono.just(body);
+                });
+
+        // 通过 BodyInserter 插入 body(支持修改body), 避免 request body 只能获取一次
+        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
+        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);
+
+        return bodyInserter.insert(outputMessage,new BodyInserterContext())
+                .then(Mono.defer(() -> {
+                    // 重新封装请求
+                    ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
+
+                    // 记录响应日志
+                    ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
+
+                    // 记录普通的
+                    return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
+                            .then(Mono.fromRunnable(() -> {
+                                // 打印日志
+                                writeAccessLog(gatewayLog);
+                            }));
+                }));
+    }
+
+    /**
+     * 打印日志
+     * @param gatewayLog 网关日志
+     */
+    private void writeAccessLog(GatewayLog gatewayLog) {
+        log.info(gatewayLog.toString());
+        //日志写入mongodb
+        mongoTemplate.insert(gatewayLog, "gatewayLog");
+    }
+
+
+
+    private Route getGatewayRoute(ServerWebExchange exchange) {
+        return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
+    }
+
+
+    /**
+     * 请求装饰器,重新计算 headers
+     * @param exchange
+     * @param headers
+     * @param outputMessage
+     * @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<DataBuffer> getBody() {
+                return outputMessage.getBody();
+            }
+        };
+    }
+
+
+    /**
+     * 记录响应日志
+     * 通过 DataBufferFactory 解决响应体分段传输问题。
+     */
+    private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
+        ServerHttpResponse response = exchange.getResponse();
+        DataBufferFactory bufferFactory = response.bufferFactory();
+
+        return new ServerHttpResponseDecorator(response) {
+
+            @Override
+            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
+                if (body instanceof Flux) {
+                    Date responseTime = Calendar.getInstance().getTime();
+                    gatewayLog.setResponseTime(responseTime);
+                    // 计算执行时间
+                    long executeTime = (responseTime.getTime() - gatewayLog.getRequestTime().getTime());
+
+                    gatewayLog.setExecuteTime(executeTime);
+
+                    // 获取响应类型,如果是 json 就打印
+                    String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
+
+                    if (this.getStatusCode().equals(HttpStatus.OK)
+                            && StrUtil.isNotBlank(originalResponseContentType)
+                            && originalResponseContentType.contains("application/json")) {
+
+                        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
+                        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
+
+                            // 合并多个流集合,解决返回体分段传输
+                            DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
+                            DataBuffer join = dataBufferFactory.join(dataBuffers);
+                            byte[] content = new byte[join.readableByteCount()];
+                            join.read(content);
+
+                            // 释放掉内存
+                            DataBufferUtils.release(join);
+                            String responseResult = new String(content, StandardCharsets.UTF_8);
+
+                            gatewayLog.setResponseData(responseResult);
+
+
+                            return bufferFactory.wrap(content);
+                        }));
+                    }
+                }
+                // if body is not a flux. never got there.
+                return super.writeWith(body);
+            }
+        };
+    }
+}

+ 101 - 0
src/main/java/com/fdkankan/gateway/filter/AppAuthFilter.java

@@ -0,0 +1,101 @@
+package com.fdkankan.gateway.filter;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fdkankan.common.constant.ErrorCode;
+import com.fdkankan.redis.constant.RedisKey;
+import com.fdkankan.common.exception.BusinessException;
+import com.fdkankan.common.utils.DataUtils;
+import com.fdkankan.common.utils.SHAUtils;
+import com.fdkankan.gateway.dto.TmDeveloperDto;
+import com.fdkankan.redis.util.RedisUtil;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.core.Ordered;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+@Log4j2
+public class AppAuthFilter implements GatewayFilter, Ordered {
+
+    @Autowired
+    RedisUtil redisUtil;
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+
+        StringBuilder logBuilder = new StringBuilder();
+        Map<String, String> params = parseRequest(exchange, logBuilder);
+        if (!CollUtil.isEmpty(params)) {
+            String token = params.get("token");
+            String appId = params.get("appId");
+            Long timeStamp = DataUtils.getLongReturnNullIfNotExit(params.get("timeStamp"));
+            log.info("鉴权流程获取到的appid={},请求路径:{}" , appId , exchange.getRequest().getURI());
+            //先获取校验app id的合法性
+            String developerJson = (String)redisUtil.get(String.format(RedisKey.TM_DEVELOPER, appId));
+            if(StrUtil.isBlank(developerJson)){
+                throw new BusinessException(ErrorCode.APP_ID_ILLEGAL);
+            }
+            //校验token的有效性
+            TmDeveloperDto tmDeveloperDto = JSONUtil.toBean(developerJson, TmDeveloperDto.class);
+            String tmpToken = tmDeveloperDto.getAppId() + tmDeveloperDto.getAppSecret() + timeStamp.toString();
+            String enCodeToken = SHAUtils.getSHA256(tmpToken);
+            if(!StrUtil.equals(enCodeToken , token)){
+                throw new BusinessException(ErrorCode.TOKEN_ILLEGAL);
+            }
+            //校验数据是否已经被串改放到controller里面校验
+            log.info("------网关基础校验通过-------");
+        }else{
+            throw new BusinessException(ErrorCode.PARAM_REQUIRED);
+        }
+        return chain.filter(exchange);
+    }
+
+    private Map<String, String> parseRequest(ServerWebExchange exchange, StringBuilder logBuilder) {
+        ServerHttpRequest serverHttpRequest = exchange.getRequest();
+        String method = serverHttpRequest.getMethodValue().toUpperCase();
+        logBuilder.append(method).append(",").append(serverHttpRequest.getURI());
+        MultiValueMap<String, String> query = serverHttpRequest.getQueryParams();
+        Map<String, String> params = new HashMap<>();
+        query.forEach((k, v) -> {
+            params.put(k, v.get(0));
+        });
+        if ("POST".equals(method)) {
+            String body = exchange.getAttributeOrDefault("cachedRequestBody", "");
+            if (StrUtil.isNotBlank(body)) {
+                logBuilder.append(",body=").append(body);
+                String[] kvArray = body.split("&");
+                for (String kv : kvArray) {
+                    if (kv.indexOf("=") >= 0) {
+                        String k = kv.split("=")[0];
+                        String v = kv.split("=")[1];
+                        if (!params.containsKey(k)) {
+                            try {
+                                params.put(k, URLDecoder.decode(v, "UTF-8"));
+                            } catch (UnsupportedEncodingException e) {
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return params;
+    }
+
+    @Override
+    public int getOrder() {
+        return -20;
+    }
+}

+ 180 - 0
src/main/java/com/fdkankan/gateway/filter/OfficialTokenFilter.java

@@ -0,0 +1,180 @@
+package com.fdkankan.gateway.filter;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fdkankan.common.constant.ServerCode;
+import com.fdkankan.common.response.ResultData;
+import com.fdkankan.common.utils.JwtUtil;
+import com.fdkankan.redis.util.RedisUtil;
+import io.jsonwebtoken.Claims;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+@Slf4j
+@Component
+@RefreshScope
+public class OfficialTokenFilter implements GatewayFilter, Ordered {
+
+    private static final String TOKEN = "token";
+
+    private static final Map<String, List<String>> permissionMap = new HashMap<>();
+
+    private static final List<String> ignoreAuthUrls = new ArrayList<>();
+
+    @Value("${tokenCheck:check}")
+    private String tokenCheck;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    static {
+        List<String> userPermissions = new ArrayList<>();
+        permissionMap.put("user", userPermissions);
+        userPermissions.add("/api/user");
+        userPermissions.add("/api/scene/edit");
+        userPermissions.add("/api/scene/edit");
+        userPermissions.add("/api/order/scanPay");
+
+        List<String> managerPermissions = new ArrayList<>();
+        permissionMap.put("manager", managerPermissions);
+        userPermissions.add("/api/manager");
+
+        List<String> agentPermissions = new ArrayList<>();
+        permissionMap.put("agent", agentPermissions);
+        userPermissions.add("/api/agent");
+
+        List<String> appPermissions = new ArrayList<>();
+        permissionMap.put("app", appPermissions);
+        userPermissions.add("/api/app");
+
+
+        ignoreAuthUrls.add("/api/sso");
+
+    }
+
+
+    /**
+     * 忽略过滤的路径
+     */
+
+//    @Value("${com.cuslink.ignoreAuthUrls}")
+//    private String ignoreAuthUrls;
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        ServerHttpRequest request = exchange.getRequest();
+        ServerHttpResponse response = exchange.getResponse();
+        if(!"check".equals(tokenCheck)){
+            return chain.filter(exchange);
+        }
+        // 获取当前请求路径
+        String path = request.getURI().getPath();
+        log.info("当前路径为" + path + ",判断是否为忽略路径.");
+        // 查询是否是忽略路径
+        boolean ignore = decideIgnore(path);
+        if (ignore) {
+            // 忽略路径直接放行
+            log.info("当前路径为" + path + ",是忽略过滤路径,直接放行!");
+            return chain.filter(exchange);
+        } else {
+            ServerCode serverCode = null;
+            log.info("当前路径为" + path + ",不是忽略过滤路径,开始校验!");
+            // 获取当前请求中的token
+            String token = request.getHeaders().getFirst(TOKEN);
+            if(StrUtil.isBlank(token)){
+                serverCode = ServerCode.TOKEN_NOT_FOUND;
+                DataBuffer buffer = setResponseInfo(response, ResultData.error(serverCode.code(), serverCode.message()));
+                return response.writeWith(Mono.just(buffer));
+            }else{
+                //验证token是否被篡改
+                Claims claims = null;
+                try {
+                    claims = JwtUtil.parseJWT(token);
+                }catch (Exception e){
+                    serverCode = ServerCode.TOKEN_ILLEGAL;
+                    DataBuffer buffer = setResponseInfo(response, ResultData.error(serverCode.code(), serverCode.message()));
+                    return response.writeWith(Mono.just(buffer));
+                }
+
+                String userName = (String)claims.get("userName");
+                String loginType = (String)claims.get("loginType");
+
+                //查询redis,token是否存在
+//                String key = String.format(RedisKey.TOKEN_USER_TYPE, userName, loginType);
+//                boolean exist = redisTemplate.hasKey(key);
+//                if(!exist){
+//                    DataBuffer buffer = setResponseInfo(response, ResultData.error(ServerCode.TOKEN_NOT_FOUND.code(), ServerCode.TOKEN_NOT_FOUND.message()));
+//                    return response.writeWith(Mono.just(buffer));
+//                }
+
+                // TODO: 2021/12/21  校验资源权限  用户资源可以从token中取,这里暂时先写死
+                //根据用户ID查询角色列表
+                //根据角色查询可访问资源权限列表
+                //遍历权限列表,对比path,如果匹配上,则放行
+                List<String> permissions = permissionMap.get(loginType);
+                Boolean isPermission = true;
+//                for (String permission : permissions) {
+//                    if(path.contains(permission)){
+//                        isPermission = true;
+//                        break;
+//                    }
+//                }
+
+                if(isPermission){
+                    return chain.filter(exchange);
+                }
+            }
+        }
+        DataBuffer buffer = setResponseInfo(response, ResultData.error(ServerCode.AUTH_FAIL.code(), ServerCode.AUTH_FAIL.message()));
+        return response.writeWith(Mono.just(buffer));
+    }
+
+
+    private DataBuffer setResponseInfo(ServerHttpResponse response, ResultData resultData) {
+        response.setStatusCode(HttpStatus.UNAUTHORIZED);
+        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
+        byte[] responseByte = JSONUtil.parse(resultData).toString().getBytes();
+        DataBuffer buffer = response.bufferFactory().wrap(responseByte);
+        return buffer;
+    }
+
+    /**
+     * 判断是否是忽略路径
+     *
+     * @param servletPath
+     * @return
+     */
+    private boolean decideIgnore(String servletPath) {
+        //跳过不需要验证的路径
+        for (String ignore : ignoreAuthUrls) {
+            if (servletPath.contains(ignore)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int getOrder() {
+        return -101;
+    }
+
+}

+ 47 - 0
src/main/java/com/fdkankan/gateway/log/GatewayLog.java

@@ -0,0 +1,47 @@
+package com.fdkankan.gateway.log;
+
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+import java.util.Date;
+
+@Data
+@Document("GatewayLog")
+public class GatewayLog {
+
+    @Id
+    private String id;
+    /**访问实例*/
+    @Field("targetServer")
+    private String targetServer;
+    /**请求路径*/
+    @Field("requestPath")
+    private String requestPath;
+    /**请求方法*/
+    @Field("requestMethod")
+    private String requestMethod;
+    /**协议 */
+    @Field("schema")
+    private String schema;
+    /**请求体*/
+    @Field("requestBody")
+    private String requestBody;
+    /**响应体*/
+    @Field("responseData")
+    private String responseData;
+    /**请求ip*/
+    @Field("ip")
+    private String ip;
+    /**请求时间*/
+    @Field("requestTime")
+    private Date requestTime;
+    /**响应时间*/
+    @Field("responseTime")
+    private Date responseTime;
+    /**执行时间*/
+    @Field("executeTime")
+    private long executeTime;
+
+}

+ 38 - 0
src/main/java/com/fdkankan/gateway/util/WebUtil.java

@@ -0,0 +1,38 @@
+package com.fdkankan.gateway.util;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+
+public class WebUtil {
+
+    private final static String UNKNOWN = "unknown";
+
+    /**
+     * 获取客户端请求终端地址
+     * @param request  客户端请求request
+     * @return  终端ip地址
+     */
+    public static String getIpAddress(ServerHttpRequest request) {
+        HttpHeaders headers = request.getHeaders();
+        String ip = headers.getFirst("X-Forwarded-For");
+        if(StrUtil.isNotBlank(ip) && UNKNOWN.equalsIgnoreCase(ip)){
+            if(ip.indexOf(",") != -1){
+                ip = ip.split(",")[0];
+                return ip;
+            }
+        }
+        ip = headers.getFirst("Proxy-Client-IP");
+        if(StrUtil.isNotBlank(ip)) return ip;
+        ip = headers.getFirst("WL-Proxy-Client-IP");
+        if(StrUtil.isNotBlank(ip)) return ip;
+        ip = headers.getFirst("HTTP-CLIENT-IP");
+        if(StrUtil.isNotBlank(ip)) return ip;
+        ip = headers.getFirst("HTTP-X-FORWARDED-FOR");
+        if(StrUtil.isNotBlank(ip)) return ip;
+        ip = headers.getFirst("X-Real-IP");
+        if(StrUtil.isNotBlank(ip)) return ip;
+        ip = request.getRemoteAddress().getAddress().getHostAddress();
+        return ip;
+    }
+}

+ 16 - 0
src/main/resources/bootstrap.yml

@@ -0,0 +1,16 @@
+spring:
+  application:
+    name: 4dkankan-gateway
+  cloud:
+    nacos:
+      config:
+        server-addr: 192.168.0.47:8848
+        file-extension: yaml
+        namespace: 4dkankan-dev
+        extension-configs[0]:
+          data-id: common-redis-config.yaml
+          group: DEFAULT_GROUP
+          refresh: true
+      discovery:
+        server-addr: 192.168.0.47:8848
+        namespace: 4dkankan-dev

+ 231 - 0
src/main/resources/logback-spring.xml

@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
+<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
+<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
+<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
+<configuration scan="true" scanPeriod="10 seconds">
+
+	<contextName>logback</contextName>
+	<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
+	<property name="log.path" value="/home/logs/4dkankan_gateway" />
+
+	<!-- 彩色日志 -->
+	<!-- 彩色日志依赖的渲染类 -->
+	<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" />
+	<!-- 彩色日志格式 -->
+	<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}}" />
+
+	<!--输出到控制台 -->
+	<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+		<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息 -->
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<level>info</level>
+		</filter>
+		<encoder>
+			<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
+			<!-- 设置字符集 -->
+			<charset>UTF-8</charset>
+		</encoder>
+	</appender>
+	<!--输出到文件 -->
+
+	<!-- 时间滚动输出 level为 DEBUG 日志 -->
+	<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<!-- 正在记录的日志文件的路径及文件名 -->
+		<file>${log.path}/log_debug.log</file>
+		<!--日志文件输出格式 -->
+		<encoder>
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+			<charset>UTF-8</charset> <!-- 设置字符集 -->
+		</encoder>
+		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<!-- 日志归档 -->
+			<fileNamePattern>${log.path}/debug/log-debug-%d{yyyy-MM-dd}.%i.log
+			</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>100MB</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<!--日志文件保留天数 -->
+			<maxHistory>15</maxHistory>
+		</rollingPolicy>
+		<!-- 此日志文件只记录debug级别的 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>debug</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+
+	<!-- 时间滚动输出 level为 INFO 日志 -->
+	<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<!-- 正在记录的日志文件的路径及文件名 -->
+		<file>${log.path}/log_info.log</file>
+		<!--日志文件输出格式 -->
+		<encoder>
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+			<charset>UTF-8</charset>
+		</encoder>
+		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<!-- 每天日志归档路径以及格式 -->
+			<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log
+			</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>100MB</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<!--日志文件保留天数 -->
+			<maxHistory>15</maxHistory>
+		</rollingPolicy>
+		<!-- 此日志文件只记录info级别的 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>info</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+
+	<!-- 时间滚动输出 level为 WARN 日志 -->
+	<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<!-- 正在记录的日志文件的路径及文件名 -->
+		<file>${log.path}/log_warn.log</file>
+		<!--日志文件输出格式 -->
+		<encoder>
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
+		</encoder>
+		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log
+			</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>100MB</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<!--日志文件保留天数 -->
+			<maxHistory>15</maxHistory>
+		</rollingPolicy>
+		<!-- 此日志文件只记录warn级别的 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>warn</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+
+
+	<!-- 时间滚动输出 level为 ERROR 日志 -->
+	<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<!-- 正在记录的日志文件的路径及文件名 -->
+		<file>${log.path}/log_error.log</file>
+		<!--日志文件输出格式 -->
+		<encoder>
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
+		</encoder>
+		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log
+			</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>100MB</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<!--日志文件保留天数 -->
+			<maxHistory>15</maxHistory>
+		</rollingPolicy>
+		<!-- 此日志文件只记录ERROR级别的 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>ERROR</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+	
+	<appender name="PROGRAM_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<!-- 正在记录的日志文件的路径及文件名 -->
+		<file>${log.path}/program/log_program.log</file>
+		<!--日志文件输出格式 -->
+		<encoder>
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
+		</encoder>
+		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/program/log-program-%d{yyyy-MM-dd}.%i.log
+			</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>100MB</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<!--日志文件保留天数 -->
+			<maxHistory>15</maxHistory>
+		</rollingPolicy>
+		<!-- 此日志文件只记录ERROR级别的 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+	<logger name="programLog" level="INFO" additivity="true">
+        <appender-ref ref="PROGRAM_FILE"/>
+    </logger>
+
+	<appender name="VISIT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<!-- 正在记录的日志文件的路径及文件名 -->
+		<file>${log.path}/visit/log_visit.log</file>
+		<!--日志文件输出格式 -->
+		<encoder>
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
+		</encoder>
+		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/visit/log-visit-%d{yyyy-MM-dd}.%i.log
+			</fileNamePattern>
+
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>100MB</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<!--日志文件保留天数 -->
+			<maxHistory>15</maxHistory>
+		</rollingPolicy>
+		<!-- 此日志文件只记录ERROR级别的 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+	<logger name="visitLog" level="INFO" additivity="true">
+        <appender-ref ref="VISIT_FILE"/>
+    </logger>
+
+	<!-- <logger>用来设置某一个包或者具体的某一个类的日志打印级别、 以及指定<appender>。<logger>仅有一个name属性, 一个可选的level和一个可选的addtivity属性。 name:用来指定受此logger约束的某一个包或者具体的某一个类。 level:用来设置打印级别,大小写无关:TRACE, 
+		DEBUG, INFO, WARN, ERROR, ALL 和 OFF, 还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。 如果未设置此属性,那么当前logger将会继承上级的级别。 addtivity:是否向上级logger传递打印信息。默认是true。 -->
+	<!--<logger name="org.springframework.web" level="info"/> -->
+	<!--<logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/> -->
+	<!-- 使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作: 第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息 第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别: -->
+	<!-- root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性 level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF, 不能设置为INHERITED或者同义词NULL。默认是DEBUG 可以包含零个或多个元素,标识这个appender将会添加到这个logger。 -->
+
+	<root level="info">
+		<appender-ref ref="CONSOLE" />
+		<appender-ref ref="DEBUG_FILE" />
+		<appender-ref ref="INFO_FILE" />
+		<appender-ref ref="WARN_FILE" />
+		<appender-ref ref="ERROR_FILE" />
+	</root>
+
+	<!--生产环境:输出到文件 -->
+	<!--<springProfile name="pro"> -->
+	<!--<root level="info"> -->
+	<!--<appender-ref ref="CONSOLE" /> -->
+	<!--<appender-ref ref="DEBUG_FILE" /> -->
+	<!--<appender-ref ref="INFO_FILE" /> -->
+	<!--<appender-ref ref="ERROR_FILE" /> -->
+	<!--<appender-ref ref="WARN_FILE" /> -->
+	<!--</root> -->
+	<!--</springProfile> -->
+
+</configuration>
+
+

+ 34 - 0
src/test/java/com/fdkankan/gateway/GatewayApplicationTests.java

@@ -0,0 +1,34 @@
+package com.fdkankan.gateway;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.fdkankan.gateway.dto.User;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.mongodb.core.MongoTemplate;
+
+import java.util.List;
+
+@SpringBootTest
+class GatewayApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+    @Autowired
+    private MongoTemplate mongoTemplate;
+
+    @Test
+    public void test (){
+
+        List<User> userList = mongoTemplate.findAll(User.class);
+        if(CollectionUtil.isNotEmpty(userList)){
+            for (User user : userList) {
+                System.out.println(user.toString());
+            }
+        }
+
+    }
+
+}