初始化
commit
2fa4d21f06
|
@ -0,0 +1,46 @@
|
||||||
|
######################################################################
|
||||||
|
# Build Tools
|
||||||
|
|
||||||
|
.gradle
|
||||||
|
/build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# IDE
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### JRebel ###
|
||||||
|
rebel.xml
|
||||||
|
### NetBeans ###
|
||||||
|
nbproject/private/
|
||||||
|
build/*
|
||||||
|
nbbuild/
|
||||||
|
dist/
|
||||||
|
nbdist/
|
||||||
|
.nb-gradle/
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# Others
|
||||||
|
*.log
|
||||||
|
*.xml.versionsBackup
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
!*/build/*.java
|
||||||
|
!*/build/*.html
|
||||||
|
!*/build/*.xml
|
|
@ -0,0 +1,112 @@
|
||||||
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<groupId>com.muyu</groupId>
|
||||||
|
<artifactId>cloud-server</artifactId>
|
||||||
|
<version>3.6.3</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>cloud-gateway</artifactId>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
cloud-gateway网关模块
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<!-- SpringCloud Gateway -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringCloud Alibaba Nacos -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringCloud Alibaba Nacos Config -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringCloud Alibaba Sentinel -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringCloud Alibaba Sentinel Gateway -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Sentinel Datasource Nacos -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.csp</groupId>
|
||||||
|
<artifactId>sentinel-datasource-nacos</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringBoot Actuator -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringCloud Loadbalancer -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-loadbalancer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!--验证码 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>pro.fessional</groupId>
|
||||||
|
<artifactId>kaptcha</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MuYu Common Redis-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.muyu</groupId>
|
||||||
|
<artifactId>cloud-common-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
|
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
|
||||||
|
<version>4.5.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>${project.artifactId}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<!-- 加入maven deploy插件,当在deploy时,忽略些model-->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-deploy-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.muyu.gateway;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关启动程序
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
|
||||||
|
public class CloudGatewayApplication {
|
||||||
|
public static void main (String[] args) {
|
||||||
|
SpringApplication.run(CloudGatewayApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.muyu.gateway.config;
|
||||||
|
|
||||||
|
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||||
|
import com.google.code.kaptcha.util.Config;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import static com.google.code.kaptcha.Constants.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码配置
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class CaptchaConfig {
|
||||||
|
@Bean(name = "captchaProducer")
|
||||||
|
public DefaultKaptcha getKaptchaBean () {
|
||||||
|
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
|
||||||
|
Properties properties = new Properties();
|
||||||
|
// 是否有边框 默认为true 我们可以自己设置yes,no
|
||||||
|
properties.setProperty(KAPTCHA_BORDER, "yes");
|
||||||
|
// 验证码文本字符颜色 默认为Color.BLACK
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
|
||||||
|
// 验证码图片宽度 默认为200
|
||||||
|
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
|
||||||
|
// 验证码图片高度 默认为50
|
||||||
|
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
|
||||||
|
// 验证码文本字符大小 默认为40
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
|
||||||
|
// KAPTCHA_SESSION_KEY
|
||||||
|
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
|
||||||
|
// 验证码文本字符长度 默认为5
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
|
||||||
|
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
|
||||||
|
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
|
||||||
|
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
|
||||||
|
Config config = new Config(properties);
|
||||||
|
defaultKaptcha.setConfig(config);
|
||||||
|
return defaultKaptcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "captchaProducerMath")
|
||||||
|
public DefaultKaptcha getKaptchaBeanMath () {
|
||||||
|
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
|
||||||
|
Properties properties = new Properties();
|
||||||
|
// 是否有边框 默认为true 我们可以自己设置yes,no
|
||||||
|
properties.setProperty(KAPTCHA_BORDER, "yes");
|
||||||
|
// 边框颜色 默认为Color.BLACK
|
||||||
|
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
|
||||||
|
// 验证码文本字符颜色 默认为Color.BLACK
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
|
||||||
|
// 验证码图片宽度 默认为200
|
||||||
|
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
|
||||||
|
// 验证码图片高度 默认为50
|
||||||
|
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
|
||||||
|
// 验证码文本字符大小 默认为40
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
|
||||||
|
// KAPTCHA_SESSION_KEY
|
||||||
|
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
|
||||||
|
// 验证码文本生成器
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.muyu.gateway.config.KaptchaTextCreator");
|
||||||
|
// 验证码文本字符间距 默认为2
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
|
||||||
|
// 验证码文本字符长度 默认为5
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
|
||||||
|
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
|
||||||
|
// 验证码噪点颜色 默认为Color.BLACK
|
||||||
|
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
|
||||||
|
// 干扰实现类
|
||||||
|
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
|
||||||
|
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
|
||||||
|
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
|
||||||
|
Config config = new Config(properties);
|
||||||
|
defaultKaptcha.setConfig(config);
|
||||||
|
return defaultKaptcha;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.muyu.gateway.config;
|
||||||
|
|
||||||
|
import com.muyu.gateway.handler.SentinelFallbackHandler;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关限流配置
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class GatewayConfig {
|
||||||
|
@Bean
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
|
public SentinelFallbackHandler sentinelGatewayExceptionHandler () {
|
||||||
|
return new SentinelFallbackHandler();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.muyu.gateway.config;
|
||||||
|
|
||||||
|
import com.google.code.kaptcha.text.impl.DefaultTextCreator;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码文本生成器
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
public class KaptchaTextCreator extends DefaultTextCreator {
|
||||||
|
private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getText () {
|
||||||
|
Integer result = 0;
|
||||||
|
Random random = new Random();
|
||||||
|
int x = random.nextInt(10);
|
||||||
|
int y = random.nextInt(10);
|
||||||
|
StringBuilder suChinese = new StringBuilder();
|
||||||
|
int randomoperands = random.nextInt(3);
|
||||||
|
if (randomoperands == 0) {
|
||||||
|
result = x * y;
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
suChinese.append("*");
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
} else if (randomoperands == 1) {
|
||||||
|
if ((x != 0) && y % x == 0) {
|
||||||
|
result = y / x;
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
suChinese.append("/");
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
} else {
|
||||||
|
result = x + y;
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
suChinese.append("+");
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
}
|
||||||
|
} else if (randomoperands == 2) {
|
||||||
|
if (x >= y) {
|
||||||
|
result = x - y;
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
suChinese.append("-");
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
} else {
|
||||||
|
result = y - x;
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
suChinese.append("-");
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = x + y;
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
suChinese.append("+");
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
}
|
||||||
|
suChinese.append("=?@" + result);
|
||||||
|
return suChinese.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.muyu.gateway.config;
|
||||||
|
|
||||||
|
import com.muyu.gateway.handler.ValidateCodeHandler;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由配置信息
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RouterFunctionConfiguration {
|
||||||
|
@Autowired
|
||||||
|
private ValidateCodeHandler validateCodeHandler;
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
@Bean
|
||||||
|
public RouterFunction routerFunction () {
|
||||||
|
return RouterFunctions.route(
|
||||||
|
RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
|
||||||
|
validateCodeHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.muyu.gateway.config.properties;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码配置
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@RefreshScope
|
||||||
|
@ConfigurationProperties(prefix = "security.captcha")
|
||||||
|
public class CaptchaProperties {
|
||||||
|
/**
|
||||||
|
* 验证码开关
|
||||||
|
*/
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码类型(math 数组计算 char 字符)
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
public Boolean getEnabled () {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled (Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType () {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType (String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.muyu.gateway.config.properties;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 放行白名单配置
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@RefreshScope
|
||||||
|
@ConfigurationProperties(prefix = "security.ignore")
|
||||||
|
public class IgnoreWhiteProperties {
|
||||||
|
/**
|
||||||
|
* 放行白名单配置,网关不校验此处的白名单
|
||||||
|
*/
|
||||||
|
private List<String> whites = new ArrayList<>();
|
||||||
|
|
||||||
|
public List<String> getWhites () {
|
||||||
|
return whites;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWhites (List<String> whites) {
|
||||||
|
this.whites = whites;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.muyu.gateway.config.properties;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XSS跨站脚本配置
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@RefreshScope
|
||||||
|
@ConfigurationProperties(prefix = "security.xss")
|
||||||
|
public class XssProperties {
|
||||||
|
/**
|
||||||
|
* Xss开关
|
||||||
|
*/
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排除路径
|
||||||
|
*/
|
||||||
|
private List<String> excludeUrls = new ArrayList<>();
|
||||||
|
|
||||||
|
public Boolean getEnabled () {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled (Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getExcludeUrls () {
|
||||||
|
return excludeUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExcludeUrls (List<String> excludeUrls) {
|
||||||
|
this.excludeUrls = excludeUrls;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
package com.muyu.gateway.filter;
|
||||||
|
|
||||||
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
|
import com.alibaba.nacos.common.utils.StringUtils;
|
||||||
|
import com.muyu.common.core.constant.SecurityConstants;
|
||||||
|
import com.muyu.gateway.model.AccessLog;
|
||||||
|
import com.muyu.gateway.utils.WebFrameworkUtils;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
|
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
|
||||||
|
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
|
||||||
|
import org.springframework.cloud.gateway.support.BodyInserterContext;
|
||||||
|
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||||
|
import org.springframework.http.codec.HttpMessageReader;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.function.BodyInserter;
|
||||||
|
import org.springframework.web.reactive.function.BodyInserters;
|
||||||
|
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关的访问日志过滤器
|
||||||
|
* <p>
|
||||||
|
* <p>
|
||||||
|
* TODO 如果网关执行异常,不会记录访问日志,后续研究下 https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging
|
||||||
|
*/
|
||||||
|
@Log4j2
|
||||||
|
@Component
|
||||||
|
public class AccessLogFilter implements GlobalFilter, Ordered {
|
||||||
|
|
||||||
|
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印日志
|
||||||
|
*
|
||||||
|
* @param gatewayLog 网关日志
|
||||||
|
*/
|
||||||
|
private void writeAccessLog(AccessLog gatewayLog) {
|
||||||
|
log.info("[网关日志:{}]", gatewayLog.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return -99;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||||
|
// 将 Request 中可以直接获取到的参数,设置到网关日志
|
||||||
|
ServerHttpRequest request = exchange.getRequest();
|
||||||
|
// TODO traceId
|
||||||
|
AccessLog accessLog = AccessLog.builder()
|
||||||
|
.userId(request.getHeaders().getFirst(SecurityConstants.DETAILS_USER_ID))
|
||||||
|
.route(WebFrameworkUtils.getGatewayRoute(exchange))
|
||||||
|
.schema(request.getURI().getScheme())
|
||||||
|
.requestMethod(request.getMethod().name())
|
||||||
|
.requestUrl(request.getURI().getRawPath())
|
||||||
|
.queryParams(request.getQueryParams())
|
||||||
|
.requestHeaders(request.getHeaders())
|
||||||
|
.startTime(LocalDateTime.now())
|
||||||
|
.userIp(WebFrameworkUtils.getClientIP(exchange))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 继续 filter 过滤
|
||||||
|
MediaType mediaType = request.getHeaders().getContentType();
|
||||||
|
return MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)
|
||||||
|
?
|
||||||
|
filterWithRequestBody(exchange, chain, accessLog)
|
||||||
|
:
|
||||||
|
filterWithoutRequestBody(exchange, chain, accessLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
|
||||||
|
// 包装 Response,用于记录 Response Body
|
||||||
|
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
|
||||||
|
return chain.filter(exchange.mutate().response(decoratedResponse).build())
|
||||||
|
.then(Mono.fromRunnable(() -> writeAccessLog(accessLog))); // 打印日志
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参考 {@link ModifyRequestBodyGatewayFilterFactory} 实现
|
||||||
|
* <p>
|
||||||
|
* 差别主要在于使用 modifiedBody 来读取 Request Body 数据
|
||||||
|
*/
|
||||||
|
private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog) {
|
||||||
|
// 设置 Request Body 读取时,设置到网关日志
|
||||||
|
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
|
||||||
|
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
|
||||||
|
gatewayLog.setRequestBody(body);
|
||||||
|
return Mono.just(body);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建 BodyInserter 对象
|
||||||
|
BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
|
||||||
|
// 创建 CachedBodyOutputMessage 对象
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.putAll(exchange.getRequest().getHeaders());
|
||||||
|
// the new content type will be computed by bodyInserter
|
||||||
|
// and then set in the request decorator
|
||||||
|
headers.remove(HttpHeaders.CONTENT_LENGTH); // 移除
|
||||||
|
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
|
||||||
|
// 通过 BodyInserter 将 Request Body 写入到 CachedBodyOutputMessage 中
|
||||||
|
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
|
||||||
|
// 包装 Request,用于缓存 Request Body
|
||||||
|
ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
|
||||||
|
// 包装 Response,用于记录 Response Body
|
||||||
|
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
|
||||||
|
// 记录普通的
|
||||||
|
return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
|
||||||
|
.then(Mono.fromRunnable(() -> writeAccessLog(gatewayLog))); // 打印日志
|
||||||
|
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录响应日志
|
||||||
|
* 通过 DataBufferFactory 解决响应体分段传输问题。
|
||||||
|
*/
|
||||||
|
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog gatewayLog) {
|
||||||
|
ServerHttpResponse response = exchange.getResponse();
|
||||||
|
return new ServerHttpResponseDecorator(response) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||||
|
if (body instanceof Flux) {
|
||||||
|
DataBufferFactory bufferFactory = response.bufferFactory();
|
||||||
|
// 计算执行时间
|
||||||
|
gatewayLog.setEndTime(LocalDateTime.now());
|
||||||
|
gatewayLog.setDuration((int) (LocalDateTimeUtil.between(gatewayLog.getStartTime(),
|
||||||
|
gatewayLog.getEndTime()).toMillis()));
|
||||||
|
// 设置其它字段
|
||||||
|
// gatewayLog.setUserId(SecurityFrameworkUtils.getLoginUserId(exchange));
|
||||||
|
gatewayLog.setResponseHeaders(response.getHeaders());
|
||||||
|
gatewayLog.setHttpStatus(response.getStatusCode());
|
||||||
|
|
||||||
|
// 获取响应类型,如果是 json 就打印
|
||||||
|
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
|
||||||
|
if (StringUtils.isNotBlank(originalResponseContentType)
|
||||||
|
&& originalResponseContentType.contains("application/json")) {
|
||||||
|
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
|
||||||
|
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
|
||||||
|
// 设置 response body 到网关日志
|
||||||
|
byte[] content = readContent(dataBuffers);
|
||||||
|
String responseResult = new String(content, StandardCharsets.UTF_8);
|
||||||
|
gatewayLog.setResponseBody(responseResult);
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
return bufferFactory.wrap(content);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if body is not a flux. never got there.
|
||||||
|
return super.writeWith(body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 参考 ModifyRequestBodyGatewayFilterFactory 中的方法 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求装饰器,支持重新计算 headers、body 缓存
|
||||||
|
*
|
||||||
|
* @param exchange 请求
|
||||||
|
* @param headers 请求头
|
||||||
|
* @param outputMessage body 缓存
|
||||||
|
* @return 请求装饰器
|
||||||
|
*/
|
||||||
|
private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
|
||||||
|
return new ServerHttpRequestDecorator(exchange.getRequest()) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpHeaders getHeaders() {
|
||||||
|
long contentLength = headers.getContentLength();
|
||||||
|
HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
|
httpHeaders.putAll(super.getHeaders());
|
||||||
|
if (contentLength > 0) {
|
||||||
|
httpHeaders.setContentLength(contentLength);
|
||||||
|
} else {
|
||||||
|
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
|
||||||
|
}
|
||||||
|
return httpHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<DataBuffer> getBody() {
|
||||||
|
return outputMessage.getBody();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 参考 ModifyResponseBodyGatewayFilterFactory 中的方法 ==========
|
||||||
|
|
||||||
|
private byte[] readContent(List<? extends DataBuffer> dataBuffers) {
|
||||||
|
// 合并多个流集合,解决返回体分段传输
|
||||||
|
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
|
||||||
|
DataBuffer join = dataBufferFactory.join(dataBuffers);
|
||||||
|
byte[] content = new byte[join.readableByteCount()];
|
||||||
|
join.read(content);
|
||||||
|
// 释放掉内存
|
||||||
|
DataBufferUtils.release(join);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
package com.muyu.gateway.filter;
|
||||||
|
|
||||||
|
import com.muyu.common.core.constant.CacheConstants;
|
||||||
|
import com.muyu.common.core.constant.HttpStatus;
|
||||||
|
import com.muyu.common.core.constant.SecurityConstants;
|
||||||
|
import com.muyu.common.core.constant.TokenConstants;
|
||||||
|
import com.muyu.common.core.utils.JwtUtils;
|
||||||
|
import com.muyu.common.core.utils.ServletUtils;
|
||||||
|
import com.muyu.common.core.utils.StringUtils;
|
||||||
|
import com.muyu.common.redis.service.RedisService;
|
||||||
|
import com.muyu.gateway.config.properties.IgnoreWhiteProperties;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关鉴权
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AuthFilter implements GlobalFilter, Ordered {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
|
||||||
|
|
||||||
|
// 排除过滤的 uri 地址,nacos自行添加
|
||||||
|
@Autowired
|
||||||
|
private IgnoreWhiteProperties ignoreWhite;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisService redisService;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||||
|
ServerHttpRequest request = exchange.getRequest();
|
||||||
|
ServerHttpRequest.Builder mutate = request.mutate();
|
||||||
|
|
||||||
|
String url = request.getURI().getPath();
|
||||||
|
// 跳过不需要验证的路径
|
||||||
|
if (StringUtils.matches(url, ignoreWhite.getWhites())) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
String token = getToken(request);
|
||||||
|
if (StringUtils.isEmpty(token)) {
|
||||||
|
return unauthorizedResponse(exchange, "令牌不能为空");
|
||||||
|
}
|
||||||
|
Claims claims = JwtUtils.parseToken(token);
|
||||||
|
if (claims == null) {
|
||||||
|
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
|
||||||
|
}
|
||||||
|
String userkey = JwtUtils.getUserKey(claims);
|
||||||
|
boolean islogin = redisService.hasKey(getTokenKey(userkey));
|
||||||
|
if (!islogin) {
|
||||||
|
return unauthorizedResponse(exchange, "登录状态已过期");
|
||||||
|
}
|
||||||
|
String userid = JwtUtils.getUserId(claims);
|
||||||
|
String username = JwtUtils.getUserName(claims);
|
||||||
|
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
|
||||||
|
return unauthorizedResponse(exchange, "令牌验证失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户信息到请求
|
||||||
|
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
|
||||||
|
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
|
||||||
|
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
|
||||||
|
// 内部请求来源参数清除
|
||||||
|
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
|
||||||
|
return chain.filter(exchange.mutate().request(mutate.build()).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addHeader (ServerHttpRequest.Builder mutate, String name, Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String valueStr = value.toString();
|
||||||
|
String valueEncode = ServletUtils.urlEncode(valueStr);
|
||||||
|
mutate.header(name, valueEncode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeHeader (ServerHttpRequest.Builder mutate, String name) {
|
||||||
|
mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Void> unauthorizedResponse (ServerWebExchange exchange, String msg) {
|
||||||
|
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
|
||||||
|
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存key
|
||||||
|
*/
|
||||||
|
private String getTokenKey (String token) {
|
||||||
|
return CacheConstants.LOGIN_TOKEN_KEY + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求token
|
||||||
|
*/
|
||||||
|
private String getToken (ServerHttpRequest request) {
|
||||||
|
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
|
||||||
|
// 如果前端设置了令牌前缀,则裁剪掉前缀
|
||||||
|
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
|
||||||
|
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder () {
|
||||||
|
return -200;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.muyu.gateway.filter;
|
||||||
|
|
||||||
|
import com.muyu.common.core.utils.ServletUtils;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||||
|
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黑名单过滤器
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config> {
|
||||||
|
public BlackListUrlFilter () {
|
||||||
|
super(Config.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GatewayFilter apply (Config config) {
|
||||||
|
return (exchange, chain) -> {
|
||||||
|
|
||||||
|
String url = exchange.getRequest().getURI().getPath();
|
||||||
|
if (config.matchBlacklist(url)) {
|
||||||
|
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.filter(exchange);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Config {
|
||||||
|
private List<String> blacklistUrl;
|
||||||
|
|
||||||
|
private List<Pattern> blacklistUrlPattern = new ArrayList<>();
|
||||||
|
|
||||||
|
public boolean matchBlacklist (String url) {
|
||||||
|
return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getBlacklistUrl () {
|
||||||
|
return blacklistUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlacklistUrl (List<String> blacklistUrl) {
|
||||||
|
this.blacklistUrl = blacklistUrl;
|
||||||
|
this.blacklistUrlPattern.clear();
|
||||||
|
this.blacklistUrl.forEach(url -> {
|
||||||
|
this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.muyu.gateway.filter;
|
||||||
|
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
|
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
|
||||||
|
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||||
|
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取body请求数据(解决流不能重复读取问题)
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class CacheRequestFilter extends AbstractGatewayFilterFactory<CacheRequestFilter.Config> {
|
||||||
|
public CacheRequestFilter () {
|
||||||
|
super(Config.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name () {
|
||||||
|
return "CacheRequestFilter";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GatewayFilter apply (Config config) {
|
||||||
|
CacheRequestGatewayFilter cacheRequestGatewayFilter = new CacheRequestGatewayFilter();
|
||||||
|
Integer order = config.getOrder();
|
||||||
|
if (order == null) {
|
||||||
|
return cacheRequestGatewayFilter;
|
||||||
|
}
|
||||||
|
return new OrderedGatewayFilter(cacheRequestGatewayFilter, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> shortcutFieldOrder () {
|
||||||
|
return Collections.singletonList("order");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CacheRequestGatewayFilter implements GatewayFilter {
|
||||||
|
@Override
|
||||||
|
public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||||
|
// GET DELETE 不过滤
|
||||||
|
HttpMethod method = exchange.getRequest().getMethod();
|
||||||
|
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
|
||||||
|
if (serverHttpRequest == exchange.getRequest()) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Config {
|
||||||
|
private Integer order;
|
||||||
|
|
||||||
|
public Integer getOrder () {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrder (Integer order) {
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package com.muyu.gateway.filter;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.muyu.common.core.utils.ServletUtils;
|
||||||
|
import com.muyu.common.core.utils.StringUtils;
|
||||||
|
import com.muyu.gateway.config.properties.CaptchaProperties;
|
||||||
|
import com.muyu.gateway.service.ValidateCodeService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||||
|
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码过滤器
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object> {
|
||||||
|
private final static String[] VALIDATE_URL = new String[]{"/auth/login", "/auth/register"};
|
||||||
|
private static final String CODE = "code";
|
||||||
|
private static final String UUID = "uuid";
|
||||||
|
@Autowired
|
||||||
|
private ValidateCodeService validateCodeService;
|
||||||
|
@Autowired
|
||||||
|
private CaptchaProperties captchaProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GatewayFilter apply (Object config) {
|
||||||
|
return (exchange, chain) -> {
|
||||||
|
ServerHttpRequest request = exchange.getRequest();
|
||||||
|
|
||||||
|
// 非登录/注册请求或验证码关闭,不处理
|
||||||
|
if (!StringUtils.equalsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled()) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String rspStr = resolveBodyFromRequest(request);
|
||||||
|
JSONObject obj = JSON.parseObject(rspStr);
|
||||||
|
validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
|
||||||
|
}
|
||||||
|
return chain.filter(exchange);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveBodyFromRequest (ServerHttpRequest serverHttpRequest) {
|
||||||
|
// 获取请求体
|
||||||
|
Flux<DataBuffer> body = serverHttpRequest.getBody();
|
||||||
|
AtomicReference<String> bodyRef = new AtomicReference<>();
|
||||||
|
body.subscribe(buffer -> {
|
||||||
|
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
|
||||||
|
DataBufferUtils.release(buffer);
|
||||||
|
bodyRef.set(charBuffer.toString());
|
||||||
|
});
|
||||||
|
return bodyRef.get();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package com.muyu.gateway.filter;
|
||||||
|
|
||||||
|
import com.muyu.common.core.utils.StringUtils;
|
||||||
|
import com.muyu.common.core.utils.html.EscapeUtil;
|
||||||
|
import com.muyu.gateway.config.properties.XssProperties;
|
||||||
|
import io.netty.buffer.ByteBufAllocator;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.io.buffer.*;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨站脚本过滤器
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
|
||||||
|
public class XssFilter implements GlobalFilter, Ordered {
|
||||||
|
// 跨站脚本的 xss 配置,nacos自行添加
|
||||||
|
@Autowired
|
||||||
|
private XssProperties xss;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||||
|
ServerHttpRequest request = exchange.getRequest();
|
||||||
|
// xss开关未开启 或 通过nacos关闭,不过滤
|
||||||
|
if (!xss.getEnabled()) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
// GET DELETE 不过滤
|
||||||
|
HttpMethod method = request.getMethod();
|
||||||
|
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
// 非json类型,不过滤
|
||||||
|
if (!isJsonRequest(exchange)) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
// excludeUrls 不过滤
|
||||||
|
String url = request.getURI().getPath();
|
||||||
|
if (StringUtils.matches(url, xss.getExcludeUrls())) {
|
||||||
|
return chain.filter(exchange);
|
||||||
|
}
|
||||||
|
ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
|
||||||
|
return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerHttpRequestDecorator requestDecorator (ServerWebExchange exchange) {
|
||||||
|
ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
|
||||||
|
@Override
|
||||||
|
public Flux<DataBuffer> getBody () {
|
||||||
|
Flux<DataBuffer> body = super.getBody();
|
||||||
|
return body.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 bodyStr = new String(content, StandardCharsets.UTF_8);
|
||||||
|
// 防xss攻击过滤
|
||||||
|
bodyStr = EscapeUtil.clean(bodyStr);
|
||||||
|
// 转成字节
|
||||||
|
byte[] bytes = bodyStr.getBytes(StandardCharsets.UTF_8);
|
||||||
|
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
|
||||||
|
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
|
||||||
|
buffer.write(bytes);
|
||||||
|
return buffer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpHeaders getHeaders () {
|
||||||
|
HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
|
httpHeaders.putAll(super.getHeaders());
|
||||||
|
// 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
|
||||||
|
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
|
||||||
|
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
|
||||||
|
return httpHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
return serverHttpRequestDecorator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是Json请求
|
||||||
|
*
|
||||||
|
* @param exchange HTTP请求
|
||||||
|
*/
|
||||||
|
public boolean isJsonRequest (ServerWebExchange exchange) {
|
||||||
|
String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
|
||||||
|
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder () {
|
||||||
|
return -100;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.muyu.gateway.handler;
|
||||||
|
|
||||||
|
import com.muyu.common.core.utils.ServletUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
|
||||||
|
import org.springframework.cloud.gateway.support.NotFoundException;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关统一异常处理
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Order(-1)
|
||||||
|
@Configuration
|
||||||
|
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle (ServerWebExchange exchange, Throwable ex) {
|
||||||
|
ServerHttpResponse response = exchange.getResponse();
|
||||||
|
|
||||||
|
if (exchange.getResponse().isCommitted()) {
|
||||||
|
return Mono.error(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
String msg;
|
||||||
|
|
||||||
|
if (ex instanceof NotFoundException) {
|
||||||
|
msg = "服务未找到";
|
||||||
|
} else if (ex instanceof ResponseStatusException) {
|
||||||
|
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
|
||||||
|
msg = responseStatusException.getMessage();
|
||||||
|
} else {
|
||||||
|
msg = "内部服务器错误";
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());
|
||||||
|
|
||||||
|
return ServletUtils.webFluxResponseWriter(response, msg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.muyu.gateway.handler;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
import com.muyu.common.core.utils.ServletUtils;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.server.WebExceptionHandler;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义限流异常处理
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
public class SentinelFallbackHandler implements WebExceptionHandler {
|
||||||
|
private Mono<Void> writeResponse (ServerResponse response, ServerWebExchange exchange) {
|
||||||
|
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求超过最大数,请稍候再试");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> handle (ServerWebExchange exchange, Throwable ex) {
|
||||||
|
if (exchange.getResponse().isCommitted()) {
|
||||||
|
return Mono.error(ex);
|
||||||
|
}
|
||||||
|
if (!BlockException.isBlockException(ex)) {
|
||||||
|
return Mono.error(ex);
|
||||||
|
}
|
||||||
|
return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> handleBlockedRequest (ServerWebExchange exchange, Throwable throwable) {
|
||||||
|
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.muyu.gateway.handler;
|
||||||
|
|
||||||
|
import com.muyu.common.core.exception.CaptchaException;
|
||||||
|
import com.muyu.common.core.domain.Result;
|
||||||
|
import com.muyu.gateway.service.ValidateCodeService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
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.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码获取
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ValidateCodeHandler implements HandlerFunction<ServerResponse> {
|
||||||
|
@Autowired
|
||||||
|
private ValidateCodeService validateCodeService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ServerResponse> handle (ServerRequest serverRequest) {
|
||||||
|
Result ajax;
|
||||||
|
try {
|
||||||
|
ajax = validateCodeService.createCaptcha();
|
||||||
|
} catch (CaptchaException | IOException e) {
|
||||||
|
return Mono.error(e);
|
||||||
|
}
|
||||||
|
return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
package com.muyu.gateway.model;
|
||||||
|
|
||||||
|
import com.muyu.common.core.utils.StringUtils;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||||
|
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||||
|
import org.springframework.cloud.gateway.route.Route;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关的访问日志
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AccessLog {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链路追踪编号
|
||||||
|
*/
|
||||||
|
private String traceId;
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由
|
||||||
|
*
|
||||||
|
* 类似 ApiAccessLogCreateReqDTO 的 applicationName
|
||||||
|
*/
|
||||||
|
private Route route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 协议
|
||||||
|
*/
|
||||||
|
private String schema;
|
||||||
|
/**
|
||||||
|
* 请求方法名
|
||||||
|
*/
|
||||||
|
private String requestMethod;
|
||||||
|
/**
|
||||||
|
* 访问地址
|
||||||
|
*/
|
||||||
|
private String requestUrl;
|
||||||
|
/**
|
||||||
|
* 查询参数
|
||||||
|
*/
|
||||||
|
private MultiValueMap<String, String> queryParams;
|
||||||
|
/**
|
||||||
|
* 请求体
|
||||||
|
*/
|
||||||
|
private String requestBody;
|
||||||
|
/**
|
||||||
|
* 请求头
|
||||||
|
*/
|
||||||
|
private MultiValueMap<String, String> requestHeaders;
|
||||||
|
/**
|
||||||
|
* 用户 IP
|
||||||
|
*/
|
||||||
|
private String userIp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应体
|
||||||
|
*
|
||||||
|
* 类似 ApiAccessLogCreateReqDTO 的 resultCode + resultMsg
|
||||||
|
*/
|
||||||
|
private String responseBody;
|
||||||
|
/**
|
||||||
|
* 响应头
|
||||||
|
*/
|
||||||
|
private MultiValueMap<String, String> responseHeaders;
|
||||||
|
/**
|
||||||
|
* 响应结果
|
||||||
|
*/
|
||||||
|
private HttpStatusCode httpStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始请求时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
/**
|
||||||
|
* 结束请求时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
/**
|
||||||
|
* 执行时长,单位:毫秒
|
||||||
|
*/
|
||||||
|
private Integer duration;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||||
|
.append("请求简略信息",
|
||||||
|
StringUtils.format("[userId:[{}]-userIp:[{}]-traceId:[{}]] ---结果--- {{}-{}}:{} ---> {}",userId, userIp, traceId, schema, requestMethod, requestUrl,httpStatus)
|
||||||
|
)
|
||||||
|
.append("路由", route)
|
||||||
|
.append("查询参数", queryParams)
|
||||||
|
.append("请求体", requestBody)
|
||||||
|
.append("请求头", requestHeaders)
|
||||||
|
.append("响应体", responseBody)
|
||||||
|
.append("响应头", responseHeaders)
|
||||||
|
.append("耗时/时间",
|
||||||
|
StringUtils.format(
|
||||||
|
"{}MS-{}-{}",
|
||||||
|
duration,
|
||||||
|
startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
|
||||||
|
endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
|
||||||
|
)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.muyu.gateway.model.resp;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author DongZl
|
||||||
|
* @description: 验证码
|
||||||
|
* @Date 2023-11-12 下午 03:36
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CaptchaCodeResp {
|
||||||
|
|
||||||
|
private boolean captchaEnabled;
|
||||||
|
|
||||||
|
private String uuid;
|
||||||
|
|
||||||
|
private String img;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.muyu.gateway.service;
|
||||||
|
|
||||||
|
import com.muyu.common.core.exception.CaptchaException;
|
||||||
|
import com.muyu.common.core.domain.Result;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码处理
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
public interface ValidateCodeService {
|
||||||
|
/**
|
||||||
|
* 生成验证码
|
||||||
|
*/
|
||||||
|
public Result createCaptcha () throws IOException, CaptchaException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验验证码
|
||||||
|
*/
|
||||||
|
public void checkCaptcha (String key, String value) throws CaptchaException;
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package com.muyu.gateway.service.impl;
|
||||||
|
|
||||||
|
import com.google.code.kaptcha.Producer;
|
||||||
|
import com.muyu.common.core.constant.CacheConstants;
|
||||||
|
import com.muyu.common.core.constant.Constants;
|
||||||
|
import com.muyu.common.core.exception.CaptchaException;
|
||||||
|
import com.muyu.common.core.utils.StringUtils;
|
||||||
|
import com.muyu.common.core.utils.sign.Base64;
|
||||||
|
import com.muyu.common.core.utils.uuid.IdUtils;
|
||||||
|
import com.muyu.common.core.domain.Result;
|
||||||
|
import com.muyu.common.redis.service.RedisService;
|
||||||
|
import com.muyu.gateway.config.properties.CaptchaProperties;
|
||||||
|
import com.muyu.gateway.model.resp.CaptchaCodeResp;
|
||||||
|
import com.muyu.gateway.service.ValidateCodeService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.FastByteArrayOutputStream;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码实现处理
|
||||||
|
*
|
||||||
|
* @author muyu
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ValidateCodeServiceImpl implements ValidateCodeService {
|
||||||
|
@Resource(name = "captchaProducer")
|
||||||
|
private Producer captchaProducer;
|
||||||
|
|
||||||
|
@Resource(name = "captchaProducerMath")
|
||||||
|
private Producer captchaProducerMath;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisService redisService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CaptchaProperties captchaProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成验证码
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Result createCaptcha () throws IOException, CaptchaException {
|
||||||
|
boolean captchaEnabled = captchaProperties.getEnabled();
|
||||||
|
CaptchaCodeResp.CaptchaCodeRespBuilder respBuilder = CaptchaCodeResp.builder()
|
||||||
|
.captchaEnabled(captchaEnabled);
|
||||||
|
if (!captchaEnabled) {
|
||||||
|
return Result.success(respBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存验证码信息
|
||||||
|
String uuid = IdUtils.simpleUUID();
|
||||||
|
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
|
||||||
|
|
||||||
|
String capStr = null, code = null;
|
||||||
|
BufferedImage image = null;
|
||||||
|
|
||||||
|
String captchaType = captchaProperties.getType();
|
||||||
|
// 生成验证码
|
||||||
|
if ("math".equals(captchaType)) {
|
||||||
|
String capText = captchaProducerMath.createText();
|
||||||
|
capStr = capText.substring(0, capText.lastIndexOf("@"));
|
||||||
|
code = capText.substring(capText.lastIndexOf("@") + 1);
|
||||||
|
image = captchaProducerMath.createImage(capStr);
|
||||||
|
} else if ("char".equals(captchaType)) {
|
||||||
|
capStr = code = captchaProducer.createText();
|
||||||
|
image = captchaProducer.createImage(capStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
|
||||||
|
// 转换流信息写出
|
||||||
|
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
ImageIO.write(image, "jpg", os);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
CaptchaCodeResp captchaCodeResp = respBuilder.uuid(uuid)
|
||||||
|
.img(Base64.encode(os.toByteArray()))
|
||||||
|
.build();
|
||||||
|
return Result.success(captchaCodeResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验验证码
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void checkCaptcha (String code, String uuid) throws CaptchaException {
|
||||||
|
if (StringUtils.isEmpty(code)) {
|
||||||
|
throw new CaptchaException("验证码不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(uuid)) {
|
||||||
|
throw new CaptchaException("验证码已失效");
|
||||||
|
}
|
||||||
|
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
|
||||||
|
String captcha = redisService.getCacheObject(verifyKey);
|
||||||
|
redisService.deleteObject(verifyKey);
|
||||||
|
|
||||||
|
if (!code.equalsIgnoreCase(captcha)) {
|
||||||
|
throw new CaptchaException("验证码错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package com.muyu.gateway.utils;
|
||||||
|
|
||||||
|
import cn.hutool.core.net.NetUtil;
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.cloud.gateway.route.Route;
|
||||||
|
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web 工具类
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Log4j2
|
||||||
|
public class WebFrameworkUtils {
|
||||||
|
|
||||||
|
private static final String HEADER_TENANT_ID = "tenant-id";
|
||||||
|
|
||||||
|
private WebFrameworkUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Gateway 请求中的 header,设置到 HttpHeaders 中
|
||||||
|
*
|
||||||
|
* @param tenantId 租户编号
|
||||||
|
* @param httpHeaders WebClient 的请求
|
||||||
|
*/
|
||||||
|
public static void setTenantIdHeader(Long tenantId, HttpHeaders httpHeaders) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
httpHeaders.set(HEADER_TENANT_ID, String.valueOf(tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Long getTenantId(ServerWebExchange exchange) {
|
||||||
|
String tenantId = exchange.getRequest().getHeaders().getFirst(HEADER_TENANT_ID);
|
||||||
|
return tenantId != null ? Long.parseLong(tenantId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 JSON 字符串
|
||||||
|
*
|
||||||
|
* @param exchange 响应
|
||||||
|
* @param object 对象,会序列化成 JSON 字符串
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||||
|
public static Mono<Void> writeJSON(ServerWebExchange exchange, Object object) {
|
||||||
|
// 设置 header
|
||||||
|
ServerHttpResponse response = exchange.getResponse();
|
||||||
|
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
|
||||||
|
// 设置 body
|
||||||
|
return response.writeWith(Mono.fromSupplier(() -> {
|
||||||
|
DataBufferFactory bufferFactory = response.bufferFactory();
|
||||||
|
try {
|
||||||
|
return bufferFactory.wrap(JSONObject.toJSONString(object).getBytes());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ServerHttpRequest request = exchange.getRequest();
|
||||||
|
log.error("[writeJSON][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex);
|
||||||
|
return bufferFactory.wrap(new byte[0]);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得客户端 IP
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param exchange 请求
|
||||||
|
* @param otherHeaderNames 其它 header 名字的数组
|
||||||
|
* @return 客户端 IP
|
||||||
|
*/
|
||||||
|
public static String getClientIP(ServerWebExchange exchange, String... otherHeaderNames) {
|
||||||
|
String[] headers = { "X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR" };
|
||||||
|
if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
|
||||||
|
headers = ArrayUtil.addAll(headers, otherHeaderNames);
|
||||||
|
}
|
||||||
|
// 方式一,通过 header 获取
|
||||||
|
String ip;
|
||||||
|
for (String header : headers) {
|
||||||
|
ip = exchange.getRequest().getHeaders().getFirst(header);
|
||||||
|
if (!NetUtil.isUnknown(ip)) {
|
||||||
|
return NetUtil.getMultistageReverseProxyIp(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式二,通过 remoteAddress 获取
|
||||||
|
if (exchange.getRequest().getRemoteAddress() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ip = exchange.getRequest().getRemoteAddress().getHostString();
|
||||||
|
return NetUtil.getMultistageReverseProxyIp(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得请求匹配的 Route 路由
|
||||||
|
*
|
||||||
|
* @param exchange 请求
|
||||||
|
* @return 路由
|
||||||
|
*/
|
||||||
|
public static Route getGatewayRoute(ServerWebExchange exchange) {
|
||||||
|
return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
Spring Boot Version: ${spring-boot.version}
|
||||||
|
Spring Application Name: ${spring.application.name}
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Tomcat
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
# nacos线上地址
|
||||||
|
nacos:
|
||||||
|
addr: 139.224.188.104:8848
|
||||||
|
user-name: nacos
|
||||||
|
password: nacos
|
||||||
|
namespace: muyu-cloud
|
||||||
|
|
||||||
|
# Spring
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
# 应用名称
|
||||||
|
name: cloud-gateway
|
||||||
|
profiles:
|
||||||
|
# 环境配置
|
||||||
|
active: dev
|
||||||
|
cloud:
|
||||||
|
nacos:
|
||||||
|
discovery:
|
||||||
|
# 服务注册地址
|
||||||
|
server-addr: ${nacos.addr}
|
||||||
|
# nacos用户名
|
||||||
|
username: ${nacos.user-name}
|
||||||
|
# nacos密码
|
||||||
|
password: ${nacos.password}
|
||||||
|
# 命名空间
|
||||||
|
namespace: ${nacos.namespace}
|
||||||
|
config:
|
||||||
|
# 服务注册地址
|
||||||
|
server-addr: ${nacos.addr}
|
||||||
|
# nacos用户名
|
||||||
|
username: ${nacos.user-name}
|
||||||
|
# nacos密码
|
||||||
|
password: ${nacos.password}
|
||||||
|
# 命名空间
|
||||||
|
namespace: ${nacos.namespace}
|
||||||
|
# 配置文件格式
|
||||||
|
file-extension: yml
|
||||||
|
# 共享配置
|
||||||
|
shared-configs:
|
||||||
|
# 系统共享配置
|
||||||
|
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
|
||||||
|
# 系统环境Config共享配置
|
||||||
|
- application-config-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
|
||||||
|
sentinel:
|
||||||
|
# 取消控制台懒加载
|
||||||
|
eager: true
|
||||||
|
transport:
|
||||||
|
# 控制台地址
|
||||||
|
dashboard: 127.0.0.1:8718
|
||||||
|
# nacos配置持久化
|
||||||
|
datasource:
|
||||||
|
ds1:
|
||||||
|
nacos:
|
||||||
|
server-addr: ${nacos.addr}
|
||||||
|
dataId: sentinel-cloud-gateway
|
||||||
|
groupId: DEFAULT_GROUP
|
||||||
|
namespace: ${nacos.namespace}
|
||||||
|
data-type: json
|
||||||
|
rule-type: gw-flow
|
||||||
|
knife4j:
|
||||||
|
gateway:
|
||||||
|
enabled: true
|
||||||
|
# 指定服务发现的模式聚合微服务文档,并且是默认`default`分组
|
||||||
|
strategy: discover
|
||||||
|
discover:
|
||||||
|
enabled: true
|
||||||
|
# 指定版本号(Swagger2|OpenAPI3)
|
||||||
|
version : openapi3
|
||||||
|
# 需要排除的微服务(eg:网关服务)
|
||||||
|
excluded-services:
|
||||||
|
- cloud-monitor
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration scan="true" scanPeriod="60 seconds" debug="false">
|
||||||
|
<!-- 日志存放路径 -->
|
||||||
|
<property name="log.path" value="logs/cloud-gateway"/>
|
||||||
|
<!-- 日志输出格式 -->
|
||||||
|
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n"/>
|
||||||
|
|
||||||
|
<!-- 控制台输出 -->
|
||||||
|
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 系统日志输出 -->
|
||||||
|
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/info.log</file>
|
||||||
|
<!-- 循环政策:基于时间创建日志文件 -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 日志文件名格式 -->
|
||||||
|
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<!-- 过滤的级别 -->
|
||||||
|
<level>INFO</level>
|
||||||
|
<!-- 匹配时的操作:接收(记录) -->
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/error.log</file>
|
||||||
|
<!-- 循环政策:基于时间创建日志文件 -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 日志文件名格式 -->
|
||||||
|
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<!-- 过滤的级别 -->
|
||||||
|
<level>ERROR</level>
|
||||||
|
<!-- 匹配时的操作:接收(记录) -->
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 系统模块日志级别控制 -->
|
||||||
|
<logger name="com.muyu" level="info"/>
|
||||||
|
<!-- Spring日志级别控制 -->
|
||||||
|
<logger name="org.springframework" level="warn"/>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="console"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!--系统操作日志-->
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="file_info"/>
|
||||||
|
<appender-ref ref="file_error"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration scan="true" scanPeriod="60 seconds" debug="false">
|
||||||
|
<!-- 日志存放路径 -->
|
||||||
|
<property name="log.path" value="logs/cloud-gateway"/>
|
||||||
|
<!-- 日志输出格式 -->
|
||||||
|
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n"/>
|
||||||
|
<property name="log.sky.pattern" value="%d{HH:mm:ss.SSS} %yellow([%tid]) [%thread] %-5level %logger{20} - [%method,%line] - %msg%n"/>
|
||||||
|
|
||||||
|
<!-- 控制台输出 -->
|
||||||
|
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 系统日志输出 -->
|
||||||
|
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/info.log</file>
|
||||||
|
<!-- 循环政策:基于时间创建日志文件 -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 日志文件名格式 -->
|
||||||
|
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<!-- 过滤的级别 -->
|
||||||
|
<level>INFO</level>
|
||||||
|
<!-- 匹配时的操作:接收(记录) -->
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/error.log</file>
|
||||||
|
<!-- 循环政策:基于时间创建日志文件 -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 日志文件名格式 -->
|
||||||
|
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<!-- 过滤的级别 -->
|
||||||
|
<level>ERROR</level>
|
||||||
|
<!-- 匹配时的操作:接收(记录) -->
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 使用gRpc将日志发送到skywalking服务端 -->
|
||||||
|
<appender name="GRPC_LOG" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
|
||||||
|
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||||
|
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
|
||||||
|
<Pattern>${log.sky.pattern}</Pattern>
|
||||||
|
</layout>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="GRPC_LOG"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!-- 系统模块日志级别控制 -->
|
||||||
|
<logger name="com.muyu" level="info"/>
|
||||||
|
<!-- Spring日志级别控制 -->
|
||||||
|
<logger name="org.springframework" level="warn"/>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="console"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!--系统操作日志-->
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="file_info"/>
|
||||||
|
<appender-ref ref="file_error"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration scan="true" scanPeriod="60 seconds" debug="false">
|
||||||
|
<!-- 日志存放路径 -->
|
||||||
|
<property name="log.path" value="logs/cloud-gateway"/>
|
||||||
|
<!-- 日志输出格式 -->
|
||||||
|
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n"/>
|
||||||
|
<property name="log.sky.pattern" value="%d{HH:mm:ss.SSS} %yellow([%tid]) [%thread] %-5level %logger{20} - [%method,%line] - %msg%n"/>
|
||||||
|
|
||||||
|
<!-- 控制台输出 -->
|
||||||
|
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 系统日志输出 -->
|
||||||
|
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/info.log</file>
|
||||||
|
<!-- 循环政策:基于时间创建日志文件 -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 日志文件名格式 -->
|
||||||
|
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<!-- 过滤的级别 -->
|
||||||
|
<level>INFO</level>
|
||||||
|
<!-- 匹配时的操作:接收(记录) -->
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${log.path}/error.log</file>
|
||||||
|
<!-- 循环政策:基于时间创建日志文件 -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 日志文件名格式 -->
|
||||||
|
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 日志最大的历史 60天 -->
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${log.pattern}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<!-- 过滤的级别 -->
|
||||||
|
<level>ERROR</level>
|
||||||
|
<!-- 匹配时的操作:接收(记录) -->
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 使用gRpc将日志发送到skywalking服务端 -->
|
||||||
|
<appender name="GRPC_LOG" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
|
||||||
|
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||||
|
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
|
||||||
|
<Pattern>${log.sky.pattern}</Pattern>
|
||||||
|
</layout>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="GRPC_LOG"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!-- 系统模块日志级别控制 -->
|
||||||
|
<logger name="com.muyu" level="info"/>
|
||||||
|
<!-- Spring日志级别控制 -->
|
||||||
|
<logger name="org.springframework" level="warn"/>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="console"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!--系统操作日志-->
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="file_info"/>
|
||||||
|
<appender-ref ref="file_error"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
Loading…
Reference in New Issue