目录

  • 前言
  • 1. 生成Token
    • 1.1. Token生成与校验工具类
    • 1.2. 生成token
  • 2. 校验Token
  • 3. Spring Cloud Gateway
    • 3.1. GatewayFilter Factories
      • 3.1.1. AddRequestHeader GatewayFilter Factory
      • 3.1.2. AddRequestParameter GatewayFilter Factory
      • 3.1.3. AddResponseHeader GatewayFilter Factory
      • 3.1.4. DedupeResponseHeader GatewayFilter Factory
      • 3.1.5. PrefixPath GatewayFilter Factory
      • 3.1.6. RequestRateLimiter GatewayFilter Factory
      • 3.1.7. RedirectTo GatewayFilter Factory
      • 3.1.8. RemoveRequestHeader GatewayFilter Factory
      • 3.1.9. RewritePath GatewayFilter Factory
      • 3.1.10. Default Filters
    • 3.2. Global Filters
      • 3.2.1. GlobalFilter与GatewayFilter组合的顺序
      • 补充:(Token Bucket)令牌桶
  • 4. Docs

前言

在我看来,在某些场景下,网关就像是一个公共方法,把项目中的都要用到的一些功能提出来,抽象成一个服务。比如,我们可以在业务网关上做日志收集、Token校验等等,当然这么理解很狭隘,因为网关的能力远不止如此,但是不妨碍我们更好地理解它。下面的例子演示了,如何在网关校验Token,并提取用户信息放到Header中传给下游业务系统。

1. 生成Token

用户登录成功以后,生成token,此后的所有请求都带着token。网关负责校验token,并将用户信息放入请求Header,以便下游系统可以方便的获取用户信息。



为了方便演示,本例中涉及三个工程

公共项目:cjs-commons-jwt

认证服务:cjs-auth-service

网关服务:cjs-gateway-example

1.1. Token生成与校验工具类

因为生成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;}
}

1.2. 生成token

这一部分在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;@Autowiredprivate 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是1001String userId = "1001";if ("hello".equals(username) && "world".equals(password)) {//  生成TokenString token = JWTUtil.generateToken(userId, secretKey);//  生成刷新TokenString 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());}//  生成新tokenString 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: 8081servlet:context-path: /auth-serverspring:application:name: cjs-auth-serviceredis:host: 127.0.0.1password: 123456port: 6379lettuce:pool:max-active: 10max-idle: 5min-idle: 5max-wait: 5000

2. 校验Token

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;@Overridepublic 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中是否有此Tokentry {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));}@Overridepublic int getOrder() {return -100;}
}

application.yml

spring:cloud:gateway:routes:- id: path_routeuri: http://localhost:8081/auth-server/filters:- MyLog=truepredicates:- 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);}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("enabled");}@Overridepublic 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


3. Spring Cloud Gateway

@SpringBootApplication
public class DemogatewayApplication {@Beanpublic 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>.*)", "/${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();}
}

3.1. GatewayFilter Factories

路由过滤器允许以某种方式修改输入的HTTP请求或输出的HTTP响应。路由过滤器适用于特定路由。Spring Cloud Gateway包括许多内置的GatewayFilter工厂。

3.1.1. AddRequestHeader GatewayFilter Factory

AddRequestHeader GatewayFilter 采用name和value参数。

例如:下面的例子,对于所有匹配的请求,将在下游请求头中添加 X-Request-red:blue

spring:
cloud:gateway:routes:- id: add_request_header_routeuri: https://example.orgfilters:- AddRequestHeader=X-Request-red, blue

刚才说了,AddRequestHeader采用name和value作为参数。而URI中的变量可以用在value中,例如:

spring:cloud:gateway:routes:- id: add_request_header_routeuri: https://example.orgpredicates:- Path=/red/{segment}filters:- AddRequestHeader=X-Request-Red, Blue-{segment}

3.1.2. AddRequestParameter GatewayFilter Factory

AddRequestParameter GatewayFilter 也是采用name和value参数

例如:下面的例子,对于所有匹配的请求,将会在下游请求的查询字符串中添加 red=blue

spring:cloud:gateway:routes:- id: add_request_parameter_routeuri: https://example.orgfilters:- AddRequestParameter=red, blue

同样,AddRequestParameter也支持在value中引用URI中的变量,例如:

spring:cloud:gateway:routes:- id: add_request_parameter_routeuri: https://example.orgpredicates:- Host: {segment}.myhost.orgfilters:- AddRequestParameter=foo, bar-{segment}

3.1.3. AddResponseHeader GatewayFilter Factory

AddResponseHeader GatewayFilter 依然采用name和value参数。不在赘述,如下:

spring:cloud:gateway:routes:- id: add_response_header_routeuri: https://example.orgfilters:- AddResponseHeader=X-Response-Red, Blue

3.1.4. DedupeResponseHeader GatewayFilter Factory

