1.GateWay是什麼?
GateWay 是SpringCloud 生態系統中的網關,目標是替代Zuul,同樣提供了限流,監控,路由轉發、權限校驗等功能。
相關名詞:
- Route(路由):這是網關的基本構建塊。它由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言爲真,則路由匹配。
- Predicate(斷言):這是一個 Java 8 的 Predicate。輸入類型是一個 ServerWebExchange。我們可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或參數。
- Filter(過濾器):這是org.springframework.cloud.gateway.filter.GatewayFilter的實例,我們可以使用它修改請求和響應。
客戶端向 Spring Cloud Gateway 發出請求,如果HandlerMapping中找到了請求相匹配的路由,將其發送到Web Handler。Handler再通過指定過濾器鏈將請求發送到實際服務之星業務邏輯,然後返回。虛線是過濾器可能會在發送代理請求之前pre活之後post執行業務邏輯。
2.創建工程
2.1依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
注意:springcloud gateway使用的web框架爲webflux,和springMVC不兼容。
2.2代碼
@SpringBootApplication
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("test_route", r -> r.path("/test")
.uri("http://baidu.com"))
.build();
}
}
路由有兩種配置方式,一種是像上面這樣配置一個id名爲test_route的路由,當訪問項目地址/test的時候,會自動轉發到baidu.com。
另一種是在配置文件中配置,下面介紹。
2.3配置文件
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: test_route
uri: http://www.baidu.com
predicates:
- Path=/foo/**
filters:
- StripPrefix=1
字段含義如下:
- id: 自定有路由id,唯一
- uri: 跳轉的目標地址
- predicates: 路由條件
- filters: 過濾規則
上面配置的意思是配置了一個id爲test_route的路由規則,當訪問項目地址/foo/hello的時候自動轉發到地址baidu.com。如果上面例子中沒有加一個StripPrefix=1過濾器,則目標uri爲http://localhost:8000/foo/bar,StripPrefix過濾器是去掉一個路徑。
routes配置和代碼配置保留一處即可。
3.路由規則詳解
Predicate 來源於 Java 8,是 Java 8 中引入的一個函數,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其他複雜的邏輯(比如:與,或,非)。可以用於接口請求參數校驗、判斷新老數據是否有變化需要進行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性實現了各種路由匹配規則,有通過 Header、請求參數等不同的條件來進行作爲條件匹配到對應的路由。網上有一張圖總結了 Spring Cloud 內置的幾種 Predicate 的實現。
3.1時間匹配
spring:
cloud:
gateway:
routes:
- id: time_route
uri: http://ityouknow.com
predicates:
- After=2018-01-20T06:06:06+08:00[Asia/Shanghai]
Spring 是通過 ZonedDateTime 來對時間進行的對比,ZonedDateTime 是 Java 8
中日期時間功能裏,用於表示帶時區的日期與時間信息的類,ZonedDateTime
支持通過時區來設置時間,中國的時區是:Asia/Shanghai。 After Route Predicate
是指在這個時間之後的請求都轉發到目標地址。上面的示例是指,請求時間在
2018年1月20日6點6分6秒之後的所有請求都轉發到地址http://ityouknow.com。+08:00是指時間和UTC時間相差八個小時,時間地區爲Asia/Shanghai。
3.2 Cookie匹配
Cookie Route Predicate 可以接收兩個參數,一個是 Cookie name ,一個是正則表達式,路由規則會通過獲取對應的 Cookie name 值和正則表達式去匹配,如果匹配上就會執行路由,如果沒有匹配上則不執行。
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://ityouknow.com
predicates:
- Cookie=ityouknow, kee.e
3.3 通過請求路徑匹配(常用)
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://baidu.com
predicates:
- Path=/foo/{segment}
如果請求路徑符合要求,則此路由將匹配,例如:/foo/1 或者 /foo/bar。
使用 curl 測試,命令行輸入:
curl http://localhost:8080/foo/1
3.4通過請求參數匹配
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://ityouknow.com
predicates:
- Query=smile
這樣配置,只要請求中包含 smile 屬性的參數即可匹配路由。
使用 curl 測試,命令行輸入:
curl localhost:8080?smile=x&id=2
4.利用過濾器修改接口的返回報文(後置過濾)
各個子服務返回的報文各異,需要在網關對返回報文進行包裝統一返回格式。
@Component
@Slf4j
public class ResponseFilter implements GlobalFilter, Ordered {
//白名單
@Value("${filter.url.white.list.rsp}")
private String[] skipAuthUrls ;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse originalResponse = exchange.getResponse();
String url = exchange.getRequest().getURI().getPath();
//跳過不需要驗證的路徑
if(Arrays.asList(skipAuthUrls).contains(url)){
return chain.filter(exchange);
}
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
// probably should reuse buffers
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
// 釋放掉內存
DataBufferUtils.release(dataBuffer);
String rs = new String(content, Charset.forName("UTF-8"));
//默認失敗
CommonResult commonResult = CommonResult.failed();
try {
if(StringUtils.isNotBlank(rs)){
JsonResult jsonResult= JSONObject.parseObject(rs,JsonResult.class);
if(null != jsonResult ){
if(0 == jsonResult.getCode()){
commonResult = CommonResult.success(jsonResult.getData(),jsonResult.getMessage());
}else{
commonResult = CommonResult.failed(jsonResult.getMessage());
}
}
}
}catch (Exception e){
log.error("轉換異常,異常報文:{}",rs);
log.error(e.getMessage(),e);
}
byte[] newRs = JSON.toJSONString(commonResult).getBytes(Charset.forName("UTF-8"));
originalResponse.getHeaders().setContentLength(newRs.length);//如果不重新設置長度則收不到消息。
return bufferFactory.wrap(newRs);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
/**
*
* 功能描述: 執行優先級
* 此處order需要小於-1,需要先於NettyWriteResponseFilter過濾器執行
* @param:
* @return:
* @auther: lfc
* @date: 2019/8/25 18:56
*/
@Override
public int getOrder() {
return -99;
}
需要注意的是order需要小於-1,需要先於NettyWriteResponseFilter過濾器執行。
5.過濾器攔截權限認證(前置過濾)
@Component
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {
@Value("${filter.url.white.list.req}")
private String[] skipAuthUrls;
private String jwtBlacklistKeyFormat;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
//跳過不需要驗證的路徑
if(Arrays.asList(skipAuthUrls).contains(url)){
return chain.filter(exchange);
}
//從請求頭中取出token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
//未攜帶token或token在黑名單內
if (token == null ||
token.isEmpty() ||
isBlackToken(token)) {
if(log.isDebugEnabled() && isBlackToken(token)){
log.debug("**********此token已加入黑名單**********");
}
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = JSONObject.toJSONString(CommonResult.unauthorized(null)).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
//取出token包含的身份
String data = JWTUtil.getData(token, Constant.JWT_TYPE);
if(data.isEmpty()){
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = JSONObject.toJSONString(CommonResult.forbidden(null)).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
UserDTO ud = JSONObject.parseObject(data,UserDTO.class);
// 1.解析判斷是否被修改 2.根據token 獲取redis中的數據以此判斷是否超時或有效
if(!JWTUtil.verify(token) || StringUtils.isBlank(JedisUtil.getJson(ud.getJti()))){
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = JSONObject.toJSONString(CommonResult.tamperToken(null)).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
//redis 更新過期時間
JedisUtil.setJson(ud.getJti(),token,Constant.EXRP_HOUR);
data = JWTUtil.sign(JSONObject.toJSONString(ud));
//將現在的request,添加當前身份
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization", data).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
@Override
public int getOrder() {
return -100;
}
/**
*
* 功能描述: 判斷token是否在黑名單內
* @param token
* @return
* @auther: lfc
* @date: 2019/9/9 0:34
*/
private boolean isBlackToken(String token){
// assert token != null;
// return stringRedisTemplate.hasKey(String.format(jwtBlacklistKeyFormat, token));
return false;
}
GateWay中區分前置過濾還是後置過濾取決於動作在chain.filter方法前還是之後,之後回調的是後置,之前調用的是前置。