0、前言
在微服務架構中,1個系統會被拆分為了很多個微服務。
如果每1個微服務都直接對外暴露出來,讓使用者直接通路這些微服務;
那麼如何對使用者的身份和權限進行鑒定?如何對微服務中的通路流量進行限流?
此時我們需要1個統一的入口(網關服務)以上問題将迎刃而解;
一、服務網關(Gateway)簡介
微服務的網關=路由轉發+過濾器
如果沒有網關的存在,我們隻能在用戶端記錄每個微服務的位址,然後分别去調用。
以上架構,會存在着諸多的問題:
用戶端多次請求不同的微服務,增加用戶端代碼或配置編寫的複雜性
認證複雜,每個服務都需要獨立認證。
存在跨域請求,在一定場景下處理相對複雜。
上面的這些問題可以借助API網關來解決。所謂的API網關,就是指系統的統一入口。它封裝了應用程式的内部結構,為用戶端提供統一服務。
一些與業務本身功能無關的公共邏輯可以在這裡實作,諸如認證、鑒權、監控、路由轉發等等。
添加上API網關之後,系統的架構圖變成了如下所示:
Spring Cloud Gateway旨在為微服務架構提供一種簡單有效的統一的 API路由管理方式。
它不僅提供統一的路由方式,并且基于Filter鍊的方式提供了網關基本的功能,例如:安全,監控和限流。
微服務網關的作用
提供了統一通路入口,降低了服務受攻擊面 提供了統一跨域解決方案 提供了統一日志記錄操作,可以進行統一監控 提供了統一權限認證支援 提供了微服務限流功能,可以保護微服務,防止雪崩效應發生
二、Gateway搭建
1.建立1個api-gateway子產品
2.pom依賴
我們使用的網關産品為spring-cloud架構提供的gateway;服務網關需要調用服務注冊中心(Nacos)擷取服務提供者的調用位址;
<dependencies>
<!--引入gateway網關-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--Nacos服務發現依賴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
3.建立啟動類
package com.zhanggen.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4.添加配置檔案(application.yaml)
路由(Route) 是 gateway 中最基本的元件之一,表示一個具體的路由資訊載體。主要定義了下面的幾個資訊:
id,路由辨別符,差別于其他 Route。
uri,路由指向的目的地 uri,即用戶端請求最終被轉發到的微服務。
predicate,斷言的作用是進行條件判斷,隻有斷言都傳回真,才會真正的執行路由。
filter,過濾器用于修改請求和響應資訊。
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos位址
gateway:
routes: # 路由數組[路由 就是指定當請求滿足什麼條件的時候轉到哪個微服務]
- id: user-service-route # 目前路由的辨別, 要求唯一
uri: lb://user-service # 請求要轉發到的位址
predicates: # 斷言(就是路由轉發要滿足的條件)
- Path=/user/** # 當請求路徑滿足Path指定的規則時,才進行路由轉發
跨域配置
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
routes:
# 平台管理
- id: admin
uri: lb://leadnews-admin
predicates:
- Path=/admin/**
filters:
- StripPrefix= 1
- id: wemedia
uri: lb://leadnews-wemedia
predicates:
- Path=/wemedia/**
filters:
- StripPrefix= 1
5.測試網關路由轉發功能
啟動項目,并通過網關去通路使用者微服務,現請求流程如下
使用者的請求http://127.0.0.1:7000/user/1到達GatWay GatWay摘取URL中的/user/1路徑進行路由比對 比對成功之後URL變成http://user-service/user/1 GatWay調用用戶端負載均衡器(Ribin)去服務注冊中心(Nacos)拉取http://user-service/user/1對應的服務,進行負載均衡選擇 選擇出1個微服務之後http://user-service/user/1轉換成http://127.0.0.1:8081/user/1 GatWay把使用者請求轉發到http://127.0.0.1:8081/user/1;
6.Nginx轉發
使用Nginx轉發使用者請求到Gateway
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
upstream heima-app-gateway{
server localhost:51601;
}
server {
listen 8801;
location / {
root html/app/;
index index.html;
}
location ~/app/(.*) {
proxy_pass http://heima-app-gateway/$1;
proxy_set_header HOST $host; # 不改變源請求頭的值
proxy_pass_request_body on; #開啟擷取請求體
proxy_pass_request_headers on; #開啟擷取請求頭
proxy_set_header X-Real-IP $remote_addr; # 記錄真實送出請求的用戶端IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #記錄代理資訊
}
}
upstream heima-wemedia-gateway{
server localhost:51602;
}
server {
listen 8802;
location / {
root html/wemedia-web/;
index index.html;
}
location ~/wemedia/MEDIA/(.*) {
proxy_pass http://heima-wemedia-gateway/$1;
proxy_set_header HOST $host; # 不改變源請求頭的值
proxy_pass_request_body on; #開啟擷取請求體
proxy_pass_request_headers on; #開啟擷取請求頭
proxy_set_header X-Real-IP $remote_addr; # 記錄真實送出請求的用戶端IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #記錄代理資訊
}
}
upstream heima-admin-gateway{
server localhost:51603;
}
server {
listen 8803;
location / {
root html/admin-web/;
index index.html;
}
location ~/service_6001/(.*) {
proxy_pass http://heima-admin-gateway/$1;
proxy_set_header HOST $host; # 不改變源請求頭的值
proxy_pass_request_body on; #開啟擷取請求體
proxy_pass_request_headers on; #開啟擷取請求頭
proxy_set_header X-Real-IP $remote_addr; # 記錄真實送出請求的用戶端IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #記錄代理資訊
}
}
}
nginx.conf
三、斷言(了解)
斷言就是1刀切,隻有目前這1個條件判斷成功才能進行路由轉發,隻有斷言都傳回真,才會真正的執行路由。
1.基于Datetime類型的斷言
# AfterRoutePredicateFactory: 接收一個日期參數,判斷請求日期是否晚于指定日期
# BeforeRoutePredicateFactory: 接收一個日期參數,判斷請求日期是否早于指定日期
# BetweenRoutePredicateFactory: 接收兩個日期參數,判斷請求日期是否在指定時間段内
- After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
2.基于遠端位址的斷言
# RemoteAddrRoutePredicateFactory:接收一個IP位址段,判斷請求主機位址是否在位址段中
- RemoteAddr=192.168.1.1/24
3.基于Cookie的斷言
# CookieRoutePredicateFactory:接收兩個參數,cookie 名字和一個正規表達式。 判斷請求cookie是否具有給定名稱且值與正規表達式比對。
- Cookie=chocolate, ch.
4.基于Header的斷言
# HeaderRoutePredicateFactory:接收兩個參數,标題名稱和正規表達式。 判斷請求Header是否具有給定名稱且值與正規表達式比對。
- Header=X-Request-Id, \d+
5.基于Host的斷言
# HostRoutePredicateFactory:接收一個參數,主機名模式。判斷請求的Host是否滿足比對規則。
- Host=**.testhost.org
6.基于Path請求路徑的斷言
# PathRoutePredicateFactory:接收一個參數,判斷請求的URI部分是否滿足路徑規則。
- Path=/foo/{segment}
7.基于Query請求參數的斷言
# QueryRoutePredicateFactory :接收兩個參數,請求param和正規表達式, 判斷請求參數是否具有給定名稱且值與正規表達式比對。
- Query=baz, ba.
8.使用
驗證内置斷言的使用:
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos位址
gateway:
routes: # 路由數組[路由 就是指定當請求滿足什麼條件的時候轉到哪個微服務]
- id: user-service-route # 目前路由的辨別, 要求唯一
uri: lb://user-service # 請求要轉發到的位址
predicates: # 斷言(就是路由轉發要滿足的條件)
- Path=/user/** # 當請求路徑滿足Path指定的規則時,才進行路由轉發
- Before=2019-11-28T00:00:00.000+08:00 # 限制請求時間在2019-11-28之前
- Method=POST # 限制請求方式為POST
四、過濾器
Gateway也包含過濾器功能,網關服務的過濾器會對請求或響應進行攔截,完成一些通用操作。
1.過濾器執行時機
Gateway的過濾器中有2個執行時機:
PRE: 這種過濾器在請求被路由之前調用,可利用這種過濾器實作身份驗證、在叢集中選擇請求的微服務、記錄調試資訊等
POST:這種過濾器在路由到微服務以後執行,可用來為響應添加标準的HTTP Header、收集統計資訊和名額、将響應從微服務發送給用戶端等
2.過濾器類型
Gateway的Filter從作用範圍可分為2種
GatewayFilter:應用到單個路由或者一個分組的路由上
GlobalFilter:應用到所有的路由上
3.内置局部過濾器
局部過濾器是針對單個路由的過濾器。在SpringCloud Gateway中内置了很多不同類型的網關路由過濾器。具體如下:
過濾器工廠 | 作用 | 參數 |
AddRequestHeader | 為原始請求添加Header | Header的名稱及值 |
AddRequestParameter | 為原始請求添加請求參數 | 參數名稱及值 |
AddResponseHeader | 為原始響應添加Header | Header的名稱及值 |
DedupeResponseHeader | 剔除響應頭中重複的值 | 需要去重的Header名稱及去重政策 |
Hystrix | 為路由引入Hystrix的斷路器保護 | HystrixCommand的名稱 |
FallbackHeaders | 為fallbackUri的請求頭中添加具體的異常資訊 | Header的名稱 |
PrefixPath | 為原始請求路徑添加字首 | 字首路徑 |
PreserveHostHeader | 為請求添加一個preserveHostHeader=true的屬性,路由過濾器會檢查該屬性以決定是否要發送原始的Host | 無 |
RequestRateLimiter | 用于對請求限流,限流算法為令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
RedirectTo | 将原始請求重定向到指定的URL | http狀态碼及重定向的url |
RemoveHopByHopHeadersFilter | 為原始請求删除IETF組織規定的一系列Header | 預設就會啟用,可以通過配置指定僅删除哪些Header |
RemoveRequestHeader | 為原始請求删除某個Header | Header名稱 |
RemoveResponseHeader | 為原始響應删除某個Header | Header名稱 |
RewritePath | 重寫原始的請求路徑 | 原始路徑正規表達式以及重寫後路徑的正規表達式 |
RewriteResponseHeader | 重寫原始響應中的某個Header | Header名稱,值的正規表達式,重寫後的值 |
SaveSession | 在轉發請求之前,強制執行WebSession::save操作 | 無 |
secureHeaders | 為原始響應添加一系列起安全作用的響應頭 | 無,支援修改這些安全響應頭的值 |
SetPath | 修改原始的請求路徑 | 修改後的路徑 |
SetResponseHeader | 修改原始響應中某個Header的值 | Header名稱,修改後的值 |
SetStatus | 修改原始響應的狀态碼 | HTTP 狀态碼,可以是數字,也可以是字元串 |
StripPrefix | 用于截斷原始請求的路徑 | 使用數字表示要截斷的路徑的數量 |
Retry | 針對不同的響應進行重試 | retries、statuses、methods、series |
RequestSize | 設定允許接收最大請求包的大小。如果請求包大小超過設定的值,則傳回 413 Payload Too Large | 請求包大小,機關為位元組,預設值為5M |
ModifyRequestBody | 在轉發請求之前修改原始請求體内容 | 修改後的請求體内容 |
ModifyResponseBody | 修改原始響應體的内容 | 修改後的響應體内容 |
4.内置局部過濾器的使用
隻需要把過濾器配置在微服務的配置檔案中即可生效;
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos位址
gateway:
routes: # 路由數組[路由 就是指定當請求滿足什麼條件的時候轉到哪個微服務]
- id: user-service-route # 目前路由的辨別, 要求唯一
uri: lb://user-service # 請求要轉發到的位址
predicates: # 斷言(就是路由轉發要滿足的條件)
- Path=/user/** # 當請求路徑滿足Path指定的規則時,才進行路由轉發
filters:
- SetStatus=2000 # 修改傳回狀态
5.内置全局過濾器
全局過濾器作用于所有路由無需配置。通過全局過濾器可以實作對權限的統一校驗,安全性驗證等功能。
SpringCloud Gateway内部也是通過一系列的内置全局過濾器對整個路由轉發進行處理如下:
6.自定義全局過濾器(重點)
内置的過濾器已經可以完成大部分的功能,但是對于企業開發的一些業務功能處理,還是需要我們自己編寫過濾器來實作的。
下面,我們一起通過代碼的形式自定義一個過濾器,去完成統一的權限校驗。
開發中的鑒權邏輯:
當用戶端第一次請求服務時,服務端對使用者進行資訊認證(登入)
認證通過,将使用者資訊進行加密形成token,傳回給用戶端,作為登入憑證
以後每次請求,用戶端都攜帶認證的token
服務端對token進行解密,判斷是否有效。
下面的我們自定義一個GlobalFilter,去校驗所有請求的請求參數中是否包含“token”,如何不包含請求參數“token”則不轉發路由,否則執行正常的邏輯。
package com.zhanggen.gateway.auth;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//全局認證過濾器
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//認證邏輯
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//請求參數數,擷取一個叫token的參數的值 String token = request.getParamter("token")
String token = exchange.getRequest().getQueryParams().getFirst("token");
//判斷是否請求參數中攜帶了token
if (StringUtils.isBlank(token)) {
System.out.println("鑒權失敗");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//設定響應碼 resp.setStatus(401)
return exchange.getResponse().setComplete();//傳回響應
}
//調用chain.filter繼續向下遊執行
return chain.filter(exchange);
}
//決定目前過濾器的執行級别, 數組越小,優先級越高
@Override
public int getOrder() {
return 0;
}
}
com.zhanggen.gateway.auth.AuthGlobalFilter