目前整個 gossip 架構上,服務發現伺服器用來查找閘道服務,而服務請求基本上是透過閘道來轉發,然而,閘道並沒有限制哪個客戶端才能請求,怎麼辦?
在〈簡介 ZuulFilter〉中就示範過,可以在閘道上實現過濾器,檢查客戶端是否具有某個 Token,來決定是否轉發請求,當然,這也可以在 Spring Cloud Gateway 中實現,只不過,既然之前談過 Spring Security,不如直接來利用如何?
例如要求 gossip 的 Web 客戶端,必須提供名稱 webclient
與密碼 webclient12345678
,才可以請求閘道,因為現在 gossip 相關服務的組態,都是透過組態伺服器取得,為此,可以在 Git 伺服器上,新增 gateway.properties:
client.web.name={cipher}c9da2f0b483a92871ce26868cc7241e80f66d389ea477c9b839d28adcea1dc85
client.web.secret={cipher}2bda430893e6ee53354320ba8bb2c835508d56faecac03b99e937f4f0d427559517c34c0927768e7ab723350407cffbe
client.web.name
與 client.web.secret
的值是加密過的 webclient
與 webclient12345678
(加解密可參考〈Git 組態來源〉),為了能讀取 gateway.properties,組態伺服器 configsvr 的 application.properties 在 spring.cloud.config.server.git.searchPaths
增加了 gossip-services/gateway
:
spring.cloud.config.server.git.uri=https://github.com/JustinSDK/cloud-config-demo
spring.cloud.config.server.git.searchPaths=gossip-services/gateway,gossip-services/emailsvi,gossip-services/msgsvi,gossip-services/acctsvi
接下來處理閘道,在這邊以〈使用 Spring Cloud Gateway〉為例,Gateway 為了要能取得組態伺服器資訊,以及使用 Spring Security,build.gradle 加入了兩個 Starter:
implementation('org.springframework.cloud:spring-cloud-starter-config')
implementation("org.springframework.boot:spring-boot-starter-security")
然後,bootstrap.properties 設定服務發現伺服器為組態來源:
server.port=5555
spring.profiles.active=default
spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=configsvr
eureka.instance.preferIpAddress=true
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
management.endpoints.web.exposure.include: gateway
接著,要設定頁面防護以及允許請求的名稱、密碼,這定義在 GatewayApplication
:
package cc.openhome;
...略
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and()
.httpBasic()
.and()
.csrf().disable()
.build();
}
@Value("${client.web.name}")
private String clientName;
@Value("${client.web.secret}")
private String clientSecret;
@Bean
public ReactiveUserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails webclient = User
.withUsername(clientName)
.password(passwordEncoder.encode(clientSecret))
.roles("account", "message", "email")
.build();
return new MapReactiveUserDetailsService(webclient);
}
}
在這邊設定了 PasswordEncoder
,並要求任何請求都得通過驗證,這邊使用 BASIC 驗證(原理請參考〈宣告式基本驗證〉),Spring Security 預設會啟用 CSRF 防護,然而在使用 BASIC 驗證下,因為沒有表單,也就不會有 CSRF Token,因此停用 CSRF 防護。
名稱、密碼部份自動綁定為 client.web.name
與 client.web.secret
,並且注意到,Spring Cloud Gateway 是跑在 Netty 上,因此在這邊的設定方式是採用〈Security Reactive〉。
目前來說,只有一個 web 客戶端,因此簡單地採用 MapReactiveUserDetailsService
處理就可以了,之後若有需求再來考量資料庫的使用。
完成以上之後,可以啟動相關服務與閘道,對閘道發出請求的話,會出現對話方塊要求進行驗證:
現在可以處理 gossip 了,為了能取得組態資訊,在 bootstrap.properties 增加了組態發現的相關設定:
spring.application.name=gossip
spring.profiles.active=default
eureka.client.registerWithEureka=false
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=configsvr
spring.cloud.config.name=gateway
在這邊要留意的是,因為需要的是 gateway.properties 的組態,要使用 spring.cloud.config.name
指定,不然會自動使用 spring.application.name
去抓組態。
剩下的問題是,gossip 使用 Feign,要怎麼讓它支援 BASIC 驗證呢?在 Spring 的整合之下,只要加個 Bean 就可以了:
package cc.openhome.gossip;
...略
public class GossipApplication {
...略
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor(
@Value("${client.web.name}") String clientName,
@Value("${client.web.secret}") String clientSecret) {
return new BasicAuthRequestInterceptor(clientName, clientSecret);
}
}
BasicAuthRequestInterceptor
實例包含了名稱、密碼,來源是組態中的 client.web.name
與 client.web.secret
,Feign 就會自動處理 BASIC 驗證,接著就可以啟動應用程式來測試看看了。
雖然閘道已經使用 BASIC 驗證,限制只有通過驗證的客戶端才能請求,不過,Mail 服務、Account 服務、Message 服務,仍然是處於完全開放的狀態,或許你可以將之隱藏在某個內部網路,然而,內部網路也有被內部開發者誤用的可能性,最好的方式是也對 Mail 服務、Account 服務、Message 服務施以驗證授權。
不過,每一個服務都得設定驗證,就表示每一個服務都得銜接資料庫(這邊的範例只是為了簡化才使用記憶體中的資料庫),每次請求就得撈取資料庫資料來進行驗證,這是一大負擔。
若可以將驗證與授權分開,曾經通過驗證的客戶端,只要在請求服務時提出合法令牌,就可以存取資源的話,就可以解決這類問題,而這就是 OAuth 2 登場的時候了,這在後續文件中會說明,因此在這邊,就不繼續對 Mail 服務、Account 服務、Message 服務設定 Spring Security 了。
至於 Zuul 與 Spring Security 的整合,這邊並沒有說明,別擔心,因為就撰寫文件的這個時間點來說,Spring Cloud Gateway 還沒有與 Spring 的 OAuth 2 支援整合,後續就會使用 Zuul 閘道來整合 OAuth 2,到時就會看到一些示範了。
你可以在 GatewaySecurity 中找到以上的範例專案,為了方便整個 gossip 架構設定之參考,其中也放進了相關聯的各個專案。