Spring Cloud保護服務
1.概述
在上一篇文章Spring Cloud – Bootstrapping中,我們構建了一個基本的Spring Cloud
應用程序。本文介紹瞭如何保護它。
我們自然會使用Spring Security
通過Spring Session
和Redis
共享會話。此方法易於設置,並且易於擴展到許多業務場景。
共享會話使我們能夠將用戶登錄到我們的網關服務中,並將該身份驗證傳播到我們系統的任何其他服務。
如果您不熟悉Redis or
Spring Security
,那麼此時最好快速查看這些主題。儘管本文的大部分內容都已準備好用於應用程序的複制粘貼,但沒有什麼可以替代了解幕後的情況了。
有關Redis
的介紹,請閱讀本教程。有關Spring Security
的介紹,請閱讀spring-security-login,spring-security-registration的角色和特權以及spring-security-session。
2. Maven安裝
首先,將spring-boot-starter-security依賴項添加到系統中的每個模塊:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
因為我們使用Spring
依賴管理,所以我們可以省略spring-boot-starter
依賴的版本。
第二步,讓我們使用spring-session , spring-boot-starter-data-redis依賴項修改每個應用程序的pom.xml
:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
我們只有四個應用程序可以與Spring Session
:發現,網關,預訂服務和評級服務。
接下來,在與主應用程序文件相同的目錄中的所有三個服務中添加會話配置類:
@EnableRedisHttpSession
public class SessionConfig
extends AbstractHttpSessionApplicationInitializer {
}
最後,將這些屬性添加到我們的git存儲庫中的三個*.properties
文件中:
spring.redis.host=localhost
spring.redis.port=6379
現在,讓我們進入特定於服務的配置。
3.保護配置服務
config服務包含通常與數據庫連接和API密鑰相關的敏感信息。我們無法破壞這些信息,因此讓我們深入研究並確保此服務的安全。
讓我們將安全屬性添加到config服務的src/main/resources
中的application.properties
文件中:
eureka.client.serviceUrl.defaultZone=
http://discUser:discPassword@localhost:8082/eureka/
security.user.name=configUser
security.user.password=configPassword
security.user.role=SYSTEM
這將設置我們的服務以發現登錄。另外,我們正在使用application.properties
文件配置安全性。
現在讓我們配置發現服務。
4.保護髮現服務
我們的發現服務保存有關應用程序中所有服務位置的敏感信息。它還會註冊這些服務的新實例。
如果惡意客戶端獲得訪問權限,他們將了解我們系統中所有服務的網絡位置,並能夠將自己的惡意服務註冊到我們的應用程序中。發現服務的安全至關重要。
4.1。安全配置
讓我們添加一個安全過濾器來保護其他服務將使用的端點:
@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.inMemoryAuthentication().withUser("discUser")
.password("discPassword").roles("SYSTEM");
}
@Override
protected void configure(HttpSecurity http) {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and().requestMatchers().antMatchers("/eureka/**")
.and().authorizeRequests().antMatchers("/eureka/**")
.hasRole("SYSTEM").anyRequest().denyAll().and()
.httpBasic().and().csrf().disable();
}
}
這將為“ SYSTEM
”用戶設置我們的服務。這是基本的Spring Security
配置,有一些變化。讓我們看一下這些曲折:
-
@Order(1)
–告訴Spring
先連接此安全過濾器,以便在其他任何過濾器之前對其進行嘗試 -
.sessionCreationPolicy
–告訴Spring
在用戶登錄此過濾器時始終創建一個會話 -
.requestMatchers
–限制此過濾器適用於哪些端點
我們剛剛設置的安全篩選器配置了一個僅與發現服務有關的隔離身份驗證環境。
4.2 保護Eureka Dashboard
由於我們的發現應用程序具有一個不錯的UI來查看當前註冊的服務,因此讓我們使用第二個安全過濾器進行公開,並將此過濾器與其餘應用程序的身份驗證相關聯。請記住,沒有@Order()
標記意味著這是最後一個要評估的安全過濾器:
@Configuration
public static class AdminSecurityConfig
extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and().httpBasic().disable().authorizeRequests()
.antMatchers(HttpMethod.GET, "/").hasRole("ADMIN")
.antMatchers("/info", "/health").authenticated().anyRequest()
.denyAll().and().csrf().disable();
}
}
在SecurityConfig
類中添加此配置類。這將創建第二個安全過濾器,該過濾器將控制對我們UI的訪問。此過濾器具有一些不尋常的特徵,讓我們看一下:
-
httpBasic().disable()
–告訴Spring Security禁用此過濾器的所有身份驗證過程 -
sessionCreationPolicy
–我們將其設置為NEVER
以表示我們要求用戶在訪問此過濾器保護的資源之前已經通過身份驗證
此過濾器永遠不會設置用戶會話,而是依靠Redis
來填充共享的安全上下文。這樣,它依賴於另一個服務(網關)來提供身份驗證。
4.3。使用配置服務進行身份驗證
在發現項目中,讓我們在src / main / resources中的bootstrap.properties
中添加兩個屬性:
spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
這些屬性將使發現服務在啟動時通過config服務進行身份驗證。
讓我們在Git存儲庫中更新discovery.properties
eureka.client.serviceUrl.defaultZone=
http://discUser:[email protected]:8082/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
我們已經在發現服務中添加了基本身份驗證憑據,以使其能夠與config服務進行通信。此外,我們通過告訴我們的服務不要自行註冊來配置Eureka
以獨立模式運行。
讓我們將文件提交到git
倉庫。否則,將不會檢測到更改。
5.保護網關服務
我們的網關服務是我們要公開給我們的應用程序的唯一部分。因此,將需要安全性以確保只有經過身份驗證的用戶才能訪問敏感信息。
5.1。安全配置
讓我們創建一個類似於我們的發現服務的SecurityConfig
類,並使用以下內容覆蓋方法:
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.inMemoryAuthentication().withUser("user").password("password")
.roles("USER").and().withUser("admin").password("admin")
.roles("ADMIN");
}
@Override
protected void configure(HttpSecurity http) {
http.authorizeRequests().antMatchers("/book-service/books")
.permitAll().antMatchers("/eureka/**").hasRole("ADMIN")
.anyRequest().authenticated().and().formLogin().and()
.logout().permitAll().logoutSuccessUrl("/book-service/books")
.permitAll().and().csrf().disable();
}
此配置非常簡單。我們使用表單登錄聲明安全過濾器,以保護各種端點。
/ eureka / **上的安全性是為了保護一些靜態資源,這些資源我們將通過Eureka
狀態頁面的網關服務提供。如果要使用本文來構建項目,請將resource/static
文件夾從Github上的網關項目複製到您的項目。
現在,我們在配置類中修改@EnableRedisHttpSession
批註:
@EnableRedisHttpSession(
redisFlushMode = RedisFlushMode.IMMEDIATE)
我們將刷新模式設置為即時,以立即保留會話中的所有更改。這有助於準備身份驗證令牌以進行重定向。
最後,讓我們添加一個ZuulFilter
,它將在登錄後轉發身份驗證令牌:
@Component
public class SessionSavingZuulPreFilter
extends ZuulFilter {
@Autowired
private SessionRepository repository;
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpSession httpSession = context.getRequest().getSession();
Session session = repository.getSession(httpSession.getId());
context.addZuulRequestHeader(
"Cookie", "SESSION=" + httpSession.getId());
return null;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
}
此過濾器將在登錄後重定向請求時抓取該請求,並將會話密鑰作為cookie添加到標頭中。登錄後,這會將身份驗證傳播到任何支持服務。
5.2。使用配置和發現服務進行身份驗證
讓我們將以下身份驗證屬性添加到網關服務的src/main/resources
中的bootstrap.properties
文件中:
spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
http://discUser:discPassword@localhost:8082/eureka/
接下來,讓我們在Git存儲庫中更新gateway.properties
management.security.sessions=always
zuul.routes.book-service.path=/book-service/**
zuul.routes.book-service.sensitive-headers=Set-Cookie,Authorization
hystrix.command.book-service.execution.isolation.thread
.timeoutInMilliseconds=600000
zuul.routes.rating-service.path=/rating-service/**
zuul.routes.rating-service.sensitive-headers=Set-Cookie,Authorization
hystrix.command.rating-service.execution.isolation.thread
.timeoutInMilliseconds=600000
zuul.routes.discovery.path=/discovery/**
zuul.routes.discovery.sensitive-headers=Set-Cookie,Authorization
zuul.routes.discovery.url=http://localhost:8082
hystrix.command.discovery.execution.isolation.thread
.timeoutInMilliseconds=600000
我們添加了會話管理以始終生成會話,因為我們只有一個安全過濾器,可以在屬性文件中進行設置。接下來,我們添加Redis
主機和服務器屬性。
此外,我們添加了一條路由,該路由會將請求重定向到我們的發現服務。由於獨立的發現服務不會自行註冊,因此必須使用URL方案定位該服務。
我們可以從配置git存儲庫中的gateway.properties
文件中刪除serviceUrl.defaultZone
屬性。此值在bootstrap
文件中重複。
讓我們將文件提交到Git存儲庫,否則,將不會檢測到更改。
6. 保護圖書服務
圖書服務服務器將保存由各種用戶控制的敏感信息。必須對該服務進行保護,以防止我們系統中的受保護信息洩漏。
6.1。安全配置
為了保護我們的圖書服務,我們將從網關複製SecurityConfig
類,並使用以下內容覆蓋方法:
@Override
protected void configure(HttpSecurity http) {
http.httpBasic().disable().authorizeRequests()
.antMatchers("/books").permitAll()
.antMatchers("/books/*").hasAnyRole("USER", "ADMIN")
.authenticated().and().csrf().disable();
}
6.2。Properties
將這些屬性添加到預訂服務的src/main/resources
中的bootstrap.properties
文件中:
spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
http://discUser:discPassword@localhost:8082/eureka/
讓我們將屬性添加到git存儲庫中的book-service.properties
文件中:
management.security.sessions=never
我們可以從配置git存儲庫中的book-service.properties
文件中刪除serviceUrl.defaultZone
屬性。此值在bootstrap
文件中重複。
請記住進行這些更改,以便預訂服務將其提取出來。
7. 保護評級服務
評級服務也需要得到保證。
7.1。安全配置
為了保護我們的評級服務,我們將從網關複製SecurityConfig
類,並使用以下內容覆蓋方法:
@Override
protected void configure(HttpSecurity http) {
http.httpBasic().disable().authorizeRequests()
.antMatchers("/ratings").hasRole("USER")
.antMatchers("/ratings/all").hasAnyRole("USER", "ADMIN").anyRequest()
.authenticated().and().csrf().disable();
}
我們可以從網關服務中刪除configureGlobal()
方法。
7.2 Properties
將這些屬性添加到評級服務的src/main/resources
中的bootstrap.properties
文件中:
spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
http://discUser:discPassword@localhost:8082/eureka/
讓我們將屬性添加到git存儲庫中的rating-service .properties
文件中:
management.security.sessions=never
我們可以從配置git存儲庫中的rating-service .properties
文件中刪除serviceUrl.defaultZone
屬性。此值在bootstrap
文件中重複。
記住要進行這些更改,以便評分服務將其選中。
8.運行和測試
啟動Redis
以及該應用程序的所有服務: config,發現,**網關,book-service和rating-service** 。現在讓我們測試!
首先,讓我們在網關項目中創建一個測試類,並為我們的測試創建一個方法:
public class GatewayApplicationLiveTest {
@Test
public void testAccess() {
...
}
}
接下來,讓我們進行測試,並通過在測試方法內添加以下代碼段來驗證是否可以訪問不受保護的/book-service/books
資源:
TestRestTemplate testRestTemplate = new TestRestTemplate();
String testUrl = "http://localhost:8080";
ResponseEntity<String> response = testRestTemplate
.getForEntity(testUrl + "/book-service/books", String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());
運行此測試並驗證結果。如果我們看到失敗,請確認整個應用程序已成功啟動,並且已從配置git存儲庫中加載了配置。
現在,通過將以下代碼附加到測試方法的末尾,來測試當我們以未經身份驗證的用戶身份訪問受保護資源時,將重定向用戶以登錄:
response = testRestTemplate
.getForEntity(testUrl + "/book-service/books/1", String.class);
Assert.assertEquals(HttpStatus.FOUND, response.getStatusCode());
Assert.assertEquals("http://localhost:8080/login", response.getHeaders()
.get("Location").get(0));
再次運行測試,並確認測試成功。
接下來,讓我們實際登錄,然後使用會話訪問用戶保護的結果:
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("username", "user");
form.add("password", "password");
response = testRestTemplate
.postForEntity(testUrl + "/login", form, String.class);
現在,讓我們從cookie中提取會話並將其傳播到以下請求:
String sessionCookie = response.getHeaders().get("Set-Cookie")
.get(0).split(";")[0];
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
HttpEntity<String> httpEntity = new HttpEntity<>(headers);
並請求受保護的資源:
response = testRestTemplate.exchange(testUrl + "/book-service/books/1",
HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());
再次運行測試以確認結果。
現在,讓我們嘗試使用相同的會話訪問admin部分:
response = testRestTemplate.exchange(testUrl + "/rating-service/ratings/all",
HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());
再次運行測試,並且正如預期的那樣,我們被限制以普通老用戶身份訪問管理區域。
下一個測試將驗證我們可以以管理員身份登錄並訪問受管理員保護的資源:
form.clear();
form.add("username", "admin");
form.add("password", "admin");
response = testRestTemplate
.postForEntity(testUrl + "/login", form, String.class);
sessionCookie = response.getHeaders().get("Set-Cookie").get(0).split(";")[0];
headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
httpEntity = new HttpEntity<>(headers);
response = testRestTemplate.exchange(testUrl + "/rating-service/ratings/all",
HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());
我們的測試越來越大!但是我們可以看到,以管理員身份登錄時,可以訪問管理員資源。
我們的最終測試是通過網關訪問發現服務器。為此,請將此代碼添加到測試的末尾:
response = testRestTemplate.exchange(testUrl + "/discovery",
HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
最後一次運行此測試以確認一切正常。成功!!!
你想念嗎?因為我們登錄了網關服務並查看了我們的圖書,評分和發現服務上的內容,而無需登錄四個單獨的服務器!
通過利用Spring Session
在服務器之間傳播我們的身份驗證對象,我們能夠一次在網關上登錄並使用該身份驗證來訪問任意數量的後備服務上的控制器。
9.結論
雲中的安全性無疑變得更加複雜。但是在Spring Security
和Spring Session
的幫助下,我們可以輕鬆解決此關鍵問題。
現在,我們有了一個具有圍繞我們的服務的安全性的雲應用程序。使用Zuul
和Spring Session
我們只能將用戶記錄在一項服務中,並將身份驗證傳播到我們的整個應用程序。這意味著我們可以輕鬆地將應用程序劃分為適當的域,並在我們認為合適的情況下保護每個域。
與往常一樣,您可以在GitHub上找到源代碼。