1.網關是怎么演化來的
- 單體應用拆分成多個服務后,對外需要一個統一入口,解耦客戶端與內部服務
2.網關的基本功能
- 網關核心功能是路由轉發,因此不要有耗時操作在網關上處理,讓請求快速轉發到后端服務上
- 網關還能做統一的熔斷、限流、認證、日志監控等
3.關於Spring Cloud Gateway
Spring Cloud Gateway是由spring官方基於Spring5.0、Spring Boot2.0、Project Reactor等技術開發的網關,使用非阻塞API,Websockets得到支持,目的是代替原先版本中的Spring Cloud Netfilx Zuul,目前Netfilx已經開源了Zuul2.0,但Spring 沒有考慮集成,而是推出了自己開發的Spring Cloud GateWay。這里需要注意一下gateway使用的netty+webflux實現,不要加入web依賴(不要引用webmvc),否則初始化會報錯 ,需要加入webflux依賴。
gateway與zuul的簡單比較:gateway使用的是異步請求,zuul是同步請求,gateway的數據封裝在ServerWebExchange里,zuul封裝在RequestContext里,同步方便調式,可以把數據封裝在ThreadLocal中傳遞。
Spring Cloud Gateway有三個核心概念:路由、斷言、過濾器
過濾器:gateway有兩種filter:GlobalFilter、GatewayFilter,全局過濾器默認對所有路由有效。
文檔地址:https://cloud.spring.io/spring-cloud-static/Finchley.SR2/multi/multi_spring-cloud.html
網關作為所有請求流量的入口,在實際生產環境中為了保證高可靠和高可用,盡量避免重啟,需要用到動態路由配置,在網關運行過程中更改路由配置
4.代碼實踐
需要用到3個項目,eureka-server、gateway、consumer-service
- 1.eureka-server 服務發現注冊,供gateway轉發請求時獲取服務實例 ip+port,使用前面博客中的示例代碼
- 2.新建 gateway 網關項目,項目引用如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在主類上啟用服務發現注冊注解 @EnableDiscoveryClient
配置文件內容如下:
server:
port: 9999
spring:
profiles:
active: dev
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
# 服務名小寫
lower-case-service-id: true
routes:
- id: consumer-service
# lb代表從注冊中心獲取服務,且已負載均衡方式轉發
uri: lb://consumer-service
predicates:
- Path=/consumer/**
# 加上StripPrefix=1,否則轉發到后端服務時會帶上consumer前綴
filters:
- StripPrefix=1
# 注冊中心
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://zy:zy123@localhost:10025/eureka/
# 暴露監控端點
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
上面就完成了網關代碼部分,下面新建consumer-service
- 3.consumer-service 消費者服務 ,通過網關路由轉發到消費者服務,並返回信息回去,因此是個mvc的項目
項目引用如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在主類上啟用服務發現注冊注解 @EnableDiscoveryClient
在配置文件中添加配置:
server.port=9700
spring.application.name=consumer-service
eureka.instance.prefer-ip-address=true
# 配置eureka-server security的賬戶信息
eureka.client.serviceUrl.defaultZone=http://zy:zy123@localhost:10025/eureka/
新建 IndexController ,添加一個 hello 方法,傳入name參數,訪問后返回 hi + name 字符串
@RestController
public class IndexController {
@RequestMapping("/hello")
public String hello(String name){
return "hi " + name;
}
}
- 4.分別啟動3個項目,訪問 http://localhost:10025 看eureka上gateway與consumer-service實例是否注冊了,可以看到已經注冊了,分別在9700、9999端口
通過網關訪問consumer-service的hello方法,http://localhost:9999/consumer/hello?name=zy ,效果如下,說明請求已經轉發到consumer-service服務上了
以上完成了網關的基本代碼,下面繼續介紹一些常用的過濾器,通過過濾器實現統一認證鑒權、日志、安全等檢驗
- 5.在網關項目中添加 GlobalFilter 全局過濾器,打印每次請求的url,代碼如下:
/**
* 全局過濾器
* 所有請求都會執行
* 可攔截get、post等請求做邏輯處理
*/
@Component
public class RequestGlobalFilter implements GlobalFilter,Ordered {
//執行邏輯
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest= exchange.getRequest();
String uri = serverHttpRequest.getURI().toString();
System.out.println(" uri : " + uri);//打印每次請求的url
String method = serverHttpRequest.getMethodValue();
if ("POST".equals(method)) {
//從請求里獲取Post請求體
String bodyStr = resolveBodyFromRequest(serverHttpRequest);
//TODO 得到Post請求的請求參數后,做你想做的事
//下面的將請求體再次封裝寫回到request里,傳到下一級,否則,由於請求體已被消費,后續的服務將取不到值
URI uri = serverHttpRequest.getURI();
ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build();
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
request = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
//封裝request,傳給下一級
return chain.filter(exchange.mutate().request(request).build());
} else if ("GET".equals(method)) {
Map requestQueryParams = serverHttpRequest.getQueryParams();
//TODO 得到Get請求的請求參數后,做你想做的事
return chain.filter(exchange);
}
return chain.filter(exchange);
}
/**
* 從Flux<DataBuffer>中獲取字符串的方法
* @return 請求體
*/
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
//獲取請求體
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
//獲取request body
return bodyRef.get();
}
private DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
//執行順序
@Override
public int getOrder() {
return 1;
}
}
重新運行網關項目,並訪問 http://localhost:9999/consumer/hello?name=zy ,查看控制台,可看到 uri 日志被打印出來了
- 6.在網關項目中添加 GatewayFilter 過濾器 ,我們給consumer-service 添加 token 認證過濾器 ,和全局過濾器u同的是,GatewayFilter需要在配置文件中指定那個服務使用此過濾器,代碼如下:
/**
* 可對客戶端header 中的 Authorization 信息進行認證
*/
@Component
public class TokenAuthenticationFilter extends AbstractGatewayFilterFactory {
private static final String Bearer_ = "Bearer ";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
ServerHttpResponse response = exchange.getResponse();
try {
//String token = exchange.getRequest().getQueryParams().getFirst("authToken");
//1.獲取header中的Authorization
String header = request.getHeaders().getFirst("Authorization");
if (header == null || !header.startsWith(Bearer_)) {
throw new RuntimeException("請求頭中Authorization信息為空");
}
//2.截取Authorization Bearer
String token = header.substring(7);
//可把token存到redis中,此時直接在redis中判斷是否有此key,有則校驗通過,否則校驗失敗
if(!StringUtils.isEmpty(token)){
System.out.println("驗證通過");
//3.有token,把token設置到header中,傳遞給后端服務
mutate.header("userDetails",token).build();
}else{
//4.token無效
System.out.println("token無效");
DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,"無效的請求");
return response.writeWith(Mono.just(bodyDataBuffer));
}
}catch (Exception e){
//沒有token
DataBuffer bodyDataBuffer = responseErrorInfo(response , HttpStatus.UNAUTHORIZED.toString() ,e.getMessage());
return response.writeWith(Mono.just(bodyDataBuffer));
}
ServerHttpRequest build = mutate.build();
return chain.filter(exchange.mutate().request(build).build());
};
}
/**
* 自定義返回錯誤信息
* @param response
* @param status
* @param message
* @return
*/
public DataBuffer responseErrorInfo(ServerHttpResponse response , String status ,String message){
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
Map<String,String> map = new HashMap<>();
map.put("status",status);
map.put("message",message);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(map.toString().getBytes());
return bodyDataBuffer;
}
}
在配置文件中指定consumer-service服務使用 TokenAuthenticationFilter ,配置如下:
routes:
- id: consumer-service
uri: lb://consumer-service
predicates:
- Path=/consumer/**
filters:
# 進行token過濾
- TokenAuthenticationFilter
- StripPrefix=1
運行項目,再次訪問 http://localhost:9999/consumer/hello?name=zy
- 7.前后端分離項目解決網關跨域問題,在網關主類中添加以下代碼:
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(ctx);
}
HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = ctx.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
if (requestMethod != null) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "all");
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx);
};
}
代碼已上傳至碼雲,源碼,項目使用的版本信息如下:
- SpringBoot 2.0.6.RELEASE
- SpringCloud Finchley.SR2
轉自:https://blog.csdn.net/dark868/article/details/106532216/