DedupeResponseHeader GatewayFilter 采用一个name参数和一个可选的strategy参数。name可以包含以空格分隔的header名称列表。例如:下面的例子,如果在网关CORS逻辑和下游逻辑都将它们添加的情况下,这将删除Access-Control-Allow-Credentials和Access-Control-Allow-Origin响应头中的重复值。

spring:cloud:gateway:routes:- id: dedupe_response_header_routeuri: https://example.orgfilters:- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

3.1.5. PrefixPath GatewayFilter Factory

PrefixPath GatewayFilter 只有一个prefix参数。下面的例子,对于所有匹配的请求,将会在请求url上加上前缀/mypath,因此请求/hello在被转发后的url变成/mypath/hello

spring:
cloud:gateway:routes:- id: prefixpath_routeuri: https://example.orgfilters:- PrefixPath=/mypath

3.1.6. RequestRateLimiter GatewayFilter Factory

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_routeuri: https://example.orgfilters:- name: RequestRateLimiterargs:redis-rate-limiter.replenishRate: 10redis-rate-limiter.burstCapacity: 20redis-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_routeuri: https://example.orgfilters:- name: RequestRateLimiterargs:rate-limiter: "#{@myRateLimiter}"key-resolver: "#{@userKeyResolver}"

3.1.7. RedirectTo GatewayFilter Factory

RedirectTo GatewayFilter 有两个参数:status 和 url。status应该是300系列的。不解释,看示例:

spring:cloud:gateway:routes:- id: prefixpath_routeuri: https://example.orgfilters:- RedirectTo=302, https://acme.org

3.1.8. RemoveRequestHeader GatewayFilter Factory

spring:cloud:gateway:routes:- id: removerequestheader_routeuri: https://example.orgfilters:- RemoveRequestHeader=X-Request-Foo

3.1.9. RewritePath GatewayFilter Factory

spring:cloud:gateway:routes:- id: rewritepath_routeuri: https://example.orgpredicates:- Path=/foo/**filters:- RewritePath=/red(?<segment>/?.*), $\{segment}

3.1.10. Default Filters

为了添加一个过滤器,并将其应用到所有路由上,你可以使用spring.cloud.gateway.default-filters,这个属性值是一个过滤器列表

spring:cloud:gateway:default-filters:- AddResponseHeader=X-Response-Default-Red, Default-Blue- PrefixPath=/httpbin

3.2. Global Filters

GlobalFilter应用于所有路由

3.2.1. GlobalFilter与GatewayFilter组合的顺序

当一个请求请求与匹配某个路由时,过滤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 {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {log.info("custom global filter");return chain.filter(exchange);}@Overridepublic int getOrder() {return -1;}
}

补充:(Token Bucket)令牌桶

https://en.wikipedia.org/wiki/Token_bucket

令牌桶是在分组交换计算机网络和电信网络中使用的算法。它可以用来检查数据包形式的数据传输是否符合定义的带宽和突发性限制(对流量不均匀性或变化的度量)。

令牌桶算法就好比是一个的固定容量桶,通常以固定速率向其中添加令牌。一个令牌通常代表一个字节。当要检查数据包是否符合定义的限制时,将检查令牌桶以查看其当时是否包含足够的令牌。如果有足够数量的令牌,并假设令牌以字节为单位,那么,与数据包字节数量等效数量的令牌将被删除,并且该数据包可以通过继续传输。如果令牌桶中的令牌数量不够,则数据包不符合要求,并且令牌桶的令牌数量不会变化。不合格的数据包可以有多种处理方式:

  • 它们可能会被丢弃
  • 当桶中积累了足够的令牌时,可以将它们加入队列进行后续传输
  • 它们可以被传输,但被标记为不符合,如果网络负载过高,可能随后被丢弃

(PS:这句话的意思是说,想象有一个桶,以固定速率向桶中添加令牌。假设一个令牌等效于一个字节,当一个数据包到达时,假设这个数据包的大小是n字节,如果桶中有足够多的令牌,即桶中令牌的数量大于n,则该数据可以通过,并且桶中要删除n个令牌。如果桶中令牌数不够,则根据情况该数据包可能直接被丢弃,也可能一直等待直到令牌足够,也可能继续传输,但被标记为不合格。还是不够通俗,这样,如果把令牌桶想象成一个水桶的话,令牌想象成水滴的话,那么这个过程就变成了以恒定速率向水桶中滴水,当有人想打一碗水时,如果这个人的碗很小,只能装30滴水,而水桶中水滴数量超过30,那么这个人就可以打一碗水,然后就走了,相应的,水桶中的水在这个人打完以后自然就少了30滴。过了一会儿,又有一个人来打水,他拿的碗比较大,一次能装100滴水,这时候桶里的水不够,这个时候他可能就走了,或者在这儿等着,等到桶中积累了100滴的时候再打。哈哈哈,就是酱紫,不知道大家见过水车没有……)

