This commit is contained in:
Jesse-Ma
2022-07-21 11:45:02 +08:00
parent bacaff41ce
commit 83f70c2f88
15 changed files with 678 additions and 19 deletions

48
.gitignore vendored
View File

@@ -1,23 +1,33 @@
# Compiled class file
*.class
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
# Log file
*.log
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
# BlueJ files
*.ctxt
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
# Mobile Tools for Java (J2ME)
.mtj.tmp/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### VS Code ###
.vscode/

81
pom.xml Normal file
View File

@@ -0,0 +1,81 @@
<?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.7.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.flagnote</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2021.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<!--
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,12 @@
package com.flagnote.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

View File

@@ -0,0 +1,28 @@
package com.flagnote.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;
@Configuration
public class ApiLimiterConfiguration {
@Bean(name="remoteSessionKeyResolver")
public KeyResolver remoteSessionKeyResolver() {
return exchange -> Mono.just(exchange.getSession().block().getId());
}
@Bean(name="noteKeyResolver")
public KeyResolver noteKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
@Primary
@Bean(name="remoteAddrKeyResolver")
public KeyResolver remoteAddrKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}

View File

@@ -0,0 +1,15 @@
package com.flagnote.gateway.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
@EnableWebFlux
public class WebFluxWebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024);
}
}

View File

@@ -0,0 +1,57 @@
package com.flagnote.gateway.filter;
import java.util.Map;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.flagnote.gateway.utils.BizKeyUtils;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import reactor.core.publisher.Mono;
@Component
public class ValidateNoteCipherFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Map<String, String> uriVariables = ServerWebExchangeUtils.getUriTemplateVariables(exchange);
String noteKey = uriVariables.get("key");
String requestBody = exchange.getAttribute(ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR);
JSONObject body = JSONUtil.parseObj(requestBody);
String cipher = body.getStr("cipher");
String key = body.getStr("key");
if (!noteKey.equals(key)) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
if (!BizKeyUtils.validateCipher(noteKey, cipher)) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 优先级: 数字越小优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 2;
}
}

View File

@@ -0,0 +1,47 @@
package com.flagnote.gateway.filter;
import java.util.Map;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.flagnote.gateway.utils.BizKeyUtils;
import reactor.core.publisher.Mono;
/**
* 全局过滤器
*/
@Component
public class ValidateNoteKeyFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Map<String, String> uriVariables = ServerWebExchangeUtils.getUriTemplateVariables(exchange);
String noteKey = uriVariables.get("key");
if (!BizKeyUtils.validateKey(noteKey)) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 优先级: 数字越小优先级越高
*
* @return
*/
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1,39 @@
package com.flagnote.gateway.filter.factory;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig;
import org.springframework.cloud.gateway.support.GatewayToStringStyler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.flagnote.gateway.filter.ValidateNoteCipherFilter;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class ValidateNoteCipherGatewayFilterFactory
extends AbstractGatewayFilterFactory<AbstractGatewayFilterFactory.NameConfig> {
@Autowired
private ValidateNoteCipherFilter validateNoteCipherFilter;
public ValidateNoteCipherGatewayFilterFactory() {
super(NameConfig.class);
}
@Override
public GatewayFilter apply(NameConfig config) {
return validateNoteCipherFilter;
}
}

View File

@@ -0,0 +1,61 @@
package com.flagnote.gateway.filter.factory;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig;
import org.springframework.cloud.gateway.support.GatewayToStringStyler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.flagnote.gateway.filter.ValidateNoteKeyFilter;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class ValidateNoteKeyGatewayFilterFactory
extends AbstractGatewayFilterFactory<AbstractGatewayFilterFactory.NameConfig> {
@Autowired
private ValidateNoteKeyFilter validateNoteKeyFilter;
public ValidateNoteKeyGatewayFilterFactory() {
super(NameConfig.class);
}
// @Override
// public List<String> shortcutFieldOrder() {
// return Arrays.asList("name");
// }
@Override
public GatewayFilter apply(NameConfig config) {
return validateNoteKeyFilter;
// return new GatewayFilter() {
// @Override
// public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// // 获取请求路径
// URI uri = exchange.getRequest().getURI();
// log.info("获取到请求路径:{}", uri.toString());
// //
// log.info("配置属性:{}", config.getName());
// return chain.filter(exchange);
// }
//
// @Override
// public String toString() {
// return GatewayToStringStyler.filterToStringCreator(ValidateNoteKeyGatewayFilterFactory.this).toString();
// }
// };
}
}

