<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
server:
port: 30020
http2:
enabled: true
servlet:
context-path: /i5xforyou
spring:
application:
name: i5xforyou-service-gateway
eureka:
instance:
prefer-ip-address: true
status-page-url-path: /actuator/info
health-check-url-path: /actuator/health
client:
register-with-eureka: true
fetch-registry: false
service-url:
defaultZone: http://localhost:50000/eureka/
server.servlet.context-path,由于gateway用的是webflux,所以这个设定其实是不生效的,现在还没有一个key来设定webflux的context-path
为了nginx能把请求都统一路由到gateway,所以必须要有一个统一的前缀,这里定义为i5xforyou,nginx可以设置请求前缀为/i5xforyou的请求都转发到gateway服务上。
spring:
cloud:
gateway:
default-filters:
routes:
#------------------------------------------------------------------------
- id: i5xforyou-biz-auth
uri: lb://i5xforyou-biz-auth
predicates:
- Path= ${server.servlet.context-path}/auth/**
filters:
- StripPrefix= 1
#------------------------------------------------------------------------
- id: i5xforyou-biz-kanjia-websocket
uri: lb:ws://i5xforyou-biz-kanjia-websocket
predicates:
- Path= ${server.servlet.context-path}/kanjia-websocket/**
filters:
- StripPrefix= 1
predicates:请求匹配规则,为一个数组,每个规则为并且的关系。包含:
1. name:规则名称,目前有10个,有Path,Query,Method,Header,After,Before,Between,Cookie,Host,RemoteAddr
2. args:参数key-value键值对,例:
```
predicates:
- name: Query
args:
foo: ba
```
等价于
```
predicates:
- Query=foo, ba
```
如果args不写key的,会自动生成一个id,如下会生成一个xxx0的key,值为/foo/*
```
predicates:
- Path=/foo/*
```
3. /代表一层路径,/*代表多层目录
4. 具体详情参照:http://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.0.0.M9/multi/multi_gateway-request-predicates-factories.html
filters:请求过滤filter,为一个数组,每个filter都会顺序执行。包含:
1. name:过滤filter名称,常用的有Hystrix断路由,RequestRateLimiter限流,StripPrefix截取请求url
2. args:参数key-value键值对,例:
```
filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/incaseoffailureusethis
```
如果args不写key的,会自动生成一个id,如下会生成一个xxx0的key,值为1
```
filters:
- StripPrefix= 1
```
spring:
cloud:
gateway:
routes:
- id: nameRoot
uri: lb://nameservice
predicates:
- Path=/name/**
filters:
- StripPrefix=1
/name/bar/foo的请求会被转发为http://nameserviceip:nameserviceport/bar/foo
引入pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
配置文件:
spring:
cloud:
gateway:
default-filters:
routes:
#------------------------------------------------------------------------
- id: i5xforyou-biz-auth
uri: lb://i5xforyou-biz-auth
predicates:
- Path= ${server.servlet.context-path}/auth/**
filters:
- StripPrefix= 1
- name: Hystrix
args:
name: authHystrixCommand
fallbackUri: forward:/hystrixTimeout
#设置断路由的超时时间,毫秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds= 30000
name表示HystrixCommand代码的名称,fallbackUri表示触发断路由后的跳转请求url
HystrixCommand代码
@RestController
public class HystrixCommandController {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/hystrixTimeout")
public JsonPackage hystrixTimeout() {
log.error("i5xforyou-service-gateway触发了断路由");
return JsonPackage.getHystrixJsonPackage();
}
@HystrixCommand(commandKey="authHystrixCommand")
public JsonPackage authHystrixCommand() {
return JsonPackage.getHystrixJsonPackage();
}
}
spring:
cloud:
gateway:
default-filters:
routes:
#------------------------------------------------------------------------
- id: i5xforyou-biz-auth
uri: lb://i5xforyou-biz-auth
predicates:
- Path= ${server.servlet.context-path}/auth/**
filters:
- StripPrefix= 1
- name: Retry
args:
retries: 3 #重试次数,默认3,不包含本次
status: 404 #重试response code,默认没有
statusSeries: 500 #重试response code的系列,100(info),200(success),300(redirect),400(client error),500(server error),默认500
method: GET #重试的request请求,默认GET
没有timeout超时重试,并且没有retriesNextServer设置,导致多次重试都是到同一个服务实例。不太实用。
自定义一个用来检验jwt是否合法的gateway filter为例进行说明。
定义一个JwtCheckGatewayFilterFactory类实现GatewayFilterFactory接口。
类名一定要为filterName + GatewayFilterFactory,如这里定义为JwtCheckGatewayFilterFactory的话,它的filterName就是JwtCheck
实现gateway filter的业务逻辑
@Component
public class JwtCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
//return chain.filter(exchange);
String jwtToken = exchange.getRequest().getHeaders().getFirst("Authorization");
//校验jwtToken的合法性
if (JwtUtil.verifyToken(jwtToken) != null) {
//合法
return chain.filter(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
JsonPackage jsonPackage = new JsonPackage();
jsonPackage.setStatus(110);
jsonPackage.setMessage("未登录或登录超时");
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(jsonPackage.toJSONString().getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
};
}
}
设定配置文件即可
spring:
cloud:
gateway:
default-filters:
routes:
#------------------------------------------------------------------------
- id: i5xforyou-biz-auth
uri: lb://i5xforyou-biz-auth
predicates:
- Path= ${server.servlet.context-path}/auth/**
filters:
- StripPrefix= 1
- JwtCheck
gateway自带的RequestRateLimiter可定制的内容太少,真正用的话,需要:
1. 自定义限流后的response返回值
2. 不同的key(即接口)限流数不同
所以需要自定义一个限流的gateway filter
@Component
public class RateCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<RateCheckGatewayFilterFactory.Config> implements ApplicationContextAware {
private static Logger log = LoggerFactory.getLogger(RateCheckGatewayFilterFactory.class);
private static ApplicationContext applicationContext;
private RateCheckRedisRateLimiter rateLimiter;
private KeyResolver keyResolver;
public RateCheckGatewayFilterFactory() {
super(Config.class);
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
log.info("RateCheckGatewayFilterFactory.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: " + response);
// TODO: set some headers for rate, tokens left
if (response.isAllowed()) {
return chain.filter(exchange);
}
//超过了限流的response返回值
return setRateCheckResponse(exchange);
}));
};
}
private Mono<Void> 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
JsonPackage jsonPackage = new JsonPackage();
jsonPackage.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
jsonPackage.setMessage("系统繁忙,请稍后重试");
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(jsonPackage.toJSONString().getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
}
public static class Config {
private String keyResolver;//限流id
public String getKeyResolver() {
return keyResolver;
}
public void setKeyResolver(String keyResolver) {
this.keyResolver = keyResolver;
}
}
}
在这里可以自定义限流后的response返回值
@Component
@Primary
public class RateCheckRedisRateLimiter extends AbstractRateLimiter<RateCheckRedisRateLimiter.Config> implements ApplicationContextAware {
public static final String CONFIGURATION_PROPERTY_NAME = "redis-rate-limiter";
public static final String REDIS_SCRIPT_NAME = "redisRequestRateLimiterScript";
private static Logger log = LoggerFactory.getLogger(RateCheckGatewayFilterFactory.class);
private ReactiveRedisTemplate<String, String> redisTemplate;
private RedisScript<List<Long>> script;
private AtomicBoolean initialized = new AtomicBoolean(false);
private Config defaultConfig;
public RateCheckRedisRateLimiter() {
super(Config.class, CONFIGURATION_PROPERTY_NAME, null);
}
// public RateCheckRedisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate,
// RedisScript<List<Long>> 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);
// }
private Config setConfig(String key) {
//TODO 根据key(接口)找到对应的限流配置
int replenishRate = 0;//令牌通流量,每秒
int burstCapacity = 0;//令牌通容量
defaultConfig = new Config()
.setReplenishRate(replenishRate)
.setBurstCapacity(burstCapacity);
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));
}
}
}
/* for testing */
Config getDefaultConfig() {
return defaultConfig;
}
/**
* 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.
*/
@Override
public Mono<Response> isAllowed(String routeId, String id) {
if (!this.initialized.get()) {
throw new IllegalStateException("RedisRateLimiter is not initialized");
}
//根据key(接口)找到对应的限流配置
Config routeConfig = setConfig(id);
// How many requests per second do you want a user to be allowed to do?
int replenishRate = routeConfig.getReplenishRate();
// How much bursting do you want to allow?
int burstCapacity = routeConfig.getBurstCapacity();
try {
List<String> keys = getKeys(id);
// The arguments to the LUA script. time() returns unixtime in seconds.
List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
Instant.now().getEpochSecond() + "", "1");
// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
Flux<List<Long>> 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<Long>(), (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, 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%.
*/
log.error("Error determining if user allowed from redis", e);
}
return Mono.just(new Response(true, -1));
}
static List<String> 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);
}
@Validated
public static class Config {
@Min(1)
private int replenishRate;
@Min(0)
private int burstCapacity = 0;
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 +
'}';
}
}
}
在这里可以根据不同的key(接口)来获取不同的限流设置,具体配置文件及映射方法根据自己项目需要自行配置即可。
spring:
cloud:
gateway:
default-filters:
routes:
#------------------------------------------------------------------------
- id: i5xforyou-biz-auth
uri: lb://i5xforyou-biz-auth
predicates:
- Path= ${server.servlet.context-path}/auth/**
filters:
- StripPrefix= 1
- name: RateCheck
args:
keyResolver: apiKeyResolver
apiKeyResolver为keyResolver bean的名字
例:
@Configuration
public class GatewayConfiguration {
@Bean(name="apiKeyResolver")
public KeyResolver apiKeyResolver() {
//根据api接口来限流
return exchange -> {
return Mono.just(exchange.getRequest().getPath().value());
};
}
}
限流算法是通过redis来存储的,需要加入其响应式的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
配置连接即可
spring:
redis:
cluster:
nodes: ${redis.host.cluster}
password: ${redis.password}