令牌桶算法可以简单地这样理解:

  • 每 1/r 秒有一个令牌被添加到令牌桶
  • 令牌桶最多可以容纳 b 个令牌。当一个令牌到达时,令牌桶已经满了,那么它将会被丢弃。
  • 当一个 n 字节大小的数据包到达时: 如果令牌桶中至少有n个令牌,则从令牌桶中删除n个令牌,并将数据包发送到网络。如果可用的令牌少于n个,则不会从令牌桶中删除任何令牌,并且将数据包视为不合格。

4. Docs

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

Spring Cloud Gateway 实现Token校验相关推荐

  1. csrf token invalid什么意思_Spring Cloud Gateway 实现Token校验

    在我看来,在某些场景下,网关就像是一个公共方法,把项目中的都要用到的一些功能提出来,抽象成一个服务.比如,我们可以在业务网关上做日志收集.Token校验等等,当然这么理解很狭隘,因为网关的能力远不止如 ...

  2. Spring Cloud Gateway 使用 Token 验证

    引入依赖 <dependencyManagement><dependencies><dependency><groupId>org.springfram ...

  3. Spring Cloud Gateway实现网关统一鉴权,网关统一Token认证

    需求背景 在微服务的场景下,采用了Spring Cloud Oauth2进行token的管理,实现认证和授权,在这下背景下,有两种解决方案: 网关统一鉴权 此模式适用于网关下的所有模式都是通过一种模式 ...

  4. SpringCloud 2020版本教程2:使用spring cloud gateway作为服务网关

    点击关注公众号,Java干货及时送达 Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关.网关作为流量的,在微服务系统中有着非常作用,网关常见 ...

  5. spring cloud gateway之filter篇

    点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 在上一篇文章详细的介绍了Gateway的Predict,Predict决定了请求由哪一个路由处理, ...

  6. Spring Cloud Gateway 入门

    认识 Spring Cloud Gateway Spring Cloud Gateway 是一款基于 Spring 5,Project Reactor 以及 Spring Boot 2 构建的 API ...

  7. Spring Cloud Gateway(过滤器)

    在上一篇文章中,我们了解了 Spring Cloud Gateway 作为网关所具备的基础功能:路由.本篇我们将关注它的另一个功能:过滤器. Spring Cloud Gateway 已经内置了很多实 ...

  8. 微服务接入oauth2_微服务权限终极解决方案,Spring Cloud Gateway+Oauth2实现统一认证和鉴权!...

    最近发现了一个很好的微服务权限解决方案,可以通过认证服务进行统一认证,然后通过网关来统一校验认证和鉴权.此方案为目前最新方案,仅支持Spring Boot 2.2.0.Spring Cloud Hox ...

  9. Spring Cloud —— Gateway 服务网关

    导航 一.什么是服务网关 二.业界常见网关组件 三.Spring Cloud Gateway 四.Gateway 快速入门 4.1 创建 gateway 服务 4.2 添加 gateway 依赖和 n ...

最新文章

  1. 深度学习目标检测详细解析以及Mask R-CNN示例
  2. 【ApsaraSRE专题推荐--1期】Sedawk笔记系列
  3. Linux任务计划、周期性任务执行
  4. js 中 document.createEvent的用法-转载
  5. [资源分享] 推荐两本电子书
  6. jap sql 保存_【hibernate spring data jpa】执行了save()方法 sql语句也执行了,但是数据并未插入数据库中...
  7. CCS 3.3中统计程序运行的时间
  8. java web权限设计_java web简单权限管理设计
  9. 怎样获得正确的LINUX用户的文档音乐视频等目录?
  10. 制作VOC格式的数据集
  11. 程序员代码面试指南 IT名企算法与数据结构题目最优解.pdf
  12. 本工具仅仅交流之用,把黑群晖洗白用,如果对此感兴趣请支持正版,请勿用于违法,作者不承担法律和相关连带责任,工具内有详细sn算号器,可供使用还有教程
  13. spss统计分析基础教程 SPSS统计分析从入门到精通光盘
  14. 【ICEPAK】手把手教你热仿真
  15. 基于crontab的服务器恶意程序
  16. DevicePolicyManagerService之DeviceOwner和ProfileOwner
  17. Caused by: java.util.concurrent.ExecutionException: java.util.concurrent.ExecutionException: com.and
  18. Creo4.0安装与VS2015环境下的开发配置
  19. Maven中的依赖冲突
  20. Python这么强大, 怎样快速学习?

热门文章

  1. 人工智能:知识图谱实战
  2. HTML页面唤醒微信
  3. [软件分享]DVD Decrypter
  4. Windows 11 Manager v1.0.6 专业Win11系统优化管理工具便携版
  5. Win10必备系统优化软件:Windows 10 Manager 3.1.1
  6. 透明图片怎么发给别人_坦荡书法图片大全下载-抖音坦荡书法图片大全高清 v1.0...
  7. 2022年这样高颜值高内涵的智慧工地系统,你觉得需要多少一套
  8. iShowU Studio for Mac(高清录屏工具)
  9. 3.SpringMVC详解
  10. 2020年中国研究生数学建模竞赛A题(华为公司命题)——ASIC芯片上的载波恢复DSP算法设计与实践