View File

@@ -0,0 +1,19 @@
//package com.flagnote.gateway.resolver;
//
//import org.springframework.beans.factory.annotation.Qualifier;
//import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
//import org.springframework.stereotype.Component;
//import org.springframework.web.server.ServerWebExchange;
//
//import reactor.core.publisher.Mono;
//
//@Component("remoteAddrKeyResolver")
//@Qualifier("remoteAddrKeyResolver")
//public class RemoteAddrKeyResolver implements KeyResolver {
//
// @Override
// public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
// }
//
//}

View File

@@ -0,0 +1,19 @@
//package com.flagnote.gateway.resolver;
//
//import org.springframework.beans.factory.annotation.Qualifier;
//import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
//import org.springframework.stereotype.Component;
//import org.springframework.web.server.ServerWebExchange;
//
//import reactor.core.publisher.Mono;
//
//@Component("remoteSessionKeyResolver")
//@Qualifier("remoteSessionKeyResolver")
//public class RemoteSessionKeyResolver implements KeyResolver {
//
// @Override
// public Mono<String> resolve(ServerWebExchange exchange) {
// return Mono.just(exchange.getSession().block().getId());
// }
//
//}

View File

@@ -0,0 +1,100 @@
package com.flagnote.gateway.utils;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import org.springframework.util.DigestUtils;
public class BizKeyUtils {
public static final String MIX_STRING = "6v8muhqp8ta45ncsyi8y";
public static final String RANGE_STRING = "abcdefhikmnopqstuvwxyz23456789";
public static String getKey() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 13; i++) {
sb.append(RANGE_STRING.charAt(RandomUtils.nextInt(30)));
}
sb.append(getFilteredKey(md5(sb.toString() + MIX_STRING + getPrefixTime())));
return sb.toString();
}
public static String getFilteredKey(String key) {
String result = "";
for (int i = 0; i < key.length(); i++) {
String ts = String.valueOf(key.charAt(i));
if (RANGE_STRING.indexOf(ts) >= 0) {
result += ts;
}
if (result.length() == 3) {
return result;
}
}
for (int i = 1; i < 3; i++) {
result += "x";
}
return result;
}
public static Boolean validateKey(String key) {
String oKey = key.substring(0, 13);
String cKey = oKey + getFilteredKey(md5(oKey + MIX_STRING + getPrefixTime()));
if (cKey.equals(key)) {
return true;
}
cKey = oKey + getFilteredKey(md5(oKey + MIX_STRING + (getPrefixTime() - 1)));
if (cKey.equals(key)) {
return true;
}
cKey = oKey + getFilteredKey(md5(oKey + MIX_STRING + (getPrefixTime() + 1)));
if (cKey.equals(key)) {
return true;
}
return false;
}
public static String mixKey(String key) {
return md5(key + md5(MIX_STRING + key));
}
public static String md5(String text) {
try {
return DigestUtils.md5DigestAsHex(text.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("无法生成MD5");
}
}
public static Integer getPrefixTime() {
return Integer.parseInt(String.valueOf(new Date().getTime()).substring(0, 4));
}
public static String getCipher(String key) {
return md5(key + MIX_STRING + key);
}
public static Boolean validateCipher(String key,String cipher) {
return md5(key + MIX_STRING + key).equals(cipher);
}
public static String getSecretKey(String key, String password) {
return md5(key + md5(MIX_STRING + password));
}
}

View File

@@ -0,0 +1,15 @@
package com.flagnote.gateway.utils;
import java.util.concurrent.ThreadLocalRandom;
public class RandomUtils {
public static ThreadLocalRandom getRandom() {
return ThreadLocalRandom.current();
}
public static Integer nextInt(Integer num) {
return getRandom().nextInt(num);
}
}

