Spring Cloud Gateway 实现Token校验
- 2020 年 3 月 9 日
- 笔记
在我看来,在某些场景下,网关就像是一个公共方法,把项目中的都要用到的一些功能提出来,抽象成一个服务。比如,我们可以在业务网关上做日志收集、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
https://en.wikipedia.org/wiki/Token_bucket