connect time out 获取token失败_Spring Cloud Gateway 实现Token校验

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

1. 生成Token

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

fdc84e5f3e24ca4148364be6d4131097.png
4a8789a115d674b657e8fd2b346baabd.png

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

公共项目:cjs-commons-jwt

认证服务:cjs-auth-service

网关服务:cjs-gateway-example

1.1. Token生成与校验工具类

因为生成token在认证服务中,token校验在网关服务中,因此,我把这一部分写在了公共项目cjs-commons-jwt中

pom.xml

 1 <?xml version="1.0" encoding="UTF-8"?> 2  3  5     4.0.0 6  7     com.cjs.example 8     cjs-commons-jwt 9     1.0-SNAPSHOT10 11     12         UTF-813         1.814         1.815     16 17     18         19             com.auth020             java-jwt21             3.10.022         23         24             org.apache.commons25             commons-lang326             3.927         28         29             com.alibaba30             fastjson31             1.2.6632         33     34 35 

JWTUtil.java

 1 package com.cjs.example.utils; 2  3 import com.auth0.jwt.JWT; 4 import com.auth0.jwt.JWTVerifier; 5 import com.auth0.jwt.algorithms.Algorithm; 6 import com.auth0.jwt.exceptions.JWTDecodeException; 7 import com.auth0.jwt.exceptions.SignatureVerificationException; 8 import com.auth0.jwt.exceptions.TokenExpiredException; 9 import com.auth0.jwt.interfaces.DecodedJWT;10 import com.cjs.example.enums.ResponseCodeEnum;11 import com.cjs.example.exception.TokenAuthenticationException;12 13 import java.util.Date;14 15 /**16  * @author ChengJianSheng17  * @date 2020-03-0818  */19 public class JWTUtil {20 21     public static final long TOKEN_EXPIRE_TIME = 7200 * 1000;22     private static final String ISSUER = "cheng";23 24     /**25      * 生成Token26      * @param username 用户标识(不一定是用户名,有可能是用户ID或者手机号什么的)27      * @param secretKey28      * @return29      */30     public static String generateToken(String username, String secretKey) {31         Algorithm algorithm = Algorithm.HMAC256(secretKey);32         Date now = new Date();33         Date expireTime = new Date(now.getTime() + TOKEN_EXPIRE_TIME);34 35         String token = JWT.create()36                 .withIssuer(ISSUER)37                 .withIssuedAt(now)38                 .withExpiresAt(expireTime)39                 .withClaim("username", username)40                 .sign(algorithm);41 42         return token;43     }44 45     /**46      * 校验Token47      * @param token48      * @param secretKey49      * @return50      */51     public static void verifyToken(String token, String secretKey) {52         try {53             Algorithm algorithm = Algorithm.HMAC256(secretKey);54             JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(ISSUER).build();55             jwtVerifier.verify(token);56         } catch (JWTDecodeException jwtDecodeException) {57             throw new TokenAuthenticationException(ResponseCodeEnum.TOKEN_INVALID.getCode(), ResponseCodeEnum.TOKEN_INVALID.getMessage());58         } catch (SignatureVerificationException signatureVerificationException) {59             throw new TokenAuthenticationException(ResponseCodeEnum.TOKEN_SIGNATURE_INVALID.getCode(), ResponseCodeEnum.TOKEN_SIGNATURE_INVALID.getMessage());60         } catch (TokenExpiredException tokenExpiredException) {61             throw new TokenAuthenticationException(ResponseCodeEnum.TOKEN_EXPIRED.getCode(), ResponseCodeEnum.TOKEN_INVALID.getMessage());62         } catch (Exception ex) {63             throw new TokenAuthenticationException(ResponseCodeEnum.UNKNOWN_ERROR.getCode(), ResponseCodeEnum.UNKNOWN_ERROR.getMessage());64         }65     }66 67     /**68      * 从Token中提取用户信息69      * @param token70      * @return71      */72     public static String getUserInfo(String token) {73         DecodedJWT decodedJWT = JWT.decode(token);74         String username = decodedJWT.getClaim("username").asString();75         return username;76     }77 78 }

