API 網關的出現的原因是微服務架構的出現,不同的微服務一般會有不同的服務地址,而外部客戶端可能需要調用多個服務的接口才能完成一個業務需求,如果讓客戶端直接與各個微服務通信,會有以下的問題:
- 客戶端會多次請求不同的微服務,增加了客戶端的復雜性。
- 存在跨域請求,在一定場景下處理相對復雜。
- 認證復雜,每個服務都需要獨立認證。
- 難以重構,隨着項目的迭代,可能需要重新划分微服務。例如,可能將多個服務合並成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通信,那么重構將會很難實施。
- 某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難。
網關是介於客戶端和服務器端之間的中間層,所有的外部請求都會先經過 API 網關這一層。也就是說,API 的實現方面更多的考慮業務邏輯,而安全、性能、監控可以交由 API 網關來做 ,所以網關的性能,高可用,安全性都是至關重要的。
備注:Spring Cloud 微服務中搭建 OAuth2.0 認證授權服務
常用網關有哪些 ?
Nginx、Kong、ZUUL、Spring Cloud Gateway(Spring Cloud 官方)、Linkerd 等
Spring Cloud Zuul
Zuul 是 Netflix 開源的微服務網關組件,它可以和 Eureka、Ribbon、Hystrix 等組件配合使用。Zuul 的核心是一系列的過濾器 (比如:動態路由)。Spring Cloud Zuul 對 Zuul 進行了整合 ,從而更方便的與 Spring Cloud 一起使用。
Zuul1
Zuul1 是基於 Servlet 框架構建,采用的是阻塞和多線程方式,即一個線程處理一次連接請求,這種方式在內部延遲嚴重、設備故障較多情況下會引起存活的連接增多和線程增加的情況發生。
Zuul2
Zuul2 與 Zuul1 最大的區別是它運行在異步和無阻塞框架上,每個 CPU 核一個線程,處理所有的請求和響應,請求和響應的生命周期是通過事件和回調來處理的,這種方式減少了線程數量,因此開銷較小。又由於數據被存儲在同一個 CPU 里,可以復用 CPU 級別的緩存,前面提及的延遲和重試風暴問題也通過隊列存儲連接數和事件數方式減輕了很多(較線程切換來說輕量級很多,自然消耗較小)。這一變化一定會大大提升性能。
注:zuul 2.0 版本 Spring Cloud 官方現階段不打算集成,官方還是推薦使用 Spring Cloud Gateway
性能
可以參考:糾錯帖:Zuul & Spring Cloud Gateway & Linkerd性能對比 ,簡單來說,Zuul 1.x 是一個基於阻塞 IO 的 API Gateway,另外 Spring Cloud Gateway 性能很好。
高可用
一般生產環境需要將多個 Zuul 節點注冊到 Eureka Server 上,就可以實現 Zuul 的高可用。事實上,這種情況下的高可用和其他服務做高可用(例如:Eurka Server 集群)的方案沒有什么區別。當 Zuul 客戶端注冊到 Eureka Server 上時,Zuul 客戶端會自動從 Eureka Server 查詢 Zuul Server 列表,然后使用負載均衡組件(例如: Ribbon)請求 Zuul 集群。另外的方式也可以使用 Nginx 或者硬件 F5 的來實現。
安全性
Spring Cloud 的微服務化后,一般可以使用 Spring Cloud Security 結合 OAuth2.0,生成的 Token 采用 JWT 來驗證票據,但 Spring Cloud Security 暫時還不支持 OpenID Connect 協議。Zuul 將自己注冊為 Eureka 服務治理下,同時也從 Eureka 服務治理中獲得所有其他微服務的實例信息。通過搭建獨立的 OAuth2 認證授權服務,將微服務單獨剝離出來,這些認證與微服務自己的業務並沒有太大的關系,所以這些功能完全可以獨立成一個單獨的服務存在。獨立出來之后,並不是給每個微服務調用(業務服務一般在內網),而是通過 API網關進行統一調用,來對微服務接口做前置過濾,實現對分布式系統中的其他的微服務接口的攔截和安全校驗。
創建 Zuul 網關服務
Maven
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
@SpringBootApplication //@EnableOAuth2Sso @EnableZuulProxy public class MicrosrvZuulGatewayApplication { public static void main(String[] args) { SpringApplication.run(MicrosrvZuulGatewayApplication.class, args); } }
application.yml
spring: application: name: microsrv-zuul-gateway server: port: 5555 eureka: instance: preferIpAddress: true client: serviceUrl: defaultZone: http://10.255.131.162:8000/eureka/,http://10.255.131.163:8000/eureka/,http://10.255.131.164:8000/eureka/ zuul: host: connect-timeout-millis: 20000 socket-timeout-millis: 20000 ignoredServices: '*' prefix: /api # 設置一個公共的前綴 routes: auth-service: path: /auth/** sensitiveHeaders: serviceId: idsrv-server order-service: path: /order/** sensitiveHeaders: serviceId: order-service add-proxy-headers: true
因為使用 Eureka 來服務發現,所以請求URL格式形如 /service-id/** 會被自動轉發到在 Eureka Server 上注冊的 service id 為“service-id”的微服務應用上。例如上面我們定義了兩個路由規則,比如將“order-service”的請求轉發到相應 service-id 注冊的服務上,也可以通過修改 zuul.prefix=/api 配置來配置全局的前綴地址。默認 Eureka Server會暴露所有注冊在它上面的微服務。你可以使用 zuul.ignored-services 屬性來禁止這種行為,且只有顯式配置的服務才會被暴露。
Zuul 整合 OAuth2.0 認證授權
Zuul 整合 OAuth2.0 有兩種思路,一種是授權服務器采用 JwtToken 統一在網關層使用公鑰驗證票據,判斷權限等操作;另一種是讓資源端處理,網關只做路由轉發。
資源端配置
maven
<!-- oauth2 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.0.5.RELEASE</version> </dependency>
Spring Boot
@SpringBootApplication @EnableResourceServer public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } @RestController public class AccountController { @GetMapping("/principal") @PreAuthorize("hasAnyAuthority('user')") public Principal user(Principal principal) { return principal; } @GetMapping("/query") @PreAuthorize("hasAnyAuthority('all')") public String all () { return "具有 all 權限"; } }
application.yml
logging: level: org.springframework: DEBUG server: port: 5000 security: oauth2: resource: # prefer-token-info: true # user-info-uri: http://localhost:8080/api/v1/users/principal # token-info-uri: http://localhost:8080/oauth/check_token jwt: # key-uri: http://localhost:8080/oauth/token_key key-value: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4irSNcR7CSSfXconxL4 g4M4j34wTWdTv93ocMn4VmdB7rCBU/BlxXtBUf/cgLIgQhQrAPszSZSmxiEXCOkG Pr4aQBQuPgmNIR95Dhbzw/ZN0BnecAt3ZfkkDBHv8kH3kR/jYGTdwrxKeDgXGljN sTRhbjuASxPG/Z6gU1yRPCsgc2r8NYnztWGcDWqaobqjG3/yzFmusoAboyV7asIp o4yk378LmonDNwxnOOTb2Peg5PeelwfOwJPbftK1VOOt18zA0cchw6dHUzq9NlB8 clps/VdBap9BxU3/0YoFXRIc18nyzrWo2BcY2KQqX//AJC3OAfrfDmo+BGK8E0mp 8wIDAQAB -----END PUBLIC KEY-----
最后可以在 Zuul 上啟用 @EnableOAuth2Sso 注解作為 OAuth2.0 的一個客戶端(非必須),這樣當用戶訪問到網關沒有授權的話,會跳轉到授權服務器登錄授權。
security: oauth2: client: access-token-uri:http://localhost:8080/oauth/token user-authorization-uri: http://localhost:8080/oauth/authorize client-id: client_test client-secret: secret_test resource: user-info-uri: http://localhost:8080/api/v1/users/principal prefer-token-info: false
REFER:
https://docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/



