在我看來,在某些場景下,網關就像是一個公共方法,把項目中的都要用到的一些功能提出來,抽象成一個服務。比如,我們可以在業務網關上做日志收集、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
1 <?xml version="1.0" encoding="UTF-8"?>
2
3 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5 <modelVersion>4.0.0</modelVersion>
6
7 <groupId>com.cjs.example</groupId>
8 <artifactId>cjs-commons-jwt</artifactId>
9 <version>1.0-SNAPSHOT</version>
10
11 <properties>
12 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
13 <maven.compiler.source>1.8</maven.compiler.source>
14 <maven.compiler.target>1.8</maven.compiler.target>
15 </properties>
16
17 <dependencies>
18 <dependency>
19 <groupId>com.auth0</groupId>
20 <artifactId>java-jwt</artifactId>
21 <version>3.10.0</version>
22 </dependency>
23 <dependency>
24 <groupId>org.apache.commons</groupId>
25 <artifactId>commons-lang3</artifactId>
26 <version>3.9</version>
27 </dependency>
28 <dependency>
29 <groupId>com.alibaba</groupId>
30 <artifactId>fastjson</artifactId>
31 <version>1.2.66</version>
32 </dependency>
33 </dependencies>
34
35 </project>
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 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";
23
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);
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 * 校驗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;
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<T> {
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 <T> ResponseResult<T> 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 <T> ResponseResult<T> 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 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>2.2.5.RELEASE</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>com.cjs.example</groupId>
12 <artifactId>cjs-auth-service</artifactId>
13 <version>0.0.1-SNAPSHOT</version>
14 <name>cjs-auth-service</name>
15
16 <properties>
17 <java.version>1.8</java.version>
18 </properties>
19
20 <dependencies>
21 <dependency>
22 <groupId>org.springframework.boot</groupId>
23 <artifactId>spring-boot-starter-data-redis</artifactId>
24 </dependency>
25 <dependency>
26 <groupId>org.springframework.boot</groupId>
27 <artifactId>spring-boot-starter-web</artifactId>
28 </dependency>
29
30 <dependency>
31 <groupId>org.apache.commons</groupId>
32 <artifactId>commons-lang3</artifactId>
33 <version>3.9</version>
34 </dependency>
35 <dependency>
36 <groupId>commons-codec</groupId>
37 <artifactId>commons-codec</artifactId>
38 <version>1.14</version>
39 </dependency>
40 <dependency>
41 <groupId>org.apache.commons</groupId>
42 <artifactId>commons-pool2</artifactId>
43 <version>2.8.0</version>
44 </dependency>
45
46 <dependency>
47 <groupId>com.cjs.example</groupId>
48 <artifactId>cjs-commons-jwt</artifactId>
49 <version>1.0-SNAPSHOT</version>
50 </dependency>
51
52 <dependency>
53 <groupId>org.projectlombok</groupId>
54 <artifactId>lombok</artifactId>
55 <optional>true</optional>
56 </dependency>
57 </dependencies>
58
59 <build>
60 <plugins>
61 <plugin>
62 <groupId>org.springframework.boot</groupId>
63 <artifactId>spring-boot-maven-plugin</artifactId>
64 </plugin>
65 </plugins>
66 </build>
67
68 </project>
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<String, String, String> 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<String, String, String> 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<String, String, String> 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
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 ChengJianSheng
10 * @date 2020-03-08
11 */
12 @RestController
13 @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.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
2. 校驗Token
GatewayFilter和GlobalFilter都可以,這里用GlobalFilter
pom.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>2.2.5.RELEASE</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>com.cms.example</groupId>
12 <artifactId>cjs-gateway-example</artifactId>
13 <version>0.0.1-SNAPSHOT</version>
14 <name>cjs-gateway-example</name>
15
16 <properties>
17 <java.version>1.8</java.version>
18 <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
19 </properties>
20
21 <dependencies>
22 <dependency>
23 <groupId>org.springframework.boot</groupId>
24 <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>org.springframework.cloud</groupId>
28 <artifactId>spring-cloud-starter-gateway</artifactId>
29 </dependency>
30 <dependency>
31 <groupId>com.auth0</groupId>
32 <artifactId>java-jwt</artifactId>
33 <version>3.10.0</version>
34 </dependency>
35 <dependency>
36 <groupId>com.cjs.example</groupId>
37 <artifactId>cjs-commons-jwt</artifactId>
38 <version>1.0-SNAPSHOT</version>
39 </dependency>
40
41
42 <dependency>
43 <groupId>org.projectlombok</groupId>
44 <artifactId>lombok</artifactId>
45 <optional>true</optional>
46 </dependency>
47 </dependencies>
48
49 <dependencyManagement>
50 <dependencies>
51 <dependency>
52 <groupId>org.springframework.cloud</groupId>
53 <artifactId>spring-cloud-dependencies</artifactId>
54 <version>${spring-cloud.version}</version>
55 <type>pom</type>
56 <scope>import</scope>
57 </dependency>
58 </dependencies>
59 </dependencyManagement>
60
61 <build>
62 <plugins>
63 <plugin>
64 <groupId>org.springframework.boot</groupId>
65 <artifactId>spring-boot-maven-plugin</artifactId>
66 </plugin>
67 </plugins>
68 </build>
69
70 </project>
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 ChengJianSheng
27 * @date 2020-03-08
28 */
29 @Slf4j
30 @Component
31 public class AuthorizeFilter implements GlobalFilter, Ordered {
32
33 @Value("${secretKey:123456}")
34 private String secretKey;
35
36 // @Autowired
37 // private StringRedisTemplate stringRedisTemplate;
38
39 @Override
40 public Mono<Void> 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中是否有此Token
57
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<Void> 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 }
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 ChengJianSheng
16 * @date 2020-03-08
17 */
18 @Component
19 public class MyLogGatewayFilterFactory extends AbstractGatewayFilterFactory<MyLogGatewayFilterFactory.Config> {
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 @Override
29 public List<String> shortcutFieldOrder() {
30 return Arrays.asList("enabled");
31 }
32
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 }
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/sayHi
http://localhost:8080/auth-server/hello/sayHello?name=aaa
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>.*)", "/${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();
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_route
6 uri: https://example.org
7 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_route
6 uri: https://example.org
7 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_route
6 uri: https://example.org
7 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_route
6 uri: https://example.org
7 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_route
6 uri: https://example.org
7 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: 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}去引用它。例如:
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_route
6 uri: https://example.org
7 filters:
8 - RedirectTo=302, https://acme.org
3.1.8. RemoveRequestHeader GatewayFilter Factory
1 spring:
2 cloud:
3 gateway:
4 routes:
5 - id: removerequestheader_route
6 uri: https://example.org
7 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>/?.*), $\{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-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”階段是最后一個。
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<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
10 log.info("custom global filter");
11 return chain.filter(exchange);
12 }
13
14 @Override
15 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個,則不會從令牌桶中刪除任何令牌,並且將數據包視為不合格。
4. Docs