ResponseCodeEnum.java

 1 package com.cjs.example.enums; 2  3 /** 4  * @author ChengJianSheng 5  * @date 2020-03-08 6  */ 7 public enum ResponseCodeEnum { 8  9     SUCCESS(0, "成功"),10     FAIL(-1, "失败"),11     LOGIN_ERROR(1000, "用户名或密码错误"),12     UNKNOWN_ERROR(2000, "未知错误"),13     PARAMETER_ILLEGAL(2001, "参数不合法"),14     TOKEN_INVALID(2002, "无效的Token"),15     TOKEN_SIGNATURE_INVALID(2003, "无效的签名"),16     TOKEN_EXPIRED(2004, "token已过期"),17     TOKEN_MISSION(2005, "token缺失"),18     REFRESH_TOKEN_INVALID(2006, "刷新Token无效");19 20 21     private int code;22 23     private String message;24 25     ResponseCodeEnum(int code, String message) {26         this.code = code;27         this.message = message;28     }29 30     public int getCode() {31         return code;32     }33 34     public String getMessage() {35         return message;36     }37 38 }

ResponseResult.java

 1 package com.cjs.example; 2  3 import com.cjs.example.enums.ResponseCodeEnum; 4  5 /** 6  * @author ChengJianSheng 7  * @date 2020-03-08 8  */ 9 public class ResponseResult {10 11     private int code = 0;12 13     private String msg;14 15     private T data;16 17     public ResponseResult(int code, String msg) {18         this.code = code;19         this.msg = msg;20     }21 22     public ResponseResult(int code, String msg, T data) {23         this.code = code;24         this.msg = msg;25         this.data = data;26     }27 28     public static ResponseResult success() {29         return new ResponseResult(ResponseCodeEnum.SUCCESS.getCode(), ResponseCodeEnum.SUCCESS.getMessage());30     }31 32     public static  ResponseResult success(T data) {33         return new ResponseResult(ResponseCodeEnum.SUCCESS.getCode(), ResponseCodeEnum.SUCCESS.getMessage(), data);34     }35 36     public static ResponseResult error(int code, String msg) {37         return new ResponseResult(code, msg);38     }39 40     public static  ResponseResult error(int code, String msg, T data) {41         return new ResponseResult(code, msg, data);42     }43 44     public boolean isSuccess() {45         return code == 0;46     }47 48     public int getCode() {49         return code;50     }51 52     public void setCode(int code) {53         this.code = code;54     }55 56     public String getMsg() {57         return msg;58     }59 60     public void setMsg(String msg) {61         this.msg = msg;62     }63 64     public T getData() {65         return data;66     }67 68     public void setData(T data) {69         this.data = data;70     }71 }

1.2. 生成token

这一部分在cjs-auth-service中

pom.xml

 1 <?xml version="1.0" encoding="UTF-8"?> 2  4     4.0.0 5      6         org.springframework.boot 7         spring-boot-starter-parent 8         2.2.5.RELEASE 9         10     11     com.cjs.example12     cjs-auth-service13     0.0.1-SNAPSHOT14     cjs-auth-service15 16     17         1.818     19 20     21         22             org.springframework.boot23             spring-boot-starter-data-redis24         25         26             org.springframework.boot27             spring-boot-starter-web28         29 30         31             org.apache.commons32             commons-lang333             3.934         35         36             commons-codec37             commons-codec38             1.1439         40         41             org.apache.commons42             commons-pool243             2.8.044         45 46         47             com.cjs.example48             cjs-commons-jwt49             1.0-SNAPSHOT50         51 52         53             org.projectlombok54             lombok55             true56         57     58 59     60         61             62                 org.springframework.boot63                 spring-boot-maven-plugin64             65         66     67 68 

