Spring Cloud Gateway 實作Token校驗
在我看來,在某些場景下,網關就像是一個公共方法,把項目中的都要用到的一些功能提出來,抽象成一個服務。比如,我們可以在業務網關上做日志收集、Token校驗等等,當然這麼了解很狹隘,因為網關的能力遠不止如此,但是不妨礙我們更好地了解它。下面的例子示範了,如何在網關校驗Token,并提取使用者資訊放到Header中傳給下遊業務系統。
- 生成Token
使用者登入成功以後,生成token,此後的所有請求都帶着token。網關負責校驗token,并将使用者資訊放入請求Header,以便下遊系統可以友善的擷取使用者資訊。
為了友善示範,本例中涉及三個工程
公共項目: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 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
5 4.0.0
6
7 com.cjs.example
8 cjs-commons-jwt
9 1.0-SNAPSHOT
10
11
12 UTF-8
13 1.8
14 1.8
15
16
17
18
19 com.auth0
20 java-jwt
21 3.10.0
22
23
24 org.apache.commons
25 commons-lang3
26 3.9
27
28
29 com.alibaba
30 fastjson
31 1.2.66
32
33
34
35
JWTUtil.java
1 package com.cjs.example.utils;
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 ChengJianSheng
17 * @date 2020-03-08
18 */
19 public class JWTUtil {
20
21 public static final long TOKEN_EXPIRE_TIME = 7200 * 1000;
22 private static final String ISSUER = "cheng";
24 /**
25 * 生成Token
26 * @param username 使用者辨別(不一定是使用者名,有可能是使用者ID或者手機号什麼的)
27 * @param secretKey
28 * @return
29 */
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);
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 * 校驗Token
47 * @param token
48 * @param secretKey
49 * @return
50 */
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 token
70 * @return
71 */
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;
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
21 private int code;
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 }
34 public String getMessage() {
35 return message;
36 }
37
38 }
ResponseResult.java
1 package com.cjs.example;
3 import com.cjs.example.enums.ResponseCodeEnum;
4
5 /**
6 * @author ChengJianSheng
7 * @date 2020-03-08
8 */
9 public class ResponseResult {
11 private int code = 0;
13 private String msg;
15 private T data;
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 }
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 }
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中
2 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xs4 4.0.0
5
6 org.springframework.boot
7 spring-boot-starter-parent
8 2.2.5.RELEASE
9
11 com.cjs.example
12 cjs-auth-service
13 0.0.1-SNAPSHOT
14 cjs-auth-service
17 1.8
22 org.springframework.boot
23 spring-boot-starter-data-redis
25
26 org.springframework.boot
27 spring-boot-starter-web
30
31 org.apache.commons
32 commons-lang3
33 3.9
36 commons-codec
37 commons-codec
38 1.14
40
41 org.apache.commons
42 commons-pool2
43 2.8.0
45
46
47 com.cjs.example
48 cjs-commons-jwt
49 1.0-SNAPSHOT
50
52
53 org.projectlombok
54 lombok
55 true
56
57
58
60
61
62 org.springframework.boot
63 spring-boot-maven-plugin
64
65
68
LoginController.java
1 package com.cjs.example.controller;
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.*;
19 import java.util.UUID;
20 import java.util.concurrent.TimeUnit;
22 /**
23 * @author ChengJianSheng
24 * @date 2020-03-08
25 */
26 @RestController
27 public class LoginController {
29 /**
30 * Apollo 或 Nacos
31 */
32 @Value("${secretKey:123456}")
33 private String secretKey;
35 @Autowired
36 private StringRedisTemplate stringRedisTemplate;
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 }
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("-", "");
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);
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 * 重新整理Token
101 */
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 // 生成新token
114 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
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 ChengJianSheng
10 * @date 2020-03-08
11 */
12 @RestController
13 @RequestMapping("/hello")
14 public class HelloController {
16 @GetMapping("/sayHello")
17 public String sayHello(String name) {
18 return "Hello, " + name;
19 }
21 @GetMapping("/sayHi")
22 public String sayHi(@RequestHeader("userId") String userId) {
23 return userId;
24 }
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.1
10 password: 123456
11 port: 6379
12 lettuce:
13 pool:
14 max-active: 10
15 max-idle: 5
16 min-idle: 5
17 max-wait: 5000
- 校驗Token
GatewayFilter和GlobalFilter都可以,這裡用GlobalFilter
11 com.cms.example
12 cjs-gateway-example
14 cjs-gateway-example
18 Hoxton.SR1
23 org.springframework.boot
24 spring-boot-starter-data-redis-reactive
26
27 org.springframework.cloud
28 spring-cloud-starter-gateway
31 com.auth0
32 java-jwt
33 3.10.0
36 com.cjs.example
37 cjs-commons-jwt
38 1.0-SNAPSHOT
42
43 org.projectlombok
44 lombok
45 true
48
49
52 org.springframework.cloud
53 spring-cloud-dependencies
54 ${spring-cloud.version}
55 pom
56 import
62
64 org.springframework.boot
65 spring-boot-maven-plugin
70
AuthorizeFilter.java
1 package com.cms.example.filter;
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;
25 /**
26 * @author ChengJianSheng
27 * @date 2020-03-08
28 */
29 @Slf4j
30 @Component
31 public class AuthorizeFilter implements GlobalFilter, Ordered {
33 @Value("${secretKey:123456}")
34 private String secretKey;
36 // @Autowired
37 // private StringRedisTemplate stringRedisTemplate;
38
39 @Override
40 public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
41 ServerHttpRequest serverHttpRequest = exchange.getRequest();
42 ServerHttpResponse serverHttpResponse = exchange.getResponse();
43 String uri = serverHttpRequest.getURI().getPath();
45 // 檢查白名單(配置)
46 if (uri.indexOf("/auth-server/login") >= 0) {
47 return chain.filter(exchange);
48 }
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 }
56 //todo 檢查Redis中是否有此Token
58 try {
59 JWTUtil.verifyToken(token, secretKey);
60 } catch (TokenAuthenticationException ex) {
61 return getVoidMono(serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
63 return getVoidMono(serverHttpResponse, ResponseCodeEnum.UNKNOWN_ERROR);
66 String userId = JWTUtil.getUserInfo(token);
68 ServerHttpRequest mutableReq = serverHttpRequest.mutate().header("userId", userId).build();
69 ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
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 @Override
82 public int getOrder() {
83 return -100;
84 }
85 }
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/**
這裡我還自定義了一個日志收集過濾器
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;
11 import java.util.Arrays;
12 import java.util.List;
13
14 /**
15 * @author ChengJianSheng
16 * @date 2020-03-08
17 */
18 @Component
19 public class MyLogGatewayFilterFactory extends AbstractGatewayFilterFactory {
21 private static final Log log = LogFactory.getLog(MyLogGatewayFilterFactory.class);
22 private static final String MY_LOG_START_TIME = MyLogGatewayFilterFactory.class.getName() + "." + "startTime";
24 public MyLogGatewayFilterFactory() {
25 super(Config.class);
28 @Override
29 public List shortcutFieldOrder() {
30 return Arrays.asList("enabled");
31 }
33 @Override
34 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 }
57 public static class Config {
58 /**
59 * 是否開啟
60 */
61 private boolean enabled;
63 public Config() {
66 public boolean isEnabled() {
67 return enabled;
68 }
70 public void setEnabled(boolean enabled) {
71 this.enabled = enabled;
72 }
73 }
74 }
用Postman通路就能看到效果
http://localhost:8080/auth-server/hello/sayHi http://localhost:8080/auth-server/hello/sayHello?name=aaa- 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 -> r
20 .host(".limited.org").and().path("/anything/*")
21 .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
22 .uri("http://httpbin.org"))
23 .build();
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
2 cloud:
3 gateway:
4 routes:
5 - id: add_request_header_route
6 uri:
https://example.org7 filters:
8 - AddRequestHeader=X-Request-red, blue
剛才說了,AddRequestHeader采用name和value作為參數。而URI中的變量可以用在value中,例如:
5 - id: add_request_header_route
6 uri:
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
5 - id: add_request_parameter_route
8 - AddRequestParameter=red, blue
同樣,AddRequestParameter也支援在value中引用URI中的變量,例如:
5 - id: add_request_parameter_route
8 - Host: {segment}.myhost.org
10 - AddRequestParameter=foo, bar-{segment}
3.1.3. AddResponseHeader GatewayFilter Factory
AddResponseHeader GatewayFilter 依然采用name和value參數。不在贅述,如下:
5 - id: add_response_header_route
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響應頭中的重複值。
5 - id: dedupe_response_header_route
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
5 - id: prefixpath_route
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個請求。
5 - id: requestratelimiter_route
8 - name: RequestRateLimiter
9 args:
10 redis-rate-limiter.replenishRate: 10
11 redis-rate-limiter.burstCapacity: 20
12 redis-rate-limiter.requestedTokens: 1
KeyResolver
1 @Bean
2 KeyResolver userKeyResolver() {
3 return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
4 }
上面的例子,定義了每個使用者每秒運作10個請求,令牌桶的容量是20,那麼,下一秒将隻剩下10個令牌可用。KeyResolver實作僅僅隻是簡單取請求參數中的user,當然在生産環境中不推薦這麼做。
說白了,KeyResolver就是決定哪些請求屬于同一個使用者的。比如,header中userId相同的就認為是同一個使用者的請求。
當然,你也可以自己實作一個RateLimiter,在配置的時候用SpEL表達式#{@myRateLimiter}去引用它。例如:
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系列的。不解釋,看示例:
8 - RedirectTo=302,
https://acme.org3.1.8. RemoveRequestHeader GatewayFilter Factory
5 - id: removerequestheader_route
8 - RemoveRequestHeader=X-Request-Foo
3.1.9. RewritePath GatewayFilter Factory
5 - id: rewritepath_route
8 - Path=/foo/**
10 - RewritePath=/red(?/?.*), ${segment}
3.1.10. Default Filters
為了添加一個過濾器,并将其應用到所有路由上,你可以使用spring.cloud.gateway.default-filters,這個屬性值是一個過濾器清單
4 default-filters:
5 - AddResponseHeader=X-Response-Default-Red, Default-Blue
6 - 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”階段是最後一個。
2 public GlobalFilter customFilter() {
3 return new CustomGlobalFilter();
4 }
6 public class CustomGlobalFilter implements GlobalFilter, Ordered {
8 @Override
9 public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
10 log.info("custom global filter");
11 return chain.filter(exchange);
12 }
14 @Override
15 public int getOrder() {
16 return -1;
17 }
18 }
- Docs
原文位址
https://www.cnblogs.com/cjsblog/p/12425912.html