在我看来,在某些场景下,网关就像是一个公共方法,把项目中的都要用到的一些功能提出来,抽象成一个服务。比如,我们可以在业务网关上做日志收集、Token校验等等,当然这么理解很狭隘,因为网关的能力远不止如此,但是不妨碍我们更好地理解它。下面的例子演示了,如何在网关校验Token,并提取用户信息放到Header中传给下游业务系统。
用户登录成功以后,生成token,此后的所有请求都带着token。网关负责校验token,并将用户信息放入请求Header,以便下游系统可以方便的获取用户信息。
公共项目:cjs-commons-jwt
认证服务:cjs-auth-service
网关服务:cjs-gateway-example
因为生成token在认证服务中,token校验在网关服务中,因此,我把这一部分写在了公共项目cjs-commons-jwt中
pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cjs.example</groupId>
<artifactId>cjs-commons-jwt</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.66</version>
</dependency>
</dependencies>
</project>
JWTUtil.java
/**
* @author ChengJianSheng
* @date 2020-03-08
*/
public class JWTUtil {
public static final long TOKEN_EXPIRE_TIME = 7200 * 1000;
private static final String ISSUER = "cheng";
/**
* 生成Token
*
* @param username 用户标识(不一定是用户名,有可能是用户ID或者手机号什么的)
* @param secretKey
* @return
*/
public static String generateToken(String username, String secretKey) {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
Date now = new Date();
Date expireTime = new Date(now.getTime() + TOKEN_EXPIRE_TIME);
String token = JWT.create()
.withIssuer(ISSUER)
.withIssuedAt(now)
.withExpiresAt(expireTime)
.withClaim("username", username)
.sign(algorithm);
return token;
}
/**
* 校验Token
*
* @param token
* @param secretKey
* @return
*/
public static void verifyToken(String token, String secretKey) {
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(ISSUER).build();
jwtVerifier.verify(token);
} catch (JWTDecodeException jwtDecodeException) {
throw new TokenAuthenticationException(ResponseCodeEnum.TOKEN_INVALID.getCode(), ResponseCodeEnum.TOKEN_INVALID.getMessage());
} catch (SignatureVerificationException signatureVerificationException) {
throw new TokenAuthenticationException(ResponseCodeEnum.TOKEN_SIGNATURE_INVALID.getCode(), ResponseCodeEnum.TOKEN_SIGNATURE_INVALID.getMessage());
} catch (TokenExpiredException tokenExpiredException) {
throw new TokenAuthenticationException(ResponseCodeEnum.TOKEN_EXPIRED.getCode(), ResponseCodeEnum.TOKEN_INVALID.getMessage());
} catch (Exception ex) {
throw new TokenAuthenticationException(ResponseCodeEnum.UNKNOWN_ERROR.getCode(), ResponseCodeEnum.UNKNOWN_ERROR.getMessage());
}
}
/**
* 从Token中提取用户信息
*
* @param token
* @return
*/
public static String getUserInfo(String token) {
DecodedJWT decodedJWT = JWT.decode(token);
String username = decodedJWT.getClaim("username").asString();
return username;
}
}
ResponseCodeEnum.java
/**
* @author ChengJianSheng
* @date 2020-03-08
*/
public enum ResponseCodeEnum {
SUCCESS(0, "成功"),
FAIL(-1, "失败"),
LOGIN_ERROR(1000, "用户名或密码错误"),
UNKNOWN_ERROR(2000, "未知错误"),
PARAMETER_ILLEGAL(2001, "参数不合法"),
TOKEN_INVALID(2002, "无效的Token"),
TOKEN_SIGNATURE_INVALID(2003, "无效的签名"),
TOKEN_EXPIRED(2004, "token已过期"),
TOKEN_MISSION(2005, "token缺失"),
REFRESH_TOKEN_INVALID(2006, "刷新Token无效");
private int code;
private String message;
ResponseCodeEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
ResponseResult.java
/**
* @author ChengJianSheng
* @date 2020-03-08
*/
public class ResponseResult<T> {
private int code = 0;
private String msg;
private T data;
public ResponseResult(int code, String msg) {
this.code = code;
this.msg = msg;
}
public ResponseResult(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static ResponseResult success() {
return new ResponseResult(ResponseCodeEnum.SUCCESS.getCode(), ResponseCodeEnum.SUCCESS.getMessage());
}
public static <T> ResponseResult<T> success(T data) {
return new ResponseResult(ResponseCodeEnum.SUCCESS.getCode(), ResponseCodeEnum.SUCCESS.getMessage(), data);
}
public static ResponseResult error(int code, String msg) {
return new ResponseResult(code, msg);
}
public static <T> ResponseResult<T> error(int code, String msg, T data) {
return new ResponseResult(code, msg, data);
}
public boolean isSuccess() {
return code == 0;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
这一部分在cjs-auth-service中
pom.xml
<?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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cjs.example</groupId>
<artifactId>cjs-auth-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cjs-auth-service</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>com.cjs.example</groupId>
<artifactId>cjs-commons-jwt</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
LoginController.java
/**
* @author ChengJianSheng
* @date 2020-03-08
*/
@RestController
public class LoginController {
/**
* Apollo 或 Nacos
*/
@Value("${secretKey:123456}")
private String secretKey;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 登录
*/
@PostMapping("/login")
public ResponseResult login(@RequestBody @Validated LoginRequest request, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseResult.error(ResponseCodeEnum.PARAMETER_ILLEGAL.getCode(), ResponseCodeEnum.PARAMETER_ILLEGAL.getMessage());
}
String username = request.getUsername();
String password = request.getPassword();
// 假设查询到用户ID是1001
String userId = "1001";
if ("hello".equals(username) && "world".equals(password)) {
// 生成Token
String token = JWTUtil.generateToken(userId, secretKey);
// 生成刷新Token
String refreshToken = UUID.randomUUID().toString().replace("-", "");
// 放入缓存
HashOperations<String, String, String> hashOperations = stringRedisTemplate.opsForHash();
// hashOperations.put(refreshToken, "token", token);
// hashOperations.put(refreshToken, "user", username);
// stringRedisTemplate.expire(refreshToken, JWTUtil.TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
/**
* 如果可以允许用户退出后token如果在有效期内仍然可以使用的话,那么就不需要存Redis
* 因为,token要跟用户做关联的话,就必须得每次都带一个用户标识,
* 那么校验token实际上就变成了校验token和用户标识的关联关系是否正确,且token是否有效
*/
// String key = MD5Encoder.encode(userId.getBytes());
String key = userId;
hashOperations.put(key, "token", token);
hashOperations.put(key, "refreshToken", refreshToken);
stringRedisTemplate.expire(key, JWTUtil.TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
LoginResponse loginResponse = new LoginResponse();
loginResponse.setToken(token);
loginResponse.setRefreshToken(refreshToken);
loginResponse.setUsername(userId);
return ResponseResult.success(loginResponse);
}
return ResponseResult.error(ResponseCodeEnum.LOGIN_ERROR.getCode(), ResponseCodeEnum.LOGIN_ERROR.getMessage());
}
/**
* 退出
*/
@GetMapping("/logout")
public ResponseResult logout(@RequestParam("userId") String userId) {
HashOperations<String, String, String> hashOperations = stringRedisTemplate.opsForHash();
String key = userId;
hashOperations.delete(key);
return ResponseResult.success();
}
/**
* 刷新Token
*/
@PostMapping("/refreshToken")
public ResponseResult refreshToken(@RequestBody @Validated RefreshRequest request, BindingResult bindingResult) {
String userId = request.getUserId();
String refreshToken = request.getRefreshToken();
HashOperations<String, String, String> hashOperations = stringRedisTemplate.opsForHash();
String key = userId;
String originalRefreshToken = hashOperations.get(key, "refreshToken");
if (StringUtils.isBlank(originalRefreshToken) || !originalRefreshToken.equals(refreshToken)) {
return ResponseResult.error(ResponseCodeEnum.REFRESH_TOKEN_INVALID.getCode(), ResponseCodeEnum.REFRESH_TOKEN_INVALID.getMessage());
}
// 生成新token
String newToken = JWTUtil.generateToken(userId, secretKey);
hashOperations.put(key, "token", newToken);
stringRedisTemplate.expire(userId, JWTUtil.TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
return ResponseResult.success(newToken);
}
}
HelloController.java
/**
* @author ChengJianSheng
* @date 2020-03-08
*/
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("/sayHello")
public String sayHello(String name) {
return "Hello, " + name;
}
@GetMapping("/sayHi")
public String sayHi(@RequestHeader("userId") String userId) {
return userId;
}
}
application.yml
server:
port: 8081
servlet:
context-path: /auth-server
spring:
application:
name: cjs-auth-service
redis:
host: 127.0.0.1
password: 123456
port: 6379
lettuce:
pool:
max-active: 10
max-idle: 5
min-idle: 5
max-wait: 5000
GatewayFilter和GlobalFilter都可以,这里用GlobalFilter
pom.xml
<?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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cms.example</groupId>
<artifactId>cjs-gateway-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cjs-gateway-example</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR1</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>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.0</version>
</dependency>
<dependency>
<groupId>com.cjs.example</groupId>
<artifactId>cjs-commons-jwt</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
AuthorizeFilter.java
/**
* @author ChengJianSheng
* @date 2020-03-08
*/
@Slf4j
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Value("${secretKey:123456}")
private String secretKey;
// @Autowired
// private StringRedisTemplate stringRedisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpResponse serverHttpResponse = exchange.getResponse();
String uri = serverHttpRequest.getURI().getPath();
// 检查白名单(配置)
if (uri.indexOf("/auth-server/login") >= 0) {
return chain.filter(exchange);
}
String token = serverHttpRequest.getHeaders().getFirst("token");
if (StringUtils.isBlank(token)) {
serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
return getVoidMono(serverHttpResponse, ResponseCodeEnum.TOKEN_MISSION);
}
//todo 检查Redis中是否有此Token
try {
JWTUtil.verifyToken(token, secretKey);
} catch (TokenAuthenticationException ex) {
return getVoidMono(serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
} catch (Exception ex) {
return getVoidMono(serverHttpResponse, ResponseCodeEnum.UNKNOWN_ERROR);
}
String userId = JWTUtil.getUserInfo(token);
ServerHttpRequest mutableReq = serverHttpRequest.mutate().header("userId", userId).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
private Mono<Void> getVoidMono(ServerHttpResponse serverHttpResponse, ResponseCodeEnum responseCodeEnum) {
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
ResponseResult responseResult = ResponseResult.error(responseCodeEnum.getCode(), responseCodeEnum.getMessage());
DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(JSON.toJSONString(responseResult).getBytes());
return serverHttpResponse.writeWith(Flux.just(dataBuffer));
}
@Override
public int getOrder() {
return -100;
}
}
application.yml
spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://localhost:8081/auth-server/
filters:
- MyLog=true
predicates:
- Path=/auth-server/**
这里我还自定义了一个日志收集过滤器
/**
* @author ChengJianSheng
* @date 2020-03-08
*/
@Component
public class MyLogGatewayFilterFactory extends AbstractGatewayFilterFactory<MyLogGatewayFilterFactory.Config> {
private static final Log log = LogFactory.getLog(MyLogGatewayFilterFactory.class);
private static final String MY_LOG_START_TIME = MyLogGatewayFilterFactory.class.getName() + "." + "startTime";
public MyLogGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("enabled");
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
if (!config.isEnabled()) {
return chain.filter(exchange);
}
exchange.getAttributes().put(MY_LOG_START_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(MY_LOG_START_TIME);
if (null != startTime) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
StringBuilder sb = new StringBuilder();
sb.append(serverHttpRequest.getURI().getRawPath());
sb.append(" : ");
sb.append(serverHttpRequest.getQueryParams());
sb.append(" : ");
sb.append(System.currentTimeMillis() - startTime);
sb.append("ms");
log.info(sb.toString());
}
}));
};
}
public static class Config {
/**
* 是否开启
*/
private boolean enabled;
public Config() {
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}
用Postman访问就能看到效果
http://localhost:8080/auth-server/hello/sayHi
http://localhost:8080/auth-server/hello/sayHello?name=aaa
@SpringBootApplication
public class DemogatewayApplication {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://httpbin.org"))
.route("host_route", r -> r.host("*.myhost.org")
.uri("http://httpbin.org"))
.route("rewrite_route", r -> r.host("*.rewrite.org")
.filters(f -> f.rewritePath("/foo/(?.*)" , "/${segment}"))
.uri("http://httpbin.org"))
.route("hystrix_route", r -> r.host("*.hystrix.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
.uri("http://httpbin.org"))
.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
.uri("http://httpbin.org"))
.route("limit_route", r -> r
.host("*.limited.org").and().path("/anything/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
.uri("http://httpbin.org"))
.build();
}
}
路由过滤器允许以某种方式修改输入的HTTP请求或输出的HTTP响应。路由过滤器适用于特定路由。Spring Cloud Gateway包括许多内置的GatewayFilter工厂。
AddRequestHeader GatewayFilter 采用name和value参数。
例如:下面的例子,对于所有匹配的请求,将在下游请求头中添加 X-Request-red:blue
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue
刚才说了,AddRequestHeader采用name和value作为参数。而URI中的变量可以用在value中,例如:
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- AddRequestHeader=X-Request-Red, Blue-{segment}
AddRequestParameter GatewayFilter 也是采用name和value参数
例如:下面的例子,对于所有匹配的请求,将会在下游请求的查询字符串中添加 red=blue
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=red, blue
同样,AddRequestParameter也支持在value中引用URI中的变量,例如:
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddRequestParameter=foo, bar-{segment}
AddResponseHeader GatewayFilter 依然采用name和value参数。不在赘述,如下:
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
filters:
- AddResponseHeader=X-Response-Red, Blue
DedupeResponseHeader GatewayFilter 采用一个name参数和一个可选的strategy参数。name可以包含以空格分隔的header名称列表。例如:下面的例子,如果在网关CORS逻辑和下游逻辑都将它们添加的情况下,这将删除Access-Control-Allow-Credentials和Access-Control-Allow-Origin响应头中的重复值。
spring:
cloud:
gateway:
routes:
- id: dedupe_response_header_route
uri: https://example.org
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
PrefixPath GatewayFilter 只有一个prefix参数。下面的例子,对于所有匹配的请求,将会在请求url上加上前缀/mypath,因此请求/hello在被转发后的url变成/mypath/hello
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- PrefixPath=/mypath
RequestRateLimiter GatewayFilter 用一个RateLimiter实现来决定当前请求是否被允许处理。如果不被允许,默认将返回一个HTTP 429状态,表示太多的请求。
这个过滤器采用一个可选的keyResolver参数。keyResolver是实现了KeyResolver接口的一个bean。在配置中,通过SpEL表达式引用它。例如,#{@myKeyResolver}是一个SpEL表达式,它是对名字叫myKeyResolver的bean的引用。KeyResolver默认的实现是PrincipalNameKeyResolver。
默认情况下,如果KeyResolver没有找到一个key,那么请求将会被拒绝。你可以调整这种行为,通过设置spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key (true or false) 和 spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code属性。
Redis基于 Token Bucket Algorithm (令牌桶算法)实现了一个RequestRateLimiter
redis-rate-limiter.replenishRate 属性指定一个用户每秒允许多少个请求,而没有任何丢弃的请求。这是令牌桶被填充的速率。
redis-rate-limiter.burstCapacity 属性指定用户在一秒钟内执行的最大请求数。这是令牌桶可以容纳的令牌数。将此值设置为零将阻止所有请求。
redis-rate-limiter.requestedTokens 属性指定一个请求要花费多少个令牌。这是每个请求从存储桶中获取的令牌数,默认为1。
通过将replenishRate和burstCapacity设置成相同的值可以实现稳定的速率。通过将burstCapacity设置为高于replenishRate,可以允许临时突发。 在这种情况下,速率限制器需要在两次突发之间保留一段时间(根据replenishRate),因为两个连续的突发将导致请求丢弃(HTTP 429-太多请求)。
通过将replenishRate设置为所需的请求数,将requestTokens设置为以秒为单位的时间跨度并将burstCapacity设置为replenishRate和requestedToken的乘积。可以达到1个请求的速率限制。 例如:设置replenishRate = 1,requestedTokens = 60和burstCapacity = 60将导致限制为每分钟1个请求。
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
KeyResolver
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
上面的例子,定义了每个用户每秒运行10个请求,令牌桶的容量是20,那么,下一秒将只剩下10个令牌可用。KeyResolver实现仅仅只是简单取请求参数中的user,当然在生产环境中不推荐这么做。
说白了,KeyResolver就是决定哪些请求属于同一个用户的。比如,header中userId相同的就认为是同一个用户的请求。
当然,你也可以自己实现一个RateLimiter,在配置的时候用SpEL表达式#{@myRateLimiter}去引用它。例如:
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
rate-limiter: "#{@myRateLimiter}"
key-resolver: "#{@userKeyResolver}"
RedirectTo GatewayFilter 有两个参数:status 和 url。status应该是300系列的。不解释,看示例:
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- RedirectTo=302, https://acme.org
spring:
cloud:
gateway:
routes:
- id: removerequestheader_route
uri: https://example.org
filters:
- RemoveRequestHeader=X-Request-Foo
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://example.org
predicates:
- Path=/foo/**
filters:
- RewritePath=/red(?/?.*), $\{segment}
为了添加一个过滤器,并将其应用到所有路由上,你可以使用spring.cloud.gateway.default-filters,这个属性值是一个过滤器列表
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- PrefixPath=/httpbin
GlobalFilter应用于所有路由
当一个请求请求与匹配某个路由时,过滤Web处理程序会将GlobalFilter的所有实例和GatewayFilter的所有特定于路由的实例添加到过滤器链中。该组合的过滤器链由org.springframework.core.Ordered接口排序,可以通过实现getOrder()方法进行设置。
由于Spring Cloud Gateway区分过滤器逻辑执行的“pre”和“post”阶段,因此,优先级最高的过滤器在“pre”阶段是第一个,在“post”阶段是最后一个。
@Bean
public GlobalFilter customFilter(){
return new CustomGlobalFilter();
}
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("custom global filter");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
}
https://en.wikipedia.org/wiki/Token_bucket
令牌桶是在分组交换计算机网络和电信网络中使用的算法。它可以用来检查数据包形式的数据传输是否符合定义的带宽和突发性限制(对流量不均匀性或变化的度量)。
令牌桶算法就好比是一个的固定容量桶,通常以固定速率向其中添加令牌。一个令牌通常代表一个字节。当要检查数据包是否符合定义的限制时,将检查令牌桶以查看其当时是否包含足够的令牌。如果有足够数量的令牌,并假设令牌以字节为单位,那么,与数据包字节数量等效数量的令牌将被删除,并且该数据包可以通过继续传输。如果令牌桶中的令牌数量不够,则数据包不符合要求,并且令牌桶的令牌数量不会变化。不合格的数据包可以有多种处理方式:
(PS:这句话的意思是说,想象有一个桶,以固定速率向桶中添加令牌。假设一个令牌等效于一个字节,当一个数据包到达时,假设这个数据包的大小是n字节,如果桶中有足够多的令牌,即桶中令牌的数量大于n,则该数据可以通过,并且桶中要删除n个令牌。如果桶中令牌数不够,则根据情况该数据包可能直接被丢弃,也可能一直等待直到令牌足够,也可能继续传输,但被标记为不合格。还是不够通俗,这样,如果把令牌桶想象成一个水桶的话,令牌想象成水滴的话,那么这个过程就变成了以恒定速率向水桶中滴水,当有人想打一碗水时,如果这个人的碗很小,只能装30滴水,而水桶中水滴数量超过30,那么这个人就可以打一碗水,然后就走了,相应的,水桶中的水在这个人打完以后自然就少了30滴。过了一会儿,又有一个人来打水,他拿的碗比较大,一次能装100滴水,这时候桶里的水不够,这个时候他可能就走了,或者在这儿等着,等到桶中积累了100滴的时候再打。哈哈哈,就是酱紫,不知道大家见过水车没有……)
令牌桶算法可以简单地这样理解:
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.2.RELEASE/reference/html/#gatewayfilter-factories
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.2.RELEASE/reference/html/#gateway-request-predicates-factories
https://mp.weixin.qq.com/
https://en.wikipedia.org/wiki/Token_bucket