LoginController.java

  1 package com.cjs.example.controller;  2   3 import com.cjs.example.ResponseResult;  4 import com.cjs.example.domain.LoginRequest;  5 import com.cjs.example.domain.LoginResponse;  6 import com.cjs.example.domain.RefreshRequest;  7 import com.cjs.example.enums.ResponseCodeEnum;  8 import com.cjs.example.utils.JWTUtil;  9 import org.apache.commons.lang3.StringUtils; 10 import org.apache.tomcat.util.security.MD5Encoder; 11 import org.springframework.beans.factory.annotation.Autowired; 12 import org.springframework.beans.factory.annotation.Value; 13 import org.springframework.data.redis.core.HashOperations; 14 import org.springframework.data.redis.core.StringRedisTemplate; 15 import org.springframework.validation.BindingResult; 16 import org.springframework.validation.annotation.Validated; 17 import org.springframework.web.bind.annotation.*; 18  19 import java.util.UUID; 20 import java.util.concurrent.TimeUnit; 21  22 /** 23  * @author ChengJianSheng 24  * @date 2020-03-08 25  */ 26 @RestController 27 public class LoginController { 28  29     /** 30      * Apollo 或 Nacos 31      */ 32     @Value("${secretKey:123456}") 33     private String secretKey; 34  35     @Autowired 36     private StringRedisTemplate stringRedisTemplate; 37  38     /** 39      * 登录 40      */ 41     @PostMapping("/login") 42     public ResponseResult login(@RequestBody @Validated LoginRequest request, BindingResult bindingResult) { 43         if (bindingResult.hasErrors()) { 44             return ResponseResult.error(ResponseCodeEnum.PARAMETER_ILLEGAL.getCode(), ResponseCodeEnum.PARAMETER_ILLEGAL.getMessage()); 45         } 46  47         String username = request.getUsername(); 48         String password = request.getPassword(); 49         //  假设查询到用户ID是1001 50         String userId = "1001"; 51         if ("hello".equals(username) && "world".equals(password)) { 52             //  生成Token 53             String token = JWTUtil.generateToken(userId, secretKey); 54  55             //  生成刷新Token 56             String refreshToken = UUID.randomUUID().toString().replace("-", ""); 57  58             //  放入缓存 59             HashOperations hashOperations = stringRedisTemplate.opsForHash(); 60 //            hashOperations.put(refreshToken, "token", token); 61 //            hashOperations.put(refreshToken, "user", username); 62 //            stringRedisTemplate.expire(refreshToken, JWTUtil.TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS); 63  64             /** 65              * 如果可以允许用户退出后token如果在有效期内仍然可以使用的话,那么就不需要存Redis 66              * 因为,token要跟用户做关联的话,就必须得每次都带一个用户标识, 67              * 那么校验token实际上就变成了校验token和用户标识的关联关系是否正确,且token是否有效 68              */ 69  70 //            String key = MD5Encoder.encode(userId.getBytes()); 71  72             String key = userId; 73             hashOperations.put(key, "token", token); 74             hashOperations.put(key, "refreshToken", refreshToken); 75             stringRedisTemplate.expire(key, JWTUtil.TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS); 76  77             LoginResponse loginResponse = new LoginResponse(); 78             loginResponse.setToken(token); 79             loginResponse.setRefreshToken(refreshToken); 80             loginResponse.setUsername(userId); 81  82             return ResponseResult.success(loginResponse); 83         } 84  85         return ResponseResult.error(ResponseCodeEnum.LOGIN_ERROR.getCode(), ResponseCodeEnum.LOGIN_ERROR.getMessage()); 86     } 87  88     /** 89      * 退出 90      */ 91     @GetMapping("/logout") 92     public ResponseResult logout(@RequestParam("userId") String userId) { 93         HashOperations hashOperations = stringRedisTemplate.opsForHash(); 94         String key = userId; 95         hashOperations.delete(key); 96         return ResponseResult.success(); 97     } 98  99     /**100      * 刷新Token101      */102     @PostMapping("/refreshToken")103     public ResponseResult refreshToken(@RequestBody @Validated RefreshRequest request, BindingResult bindingResult) {104         String userId = request.getUserId();105         String refreshToken = request.getRefreshToken();106         HashOperations hashOperations = stringRedisTemplate.opsForHash();107         String key = userId;108         String originalRefreshToken = hashOperations.get(key, "refreshToken");109         if (StringUtils.isBlank(originalRefreshToken) || !originalRefreshToken.equals(refreshToken)) {110             return ResponseResult.error(ResponseCodeEnum.REFRESH_TOKEN_INVALID.getCode(), ResponseCodeEnum.REFRESH_TOKEN_INVALID.getMessage());111         }112 113         //  生成新token114         String newToken = JWTUtil.generateToken(userId, secretKey);115         hashOperations.put(key, "token", newToken);116         stringRedisTemplate.expire(userId, JWTUtil.TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);117 118         return ResponseResult.success(newToken);119     }120 }

HelloController.java

 1 package com.cjs.example.controller; 2  3 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.RequestHeader; 5 import org.springframework.web.bind.annotation.RequestMapping; 6 import org.springframework.web.bind.annotation.RestController; 7  8 /** 9  * @author ChengJianSheng10  * @date 2020-03-0811  */12 @RestController13 @RequestMapping("/hello")14 public class HelloController {15 16     @GetMapping("/sayHello")17     public String sayHello(String name) {18         return "Hello, " + name;19     }20 21     @GetMapping("/sayHi")22     public String sayHi(@RequestHeader("userId") String userId) {23         return userId;24     }25 26 }

application.yml

 1 server: 2   port: 8081 3   servlet: 4     context-path: /auth-server 5 spring: 6   application: 7     name: cjs-auth-service 8   redis: 9     host: 127.0.0.110     password: 12345611     port: 637912     lettuce:13       pool:14         max-active: 1015         max-idle: 516         min-idle: 517         max-wait: 5000

2. 校验Token

GatewayFilter和GlobalFilter都可以,这里用GlobalFilter

pom.xml

 1 <?xml version="1.0" encoding="UTF-8"?> 2  4     4.0.0 5      6         org.springframework.boot 7         spring-boot-starter-parent 8         2.2.5.RELEASE 9         10     11     com.cms.example12     cjs-gateway-example13     0.0.1-SNAPSHOT14     cjs-gateway-example15 16     17         1.818         Hoxton.SR119     20 21     22         23             org.springframework.boot24             spring-boot-starter-data-redis-reactive25         26         27             org.springframework.cloud28             spring-cloud-starter-gateway29         30         31             com.auth032             java-jwt33             3.10.034         35         36             com.cjs.example37             cjs-commons-jwt38             1.0-SNAPSHOT39         40 41 42         43             org.projectlombok44             lombok45             true46         47     48 49     50         51             52                 org.springframework.cloud53                 spring-cloud-dependencies54                 ${spring-cloud.version}55                 pom56                 import57             58         59     60 61     62         63             64                 org.springframework.boot65                 spring-boot-maven-plugin66             67         68     69 70 

AuthorizeFilter.java

 1 package com.cms.example.filter; 2  3 import com.alibaba.fastjson.JSON; 4 import com.cjs.example.ResponseResult; 5 import com.cjs.example.enums.ResponseCodeEnum; 6 import com.cjs.example.exception.TokenAuthenticationException; 7 import com.cjs.example.utils.JWTUtil; 8 import lombok.extern.slf4j.Slf4j; 9 import org.apache.commons.lang3.StringUtils;10 import org.springframework.beans.factory.annotation.Autowired;11 import org.springframework.beans.factory.annotation.Value;12 import org.springframework.cloud.gateway.filter.GatewayFilterChain;13 import org.springframework.cloud.gateway.filter.GlobalFilter;14 import org.springframework.core.Ordered;15 import org.springframework.core.io.buffer.DataBuffer;16 import org.springframework.data.redis.core.StringRedisTemplate;17 import org.springframework.http.HttpStatus;18 import org.springframework.http.server.reactive.ServerHttpRequest;19 import org.springframework.http.server.reactive.ServerHttpResponse;20 import org.springframework.stereotype.Component;21 import org.springframework.web.server.ServerWebExchange;22 import reactor.core.publisher.Flux;23 import reactor.core.publisher.Mono;24 25 /**26  * @author ChengJianSheng27  * @date 2020-03-0828  */29 @Slf4j30 @Component31 public class AuthorizeFilter implements GlobalFilter, Ordered {32 33     @Value("${secretKey:123456}")34     private String secretKey;35 36 //    @Autowired37 //    private StringRedisTemplate stringRedisTemplate;38 39     @Override40     public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {41         ServerHttpRequest serverHttpRequest = exchange.getRequest();42         ServerHttpResponse serverHttpResponse = exchange.getResponse();43         String uri = serverHttpRequest.getURI().getPath();44 45         //  检查白名单(配置)46         if (uri.indexOf("/auth-server/login") >= 0) {47             return chain.filter(exchange);48         }49 50         String token = serverHttpRequest.getHeaders().getFirst("token");51         if (StringUtils.isBlank(token)) {52             serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);53             return getVoidMono(serverHttpResponse, ResponseCodeEnum.TOKEN_MISSION);54         }55 56         //todo 检查Redis中是否有此Token57 58         try {59             JWTUtil.verifyToken(token, secretKey);60         } catch (TokenAuthenticationException ex) {61             return getVoidMono(serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);62         } catch (Exception ex) {63             return getVoidMono(serverHttpResponse, ResponseCodeEnum.UNKNOWN_ERROR);64         }65 66         String userId = JWTUtil.getUserInfo(token);67 68         ServerHttpRequest mutableReq = serverHttpRequest.mutate().header("userId", userId).build();69         ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();70 71         return chain.filter(mutableExchange);72     }73 74     private Mono getVoidMono(ServerHttpResponse serverHttpResponse, ResponseCodeEnum responseCodeEnum) {75         serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");76         ResponseResult responseResult = ResponseResult.error(responseCodeEnum.getCode(), responseCodeEnum.getMessage());77         DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(JSON.toJSONString(responseResult).getBytes());78         return serverHttpResponse.writeWith(Flux.just(dataBuffer));79     }80 81     @Override82     public int getOrder() {83         return -100;84     }85 }

application.yml

 1 spring: 2   cloud: 3     gateway: 4       routes: 5         - id: path_route 6           uri: http://localhost:8081/auth-server/ 7           filters: 8             - MyLog=true 9           predicates:10             - Path=/auth-server/** 

这里我还自定义了一个日志收集过滤器

 1 package com.cms.example.filter; 2  3 import org.apache.commons.logging.Log; 4 import org.apache.commons.logging.LogFactory; 5 import org.springframework.cloud.gateway.filter.GatewayFilter; 6 import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; 7 import org.springframework.http.server.reactive.ServerHttpRequest; 8 import org.springframework.stereotype.Component; 9 import reactor.core.publisher.Mono;10 11 import java.util.Arrays;12 import java.util.List;13 14 /**15  * @author ChengJianSheng16  * @date 2020-03-0817  */18 @Component19 public class MyLogGatewayFilterFactory extends AbstractGatewayFilterFactory {20 21     private static final Log log = LogFactory.getLog(MyLogGatewayFilterFactory.class);22     private static final String MY_LOG_START_TIME = MyLogGatewayFilterFactory.class.getName() + "." + "startTime";23 24     public MyLogGatewayFilterFactory() {25         super(Config.class);26     }27 28     @Override29     public List shortcutFieldOrder() {30         return Arrays.asList("enabled");31     }32 33     @Override34     public GatewayFilter apply(Config config) {35         return (exchange, chain) -> {36             if (!config.isEnabled()) {37                 return chain.filter(exchange);38             }39             exchange.getAttributes().put(MY_LOG_START_TIME, System.currentTimeMillis());40             return chain.filter(exchange).then(Mono.fromRunnable(() -> {41                 Long startTime = exchange.getAttribute(MY_LOG_START_TIME);42                 if (null != startTime) {43                     ServerHttpRequest serverHttpRequest = exchange.getRequest();44                     StringBuilder sb = new StringBuilder();45                     sb.append(serverHttpRequest.getURI().getRawPath());46                     sb.append(" : ");47                     sb.append(serverHttpRequest.getQueryParams());48                     sb.append(" : ");49                     sb.append(System.currentTimeMillis() - startTime);50                     sb.append("ms");51                     log.info(sb.toString());52                 }53             }));54         };55     }56 57     public static class Config {58         /**59          * 是否开启60          */61         private boolean enabled;62 63         public Config() {64         }65 66         public boolean isEnabled() {67             return enabled;68         }69 70         public void setEnabled(boolean enabled) {71             this.enabled = enabled;72         }73     }74 } 

用Postman访问就能看到效果

http://localhost:8080/auth-server/hello/sayHihttp://localhost:8080/auth-server/hello/sayHello?name=aaa

1762d2e29c991b01bccaacb516857a70.png
1189f5c6422a0622753a400930db6a3d.png

3. Spring Cloud Gateway

 1 @SpringBootApplication 2 public class DemogatewayApplication { 3     @Bean 4     public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { 5         return builder.routes() 6             .route("path_route", r -> r.path("/get") 7                 .uri("http://httpbin.org")) 8             .route("host_route", r -> r.host("*.myhost.org") 9                 .uri("http://httpbin.org"))10             .route("rewrite_route", r -> r.host("*.rewrite.org")11                 .filters(f -> f.rewritePath("/foo/(?.*)", "/${segment}"))12                 .uri("http://httpbin.org"))13             .route("hystrix_route", r -> r.host("*.hystrix.org")14                 .filters(f -> f.hystrix(c -> c.setName("slowcmd")))15                 .uri("http://httpbin.org"))16             .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")17                 .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))18                 .uri("http://httpbin.org"))19             .route("limit_route", r -> r20                 .host("*.limited.org").and().path("/anything/**")21                 .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))22                 .uri("http://httpbin.org"))23             .build();24     }25 }

3.1. GatewayFilter Factories

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

3.1.1. AddRequestHeader GatewayFilter Factory

AddRequestHeader GatewayFilter 采用name和value参数。

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

1 spring:2   cloud:3     gateway:4       routes:5       - id: add_request_header_route6         uri: https://example.org7         filters:8         - AddRequestHeader=X-Request-red, blue 

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

 1 spring: 2   cloud: 3     gateway: 4       routes: 5       - id: add_request_header_route 6         uri: https://example.org 7         predicates: 8         - Path=/red/{segment} 9         filters:10         - AddRequestHeader=X-Request-Red, Blue-{segment}

3.1.2. AddRequestParameter GatewayFilter Factory

AddRequestParameter GatewayFilter 也是采用name和value参数

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

1 spring:2   cloud:3     gateway:4       routes:5       - id: add_request_parameter_route6         uri: https://example.org7         filters:8         - AddRequestParameter=red, blue

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

 1 spring: 2   cloud: 3     gateway: 4       routes: 5       - id: add_request_parameter_route 6         uri: https://example.org 7         predicates: 8         - Host: {segment}.myhost.org 9         filters:10         - AddRequestParameter=foo, bar-{segment}

3.1.3. AddResponseHeader GatewayFilter Factory

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

1 spring:2   cloud:3     gateway:4       routes:5       - id: add_response_header_route6         uri: https://example.org7         filters:8         - 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响应头中的重复值。

1 spring:2   cloud:3     gateway:4       routes:5       - id: dedupe_response_header_route6         uri: https://example.org7         filters:8         - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

3.1.5. PrefixPath GatewayFilter Factory

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

1 spring:2   cloud:3     gateway:4       routes:5       - id: prefixpath_route6         uri: https://example.org7         filters:8         - 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个请求。

 1 spring: 2   cloud: 3     gateway: 4       routes: 5       - id: requestratelimiter_route 6         uri: https://example.org 7         filters: 8         - name: RequestRateLimiter 9           args:10             redis-rate-limiter.replenishRate: 1011             redis-rate-limiter.burstCapacity: 2012             redis-rate-limiter.requestedTokens: 1 

KeyResolver

1 @Bean2 KeyResolver userKeyResolver() {3     return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));4 }

上面的例子,定义了每个用户每秒运行10个请求,令牌桶的容量是20,那么,下一秒将只剩下10个令牌可用。KeyResolver实现仅仅只是简单取请求参数中的user,当然在生产环境中不推荐这么做。

说白了,KeyResolver就是决定哪些请求属于同一个用户的。比如,header中userId相同的就认为是同一个用户的请求。

当然,你也可以自己实现一个RateLimiter,在配置的时候用SpEL表达式#{@myRateLimiter}去引用它。例如:

 1 spring: 2   cloud: 3     gateway: 4       routes: 5       - id: requestratelimiter_route 6         uri: https://example.org 7         filters: 8         - name: RequestRateLimiter 9           args:10             rate-limiter: "#{@myRateLimiter}"11             key-resolver: "#{@userKeyResolver}"

补充:(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个,则不会从令牌桶中删除任何令牌,并且将数据包视为不合格。 

3.1.7. RedirectTo GatewayFilter Factory

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

1 spring:2   cloud:3     gateway:4       routes:5       - id: prefixpath_route6         uri: https://example.org7         filters:8         - RedirectTo=302, https://acme.org

3.1.8. RemoveRequestHeader GatewayFilter Factory

1 spring:2   cloud:3     gateway:4       routes:5       - id: removerequestheader_route6         uri: https://example.org7         filters:8         - RemoveRequestHeader=X-Request-Foo

3.1.9. RewritePath GatewayFilter Factory

 1 spring: 2   cloud: 3     gateway: 4       routes: 5       - id: rewritepath_route 6         uri: https://example.org 7         predicates: 8         - Path=/foo/** 9         filters:10         - RewritePath=/red(?/?.*), ${segment}

3.1.10. Default Filters

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

1 spring:2   cloud:3     gateway:4       default-filters:5       - AddResponseHeader=X-Response-Default-Red, Default-Blue6       - 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”阶段是最后一个。

 1 @Bean 2 public GlobalFilter customFilter() { 3     return new CustomGlobalFilter(); 4 } 5  6 public class CustomGlobalFilter implements GlobalFilter, Ordered { 7  8     @Override 9     public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {10         log.info("custom global filter");11         return chain.filter(exchange);12     }13 14     @Override15     public int getOrder() {16         return -1;17     }18 }

补充:(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个,则不会从令牌桶中删除任何令牌,并且将数据包视为不合格。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值