View File

@@ -0,0 +1,143 @@
server:
port: 55555
spring:
codec:
max-in-memory-size: 100MB
servlet:
multipart:
# 最大支持文件大小
max-file-size: 100MB
# 最大支持请求大小
max-request-size: 100MB
redis:
host: cq.openif.com
port: 6379
timeout: 1000
poolMaxTotal: 10
poolMaxIdle: 10
poolMaxWait: 3
password: 1qazx1qazx
application:
name: gateway
cloud:
discovery:
client:
simple:
instances:
keyMetaService: # 一定要带端口
- uri: http://localhost:3333
# - uri: http://flagnote.com:3333
loadbalancer:
health-check:
# path:
# noteKey: /getHealth
initial-delay: 0 #运行状况检查计划程序的初始延迟值。
interval: 5s # 重新运行运行状况检查计划程序的时间间隔。
configurations: health-check #启用预定义的负载平衡器配置。
inetutils:
# 指定此客户端的ip
default-ip-address: localhost
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
routes:
- id: keyMeta
uri: lb://keyMetaService
order: -1
predicates:
- Path=/note/keyMeta
- Method=GET
filters:
- name: RequestRateLimiter
args:
# 如果返回的key是空的话false则不进行限流
# deny-empty-key: true
# status-code: TOO_MANY_REQUESTS
# 每秒产生多少个令牌
redis-rate-limiter.replenishRate: 1
# 1秒内最大的令牌即在1s内可以允许的突发流程设置为0表示阻止所有的请求
redis-rate-limiter.burstCapacity: 1
# 每次请求申请几个令牌
redis-rate-limiter.requestedTokens: 1
# IP 地址限流
key-resolver: "#{@remoteAddrKeyResolver}"
- name: RequestRateLimiter
args:
# 如果返回的key是空的话false则不进行限流
# deny-empty-key: true
# status-code: TOO_MANY_REQUESTS
# 每秒产生多少个令牌
redis-rate-limiter.replenishRate: 1
# 1秒内最大的令牌即在1s内可以允许的突发流程设置为0表示阻止所有的请求
redis-rate-limiter.burstCapacity: 3
# 每次请求申请几个令牌
redis-rate-limiter.requestedTokens: 3
# IP 地址限流
key-resolver: "#{@remoteSessionKeyResolver}"
- id: noteMeta
uri: lb://keyMetaService
order: -1
predicates:
- Path=/note/{key:[abcdefhikmnopqstuvwxyz23456789]{16}}/noteMeta
- Method=GET
filters:
- ValidateNoteKey
- id: getNote
uri: lb://keyMetaService
order: -1
predicates:
- Path=/note/{key:[abcdefhikmnopqstuvwxyz23456789]{16}}
- Method=GET
filters:
- ValidateNoteKey
- id: saveNote
uri: lb://keyMetaService
order: -1
predicates:
- Path=/note/{key:[abcdefhikmnopqstuvwxyz23456789]{16}}
- Method=POST
filters:
- name: ValidateNoteKey
- name: CacheRequestBody
args:
bodyClass: java.lang.String
- name: ValidateNoteCipher
- id: deleteNote
uri: lb://keyMetaService
order: -1
predicates:
- Path=/note/{key:[abcdefhikmnopqstuvwxyz23456789]{16}}/delete
- Method=POST
filters:
- ValidateNoteKey
- id: secretKey
uri: lb://keyMetaService
order: -1
predicates:
- Path=/note/{key:[abcdefhikmnopqstuvwxyz23456789]{16}}/secretKey
- Method=POST
filters:
- ValidateNoteKey
management:
endpoint:
health:
show-details: always
logging:
level:
org.springframework.cloud.gateway: trace
org.springframework.cloud.loadbalancer: trace
# org.springframework.web.reactive: trace

View File

@@ -0,0 +1,13 @@
package com.flagnote.gateway;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class GatewayApplicationTests {
@Test
void contextLoads() {
}
}