第一步:pom配置文件
<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.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<artifactId>wx_takeout_gatewayartifactId>
<name>网关服务name>
<description>提供路由、限流、安全策略、单点认证等功能description>
<parent>
<groupId>com.easystudygroupId>
<artifactId>wx_takeout_cloudartifactId>
<version>0.0.1-SNAPSHOTversion>
parent>
<dependencies>
<dependency>
<groupId>com.easystudygroupId>
<artifactId>wx_takeout_commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>*groupId>
<artifactId>*artifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
dependencies>
project>
第二步:自定义限流器
package com.easystudy.limiter;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.validation.constraints.Min;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.cloud.gateway.filter.ratelimit.AbstractRateLimiter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 自定义限流器
* @author Administrator
*
*/
@Component
@Primary
public class RateCheckRedisRateLimiter extends AbstractRateLimiter
implements ApplicationContextAware {
public static final String CONFIGURATION_PROPERTY_NAME = "redis-rate-limiter";
public static final String REDIS_SCRIPT_NAME = "redisRequestRateLimiterScript";
public static final String REMAINING_HEADER = "X-RateLimit-Remaining";
public static final String REPLENISH_RATE_HEADER = "X-RateLimit-Replenish-Rate";
public static final String BURST_CAPACITY_HEADER = "X-RateLimit-Burst-Capacity";
private Log log = LogFactory.getLog(getClass());
private ReactiveRedisTemplate redisTemplate;
// Lua搅拌额
private RedisScript> script;
// 初始化标识
private AtomicBoolean initialized = new AtomicBoolean(false);
// 系统默认配置
private Config defaultConfig;
private boolean includeHeaders = true;
private String remainingHeader = REMAINING_HEADER;
private String replenishRateHeader = REPLENISH_RATE_HEADER;
private String burstCapacityHeader = BURST_CAPACITY_HEADER;
public RateCheckRedisRateLimiter() {
super(Config.class, CONFIGURATION_PROPERTY_NAME, null);
}
public RateCheckRedisRateLimiter(ReactiveRedisTemplate redisTemplate, RedisScript> script, Validator validator) {
super(Config.class, CONFIGURATION_PROPERTY_NAME, validator);
this.redisTemplate = redisTemplate;
this.script = script;
initialized.compareAndSet(false, true);
}
public RateCheckRedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) {
super(Config.class, CONFIGURATION_PROPERTY_NAME, null);
this.defaultConfig = new Config().setReplenishRate(defaultReplenishRate).setBurstCapacity(defaultBurstCapacity);
}
public boolean isIncludeHeaders() {
return includeHeaders;
}
public void setIncludeHeaders(boolean includeHeaders) {
this.includeHeaders = includeHeaders;
}
public String getRemainingHeader() {
return remainingHeader;
}
public void setRemainingHeader(String remainingHeader) {
this.remainingHeader = remainingHeader;
}
public String getReplenishRateHeader() {
return replenishRateHeader;
}
public void setReplenishRateHeader(String replenishRateHeader) {
this.replenishRateHeader = replenishRateHeader;
}
public String getBurstCapacityHeader() {
return burstCapacityHeader;
}
public void setBurstCapacityHeader(String burstCapacityHeader) {
this.burstCapacityHeader = burstCapacityHeader;
}
/**
* 根据key(接口)找到对应的限流配置
* @param key
* @return
*/
@SuppressWarnings("unused")
private Config setConfig(String key) {
// 令牌桶的容积,希望允许用户每秒处理多少个请求,而不会丢失任何请求
int replenishRate = 100;
// 用户允许在一秒钟内完成的最大请求数,这是令牌桶可以容纳的令牌的数量,将此值设置为零将阻止所有请求
int burstCapacity = 10;
defaultConfig = new Config()
.setReplenishRate(replenishRate)
.setBurstCapacity(burstCapacity);
return defaultConfig;
}
Config getDefaultConfig() {
return defaultConfig;
}
@Override
@SuppressWarnings("unchecked")
public void setApplicationContext(ApplicationContext context) throws BeansException {
if (initialized.compareAndSet(false, true)) {
this.redisTemplate = context.getBean("stringReactiveRedisTemplate", ReactiveRedisTemplate.class);
this.script = context.getBean(REDIS_SCRIPT_NAME, RedisScript.class);
if (context.getBeanNamesForType(Validator.class).length > 0) {
this.setValidator(context.getBean(Validator.class));
}
}
}
/**
* This uses a basic token bucket algorithm and relies on the fact that
* Redis scripts execute atomically. No other operations can run between
* fetching the count and writing the new count.
* @param routeId 路由服务名,如配置中的spring.cloud
*/
@Override
public Mono isAllowed(String routeId, String id) {
// 会判断RedisRateLimiter是否初始化了
if (!this.initialized.get()) {
throw new IllegalStateException("RedisRateLimiter is not initialized");
}
// 根据key(接口)找到对应的限流配置
//Config routeConfig = setConfig(id);
// 获取routeId对应的限流配置
Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);
if (routeConfig == null) {
throw new IllegalArgumentException("No Configuration found for route " + routeId);
}
// 允许用户每秒做多少次请求
int replenishRate = routeConfig.getReplenishRate();
int burstCapacity = routeConfig.getBurstCapacity();
try {
// 限流key的名称根据id获取对应键值,id为自定义的值如ip、user、path
List keys = getKeys(id);
// 传给LUA脚本的4个参数[令牌桶的容积,一秒钟内完成的最大请求数,当前时间]
List scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
// 执行LUA脚本
Flux> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
// .log("redisratelimiter", Level.FINER);
return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
.reduce(new ArrayList(), (longs, l) -> {
longs.addAll(l);
return longs;
}).map(results -> {
boolean allowed = results.get(0) == 1L;
Long tokensLeft = results.get(1);
Response response = new Response(allowed, getHeaders(replenishRate, burstCapacity, tokensLeft));
if (log.isDebugEnabled()) {
log.debug("response: " + response);
}
return response;
});
} catch (Exception e) {
/*
* We don't want a hard dependency on Redis to allow traffic. Make
* sure to set an alert so you know if this is happening too much.
* Stripe's observed failure rate is 0.01%.
*/
e.printStackTrace();
log.error("Error determining if user allowed from redis", e);
}
return Mono.just(new Response(true, getHeaders(replenishRate, burstCapacity, -1L)));
}
public HashMap getHeaders(Integer replenishRate, Integer burstCapacity, Long tokensLeft) {
HashMap headers = new HashMap<>();
headers.put(this.remainingHeader, tokensLeft.toString());
headers.put(this.replenishRateHeader, String.valueOf(replenishRate));
headers.put(this.burstCapacityHeader, String.valueOf(burstCapacity));
return headers;
}
static List getKeys(String id) {
// use `{}` around keys to use Redis Key hash tags
// this allows for using redis cluster
// Make a unique key per user.
String prefix = "request_rate_limiter.{" + id;
// You need two Redis keys for Token Bucket.
String tokenKey = prefix + "}.tokens";
String timestampKey = prefix + "}.timestamp";
return Arrays.asList(tokenKey, timestampKey);
}
/**
* 系统配置,要与 key-resolver名称匹配上,spring自动注入(spring可以处理处理‘-’并使用驼峰命名)
* redis-rate-limiter.replenishRate: 10 # 令牌桶的容积,希望允许用户每秒处理多少个请求,而不会丢失任何请求
* redis-rate-limiter.burstCapacity: 20 # 用户允许在一秒钟内完成的最大请求数,这是令牌桶可以容纳的令牌的数量,将此值设置为零将阻止所有请求
* @author Administrator
*/
@Validated
public static class Config {
@Min(1)
private int replenishRate;
@Min(1)
private int burstCapacity = 1;
public int getReplenishRate() {
return replenishRate;
}
public Config setReplenishRate(int replenishRate) {
this.replenishRate = replenishRate;
return this;
}
public int getBurstCapacity() {
return burstCapacity;
}
public Config setBurstCapacity(int burstCapacity) {
this.burstCapacity = burstCapacity;
return this;
}
@Override
public String toString() {
return "Config{" + "replenishRate=" + replenishRate + ", burstCapacity=" + burstCapacity + '}';
}
}
}
第三步、限流器工厂
package com.easystudy.limiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.easystudy.error.ErrorCode;
import com.easystudy.error.ReturnValue;
import reactor.core.publisher.Mono;
@Component
public class RateCheckLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory implements ApplicationContextAware {
private static Logger log = LoggerFactory.getLogger(RateCheckLimiterGatewayFilterFactory.class);
private static ApplicationContext applicationContext;
private RateCheckRedisRateLimiter rateLimiter;
private KeyResolver keyResolver;
public RateCheckLimiterGatewayFilterFactory() {
super(Config.class);
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
log.info("RateCheckLimiterGatewayFilterFactory.setApplicationContext,applicationContext=" + context);
applicationContext = context;
}
@Override
public GatewayFilter apply(Config config) {
this.rateLimiter = applicationContext.getBean(RateCheckRedisRateLimiter.class);
this.keyResolver = applicationContext.getBean(config.keyResolver, KeyResolver.class);
return (exchange, chain) -> {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
return keyResolver.resolve(exchange).flatMap(key ->
// TODO: if key is empty?
rateLimiter.isAllowed(route.getId(), key).flatMap(response -> {
log.info("回复: " + response);
System.out.println("回复: " + response);
// TODO: set some headers for rate, tokens left
if (response.isAllowed()) {
return chain.filter(exchange);
}
// 超过了限流的response返回值
return setRateCheckResponse(exchange);
}));
};
}
private Mono setRateCheckResponse(ServerWebExchange exchange) {
// 超过了限流
ServerHttpResponse response = exchange.getResponse();
// 设置headers
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
// 设置body HttpStatus.TOO_MANY_REQUESTS.value()
ReturnValue body = new ReturnValue(ErrorCode.ERROR_BUSY);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(body.toJsonString().getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
}
/**
* 系统配置,要与 key-resolver名称匹配上,spring自动注入(spring可以处理处理‘-’并使用驼峰命名)
* @author Administrator
*/
public static class Config {
private String keyResolver;//限流id
public String getKeyResolver() {
return keyResolver;
}
public void setKeyResolver(String keyResolver) {
this.keyResolver = keyResolver;
}
}
}
第四步、限流配置:
server:
#监听端口
port: 7010
#servlet上下文根路径
servlet:
#此时的gatewy是不起作用的,匹配路径为根路径
#context-path: /gateway
spring:
application:
name: gateway
#安全认证的配置--切记,此节点在spring节点之下,否则无法使用用户名密码登录
security:
# eureka服务端启用安全验证后,服务端必须【添加一个安全认证类】,开启httpbasic方式登录【因为高版本已经启用这种方式】
user:
# 用户名
name: admin
# 密码
password: dw123456
#redis配置-限流存储
redis:
#单台主机
host: 127.0.0.1
port: 6379
pool:
maxIdle: 300
minIdle: 100
max-wait: -1
max-active: 600
cloud:
#gateway反向代理配置
gateway:
routes:
#认证的http走这里
- id: service-a
#lb代表从注册中心获取服务,后面接的就是你需要转发到的服务名称,这个服务名称必须跟eureka中的对应
#uri: lb://service-a
uri:https://www.baidu.com
#路由条件
predicates:
- Path= /service-a/**
#过滤规则
filters:
- StripPrefix= 1
#以及配置同- RequestRateLimiter=10, 20, #{@userKeyResolver}
#使用内置的请求过滤器限流,常用的有Hystrix断路由,RequestRateLimiter限流,StripPrefix截取请求url
#- name: RequestRateLimiter #使用默认的RequestRateLimiterGatewayFilterFactory(会查找过滤器名+GatewayFilterFactory的Bean去过滤)
- name: RateCheckLimiter #查找RateCheckLimiterGatewayFilterFactory进行过滤
args:
#redis限流
redis-rate-limiter.replenishRate: 10 # 令牌桶的容积,希望允许用户每秒处理多少个请求,而不会丢失任何请求
redis-rate-limiter.burstCapacity: 20 # 用户允许在一秒钟内完成的最大请求数,这是令牌桶可以容纳的令牌的数量,将此值设置为零将阻止所有请求
#采用自定义配置限流,限流键解析器 Bean对象名字,是引用名为ipKeyResolver的bean的SpEL表达式
#使用SpEL按名称引用bean见GatewayConfiguration或者不用SPEL直接指定bean名称,@Bean(name="apiKeyResolver")
#而 keyResolver是选填 ,为空时使用默认值 defaultKeyResolver
key-resolver: apiKeyResolver
#websocket走这里
- id: websocket
uri: lb:ws://websocket
#${server.servlet.context-path}/websocket/**
predicates:
- Path= /websocket/**
filters:
- StripPrefix= 1
# eureka配置
eureka:
client:
#从eureka服务器获取注册表信息的频率(以秒为单位)-用于客户端
registry-fetch-interval-seconds: 10
# 是否将eureka自身作为应用注册到eureka注册中心,默认为true
registerWithEureka: true
service-url:
# 这里可以填写所有的eureka服务器地址并以','分离,当前面不能注册时候,自动选择后面的进行注册,排除单点故障问题
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@127.0.0.1:7002/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@127.0.0.1:7004/eureka/
instance:
#显示ip
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}
#配置与服务器心跳间隔-用于客户端
lease-renewal-interval-in-seconds: 30
#配置服务超时时间(此值将设置为至少高于leaseRenewalIntervalInSeconds中指定的值)-用于客户端
#当服务关闭超过,这个时间时,eureka服务器会清除掉这个服务,配置此项要关闭服务器的自我保护模式-用于客户端
lease-expiration-duration-in-seconds: 60
#客户端自定义数据元数据-给其他eureka客户端调用--用户客户端
metadata-map:
#自定义元数据serverid
serverid: gateway
logging:
level:
org.springframework.cloud.gateway: DEBUG #TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG
#让hibernate打印出执行的SQL
#root: INFO
#org.hibernate: INFO
#org.hibernate.type.descriptor.sql.BasicBinder: TRACE
#org.hibernate.type.descriptor.sql.BasicExtractor: TRACE
第五步、自定义Bean配置
package com.easystudy.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
@Configuration
public class GatewayConfiguration {
/**
* 根据ip限流
* @return
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
/**
* 根据用户限流:使用这种方式限流,请求路径中必须携带userId参数
* @return
*/
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
/**
* 接口限流:获取请求地址的uri作为限流key。
*/
@Bean(name="apiKeyResolver")
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
}
全局过滤器:
package com.easystudy.config;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 自定义GlobalFilter只要使用了@Configuration注解或者配置为一个spring bean就会生效,不需要在RouteLocator中配置
* @author Administrator
*/
@Configuration
public class GlobalRouteFilter implements GlobalFilter {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
builder.header("GlobalFilter","GlobalFilter success");
//chain.filter(exchange.mutate().request(builder.build()).build());
return chain.filter(exchange.mutate().request(builder.build()).build());
}
}
跨域配置:
package com.easystudy.config;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.gateway.discovery.DiscoveryClientRouteDefinitionLocator;
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* 如果是前后端分离的项目,需要增加配置以解决跨域问题
*/
@Configuration
public class RouteConfiguration {
//这里为支持的请求头,如果有自定义的header字段请自己添加(不知道为什么不能使用*)
private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN,token,username,client";
private static final String ALLOWED_METHODS = "*";
private static final String ALLOWED_ORIGIN = "*";
private static final String ALLOWED_Expose = "*";
private static final String MAX_AGE = "18000L";
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
headers.add("Access-Control-Expose-Headers", ALLOWED_Expose);
headers.add("Access-Control-Allow-Credentials", "true");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
/**
*
*如果使用了注册中心(如:Eureka),进行控制则需要增加如下配置
*/
@Bean
public RouteDefinitionLocator discoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
}
}
第六步、入口函数
package com.easystudy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 该注解会根据配置文件中的地址,将服务自身注册到服务注册中
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
启动项目即可!
其他配置