本章主要內容
瞭解安全在微服務環境中的重要性
認識OAuth2標準
創建和配置基於Spring的OAuth2服務
使用OAuth2執行用戶驗證和受權
使用OAuth2保護Spring微服務
在服務之間傳播OAuth2訪問令牌
提到「安全」這個詞每每會引發開發人員情不自禁地痛苦沉吟。你會聽到他們咕噥着低聲詛咒:「它遲鈍,難以理解,甚至是很難調試。」然而,沒有任何開發人員(除了那些沒有經驗的開發人員)會說他們不擔憂安全問題。
一個安全的應用程序涉及多層保護,包括:
確保有正確的用戶控制,以即可以確認用戶是他們所說的人,而且他們有權執行正在嘗試執行的操做;
保持運行服務的基礎設施是打過補丁且最新的,以讓漏洞的風險最低;
實現網絡訪問控制,讓少許已受權的服務器可以訪問服務,並使服務只能經過定義良好的端口進行訪問。
本章只討論上述列表中的第一個要點:如何驗證調用微服務的用戶是他們所說的人,並肯定他們是否被受權執行他們從微服務中請求的操做。另外兩個主題是很是寬泛的安全主題,超出了本書的範圍。
要實現驗證和受權控制,咱們將使用Spring Cloud Security和OAuth2(Open Authentication)標準來保護基於Spring的服務。OAuth2是一個基於令牌的安全框架,容許用戶使用第三方驗證服務進行驗證。若是用戶成功進行了驗證,則會出示一個令牌,該令牌必須與每一個請求一塊兒發送。而後,驗證服務能夠對令牌進行確認。OAuth2背後的主要目標是,在調用多個服務來完成用戶請求時,用戶不須要在處理請求的時候爲每一個服務都提供本身的憑據信息就能完成驗證。Spring Boot和Spring Cloud都提供了開箱即用的OAuth2服務實現,使OAuth2安全可以很是容易地集成到服務中。
注意
本章將介紹如何使用OAuth2保護微服務。不過,一個成熟的OAuth2實現還須要一個前端Web應用程序來輸入用戶憑據。本章不會討論如何創建前端應用程序,由於這已經超出了本書關於微服務的範圍。做爲代替,本章將使用REST客戶端(如POSTMAN)來模擬憑據的提交。有關如何配置前端應用程序,我建議讀者查看如下Spring教程:
https://spring.io/blog/2015/02/03/sso- with-oauth2-angular-js-and-spring-security-part-v。
OAuth2背後真正的強大之處在於,它容許應用程序開發人員輕鬆地與第三方雲服務提供商集成,並使用這些服務進行用戶驗證和受權,而無須不斷地將用戶的憑據傳遞給第三方服務。像Facebook、GitHub和Salesforce這樣的雲服務提供商都支持將OAuth2做爲標準。
在討論使用OAuth2保護服務的技術細節以前,讓咱們先看看OAuth2架構。
7.1 OAuth2簡介
OAuth2是一個基於令牌的安全驗證和受權框架,它將安全性分解爲如下4個組成部分。
(1)受保護資源——這是開發人員想要保護的資源(在咱們的例子中是一個微服務),須要確保只有已經過驗證而且具備適當受權的用戶才能訪問它。
(2)資源全部者——資源全部者定義哪些應用程序能夠調用其服務,哪些用戶能夠訪問該服務,以及他們可使用該服務完成哪些事情。資源全部者註冊的每一個應用程序都將得到一個應用程序名稱,該應用程序名稱與應用程序密鑰一塊兒標識應用程序。應用程序名稱和密鑰的組合是在驗證OAuth2令牌時傳遞的憑據的一部分。
(3)應用程序——這是表明用戶調用服務的應用程序。畢竟,用戶不多直接調用服務。相反,他們依賴應用程序爲他們工做。
(4)OAuth2驗證服務器——OAuth2驗證服務器是應用程序和正在使用的服務之間的中間人。OAuth2驗證服務器容許用戶對本身進行驗證,而沒必要將用戶憑據傳遞給由應用程序表明用戶調用的每一個服務。
這4個組成部分互相做用對用戶進行驗證。用戶只需提交他們的憑據。若是他們成功經過驗證,則會出示一個驗證令牌,該令牌可在服務之間傳遞,如圖7-1所示。OAuth2是一個基於令牌的安全框架。針對OAuth2服務器,用戶經過提供憑據以及用於訪問資源的應用程序來進行驗證。若是用戶憑據是有效的,那麼OAuth2服務器就會提供一個令牌,每當用戶的應用程序使用的服務試圖訪問受保護的資源(微服務)時,就能夠提交這個令牌。
圖7-1 OAuth2容許用戶進行驗證,而沒必要持續提供憑據
接下來,受保護資源能夠聯繫OAuth2服務器以肯定令牌的有效性,並檢索用戶授予它們的角色。角色用於將相關用戶分組在一塊兒,並定義用戶組能夠訪問哪些資源。對於本章來講,咱們將使用OAuth2和角色來定義用戶能夠調用哪些服務端點,以及用戶能夠在端點上調用的HTTP動詞。
Web服務安全是一個極其複雜的主題。開發人員必須瞭解誰將調用本身的服務(公司網絡的內部用戶仍是外部用戶),他們將如何調用這些服務(是在內部基於Web客戶端、移動設備仍是在企業網絡以外的Web應用程序),以及他們用代碼來完成什麼操做。OAuth2容許開發人員使用稱爲受權(grant)的不一樣驗證方案,在不一樣的場景中保護基於REST的服務。OAuth2規範具備如下4種類型的受權(表示客戶端應用程序獲取用戶受權的4種方式,好比下面的受權碼在微信api和一個客戶端應用程序須要從QQ平臺獲取用戶信息):
密碼(password);
客戶端憑據(client credential);
受權碼(authorization code);
隱式(implicit)。
本書不會逐一介紹每種受權類型,或者爲每種受權類型提供代碼示例。究其緣由,僅僅是由於須要包含在一章裏的內容太多了。取而代之,本章將會完成如下事情:
討論微服務如何經過一個較簡單的OAuth2受權類型(密碼受權類型)來使用OAuth2;
使用JSON Web Token來提供一個更健壯的OAuth2解決方案,並在OAuth2令牌中創建一套信息編碼的標準;
介紹在構建微服務時須要考慮的其餘安全注意事項。
本書在附錄B中會提供其餘OAuth2受權類型的概述資料。若是讀者有興趣詳細瞭解OAuth2規範以及如何實現全部受權類型,強烈推薦Justin Richer和Antonio Sanso的著做《OAuth2 in Action》,這是對OAuth2的全面解讀。
7.2 從小事作起:使用Spring和OAuth2來保護單個端點
爲了瞭解如何創建OAuth2的驗證和受權功能,咱們將實現OAuth2密碼受權類型。要實現這一受權,咱們將執行如下操做。
創建一個基於Spring Cloud的OAuth2驗證服務。
註冊一個僞EagleEye UI應用程序做爲一個已受權的應用程序,它能夠經過OAuth2服務驗證和受權用戶身份。
使用OAuth2密碼受權來保護EagleEye服務。咱們不會爲EagleEye構建UI,而是使用POSTMAN模擬登陸的用戶對EagleEye OAuth2服務進行驗證。
保護許可證服務和組織服務,使它們只能被已經過驗證的用戶調用。
7.2.1 創建EagleEye OAuth2驗證服務
就像本書中全部的例子同樣,OAuth2驗證服務將是另外一個Spring Boot服務。驗證服務將驗證用戶憑據並頒發令牌。每當用戶嘗試訪問由驗證服務保護的服務時,驗證服務將確認OAuth2令牌是否已由其頒發而且還沒有過時。這裏的驗證服務等同於圖7-1中的驗證服務。
開始時,須要完成如下兩件事。
(1)添加引導類所需的適當Maven構建依賴項。
(2)添加一個將做爲服務的入口點的引導類。
讀者能夠在authentication-service目錄中找到驗證服務的全部代碼示例。要創建OAuth2驗證服務器,須要在authentication-service/pom.xml文件中添加如下Spring Cloud依賴項:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>
spring-cloud-security
</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
第一個依賴項spring-cloud-security引入了通用Spring和Spring Cloud安全庫。第二個依賴項spring-security-oauth2拉取了SpringOAuth2庫。
既然已經定義完Maven依賴項,那麼就能夠在引導類上進行工做。這個引導類能夠在authentication-service/src/main/java/com/thoughtmechanix
/authentication/Application.java中找到。代碼清單7-1展現Application類的代碼。
代碼清單7-1 authentication-service的引導類
// 爲了簡潔,省略了import語句
@SpringBootApplication
@RestController
@EnableResourceServer
⇽--- 用於告訴Spring Cloud,該服務將做爲OAuth2服務
@EnableAuthorizationServer
public class Application {
⇽--- 在本章稍後用於檢索有關用戶的信息
@RequestMapping(value = { "/user" }, produces = "application/json")
public Map<String, Object> user(OAuth2Authentication user) {
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
return userInfo;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
在代碼清單7-1中,要注意的第同樣東西是@EnableAuthorizationServer註解。這個註解告訴Spring Cloud,該服務將用做OAuth2服務,並添加幾個基於REST的端點(自動添加的/oauth端點和類裏手動添加的/user端點),這些端點將在OAuth2驗證和受權過程當中使用。
在代碼清單7-1中,看到的第二件事是添加了一個名爲/user(映射到/auth/user)的端點。當試圖訪問由OAuth2保護的服務時,將會用到這個端點,本章後文會進行介紹。此端點由受保護服務調用,以確認OAuth2訪問令牌,並檢索訪問受保護服務的用戶所分配的角色(確認訪問令牌的有效性和獲取訪問用戶的角色名)。本章稍後會詳細討論這個端點。
7.2.2 使用OAuth2服務註冊客戶端應用程序
此時,咱們已經有了一個驗證服務,但還沒有在驗證服務器中定義任何應用程序、用戶或角色。咱們能夠從已經過驗證服務註冊EagleEye應用程序開始(這句話的意思是從已經在驗證服務上註冊的EagleEye應用程序開始。爲此,咱們將在驗證服務中建立一個名爲OAuth2Config的類(在authentication-service/src/main/java/com/thoughtmechanix/authentication/ security/OAuth2Config.java中)。
這個類將定義經過OAuth2驗證服務註冊哪些應用程序(在OAuth2驗證服務裏註冊哪些應用程序)。須要注意的是,不能只由於應用程序經過OAuth2服務中註冊過,就認爲該服務可以訪問任何受保護資源。
驗證與受權
我常常發現開發人員混淆術語驗證(authentication)和受權(authorization)的含義。驗證是用戶經過提供憑據來證實他們是誰的行爲。受權決定是否容許用戶作他們想作的事情。例如,Jim能夠經過提供用戶ID和密碼來證實他的身份,可是他可能沒有被受權查看敏感數據,如工資單數據。出於咱們討論的目的,必須在受權發生以前對用戶進行驗證。
OAuth2Config類定義了OAuth2服務知道的應用程序和用戶憑據。在代碼清單7-2中能夠看到OAuth2Config類的代碼。
代碼清單7-2 OAuth2Config服務定義哪些應用程序可使用服務(具體客戶端應用程序可以使用哪些服務是由用戶受權的,而即便用戶受權了,若是用戶自己對該服務沒有權限,則依然客戶端應用程序操做失敗)。
// 爲了簡潔,省略了import語句
⇽--- 繼承AuthorizationServerConfigurerAdapter類,並使用@Configuration註解標註這個類
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
⇽--- 覆蓋configure()方法。這定義了哪些客戶端將註冊到驗證服務
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("eagleeye")
.secret("thisissecret")
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
.scopes("webclient", "mobileclient");
}
⇽--- 該方法定義了AuthenticationServerConfigurer中使用的不一樣組件。
這段代碼告訴Spring使用Spring提供的默認驗證管理器和用戶詳細信息服務
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
在代碼清單7-2所示的代碼中,要注意的第一件事是,這個類擴展了Spring的AuthenticationServerConfigurer類,而後使用@Configuration註解對這個類進行了標記。AuthenticationServerConfigurer類是Spring Security的核心部分,它提供了執行關鍵驗證和受權功能的基本機制。對於OAuth2Config類,咱們將要覆蓋兩個方法。第一個方法是configure(),它用於定義經過驗證服務註冊了哪些客戶端應用程序。configure()方法接受一個名爲clients的ClientDetailsServiceConfigurer類型的參數。讓咱們來更詳細地瞭解一下configure()方法中的代碼。在這個方法中作的第一件事是註冊哪些客戶端應用程序容許訪問由OAuth2服務保護的服務。這裏使用了最普遍的術語「訪問」(access),由於咱們經過檢查調用服務的用戶是否有權採起他們正在嘗試的操做,控制了客戶端應用程序的用戶之後能夠作什麼。
clients.inMemory() .withClient("eagleeye") .secret("thisissecret") .authorizedGrantTypes("password","client_credentials") .scopes("webclient","mobileclient");
對於應用程序的信息,ClientDetailsServiceConfigurer類支持兩種不一樣類型的存儲:內存存儲和JDBC存儲。對本例來講,咱們將使用clients.inMemory()存儲。
withClient()和secret()這兩個方法提供了註冊的應用程序的名稱(eagleeye)以及密鑰(一個密碼,thisissecret),該密鑰在EagleEye應用程序調用OAuth2服務器以接收OAuth2訪問令牌時提供。
下一個方法是authorizedGrantTypes(),它被傳入一個以逗號分隔的受權類型列表,這些受權類型將由OAuth2服務支持。在這個服務中,咱們將支持密碼受權類型和客戶端憑據受權類型。
scopes()方法用於定義調用應用程序在請求OAuth2服務器獲取訪問令牌時能夠操做的範圍(也就是定義該應用程序可以在哪些場景裏操做,好比手機端仍是pc端)。例如,ThoughtMechanix可能提供同一應用程序的兩個不一樣版本:基於Web的應用程序和基於手機的應用程序。在這些應用程序中均可以使用相同的客戶端名稱和密鑰來請求對OAuth2服務器保護的資源的訪問。然而,當應用程序請求一個密鑰時,它們須要定義它們所操做的特定做用域。經過定義做用域,能夠編寫特定於客戶端應用程序所工做的做用域的受權規則。
例如,可能有一個用戶使用基於Web的客戶端和手機應用程序來訪問EagleEye應用程序。EagleEye應用程序的每一個版本都:
(1)提供相同的功能;
(2)是一個「受信任的應用程序」,ThoughtMechanix既擁有前端應用程序,也擁有終端用戶服務。
所以,咱們將使用相同的應用程序名稱和密鑰來註冊EagleEye應用程序,可是Web應用程序只使用「webclient」做用域,而手機版本的應用程序則使用「mobileclient」做用域。經過使用做用域,能夠在受保護的服務中定義受權規則,該規則能夠根據登陸的應用程序限制客戶端應用程序能夠執行的操做。這與用戶擁有的權限無關。例如,咱們可能但願根據用戶是使用公司網絡中的瀏覽器,仍是使用移動設備上的應用程序進行瀏覽,來限制用戶能夠看到哪些數據。在處理敏感客戶信息(如健康記錄或稅務信息)時,基於數據訪問機制限制數據的作法是很常見的。
到目前爲止,咱們已經使用OAuth2服務器註冊了一個應用程序EagleEye。然而,由於使用的是密碼受權,因此須要在開始以前爲這些用戶建立用戶帳戶和密碼。
7.2.3 配置EagleEye用戶
咱們已經定義並存儲了應用程序級的密鑰名和密鑰。如今要建立我的用戶憑據及其所屬的角色。用戶角色將用於定義一組用戶能夠對服務執行的操做(角色是由平臺定義的,好比QQ,谷歌,oauth驗證服務和被保護的資源和服務都是引用平臺定義的角色)。
Spring能夠從內存數據存儲、支持JDBC的關係數據庫或LDAP服務器中存儲和檢索用戶信息(我的用戶的憑據和分配給用戶的角色)。
注意
我但願在定義上謹慎一些。Spring的OAuth2應用程序信息能夠存存儲在內存或關係數據庫中。Spring用戶憑據和安全角色能夠存儲在內存數據庫、關係數據庫或LDAP(活動目錄)服務器中。由於咱們的主要目的是學習OAuth2,爲了保持簡單,咱們將使用內存數據存儲。
對於本章中的代碼示例,咱們將使用內存數據存儲來定義用戶角色。咱們將定義兩個用戶帳戶,即john.carnell和william.woodward。john.carnell帳戶將擁有USER角色,而william.woodward帳戶將擁有ADMIN角色。
要配置OAuth2服務器以驗證用戶ID,必須建立一個新類WebSecurityConfigurer(在authentication-service/src/main/com/thoughtmechanix/authentication/security/WebSecurityConfigurer.java中)。
代碼清單7-3展現了這個類的代碼。
代碼清單7-3 爲應用程序定義用戶ID、密碼和角色
// 爲了簡潔,省略了import語句
⇽--- 擴展核心Spring Security的WebSecurityConfigurerAdapter
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
←Spring Security使用AuthenticationManagerBean來處理身份驗證。
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
⇽--- Spring Security使用UserDetailsService處理返回的用戶信息,這些用戶信息將由Spring Security返回
@Override
@Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
⇽--- configure()方法是定義用戶、密碼和角色的地方
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("john.carnell").password("password1").roles("USER")
.and()
.withUser("william.woodward").password("password2").roles("USER", "ADMIN");
}
}
像Spring Security框架的其餘部分同樣,要建立用戶(及其角色),要從擴展WebSecurityConfigurerAdapter類並使用@Configuration註解標記它開始。Spring Security的實現方式相似於將樂高積木搭在一塊兒來製造玩具車或模型。所以,咱們須要爲OAuth2服務器提供一種驗證用戶的機制,並返回正在驗證的用戶的用戶信息。這經過在Spring WebSecurityConfigurerAdapter實現中定義authenticationManagerBean()和userDetailsServiceBean()兩個bean來完成。這兩個bean經過使用父類WebSecurityConfigurerAdapter中的默認驗證方法authenticationManagerBean()和userDetailsServiceBean()方法來公開。
從代碼清單7-2中能夠看出,這些bean被注入到OAuth2Config類中的configure(AuthorizationServerEndpointsConfigurer endpoints)方法中。
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints .authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
咱們將在稍後的實戰中看到,這兩個bean用於配置/auth/oauth/token和/auth/user端點。
7.2.4 驗證用戶
首先,須要使用應用程序名稱和密鑰設置POSTMAN。咱們將使用基本驗證將這些元素傳遞到OAuth2服務器端點。圖7-2展現瞭如何設置POSTMAN來執行基本驗證調用。
圖7-2 使用應用程序名稱和密鑰設置基本驗證
可是,咱們尚未準備好執行調用來獲取令牌。一旦配置了應用程序名稱和密鑰,就須要在服務中傳遞如下信息做爲HTTP表單參數。
grant_type——正在執行的OAuth2受權類型。在本例中,將使用密碼(password)受權。
scope——應用程序做用域。由於咱們在註冊應用程序時只定義了兩個合法做用域(webclient和mobileclient),所以傳入的值必須是這兩個做用域之一。
username——用戶登陸的名稱。
password——用戶登陸的密碼。
與本書中的其餘REST調用不一樣,這個列表中的參數不會做爲JSON體傳遞。OAuth2標準指望傳遞給令牌生成端點的全部參數都是HTTP表單參數。
圖7-3展現瞭如何爲OAuth2調用配置HTTP表單參數。
在本地測試的時候,使用postman發送請求的界面以下:
應用程序客戶端的信息以下:
clients.inMemory()
.withClient("eagleeye")
.secret("thisissecret")
.authorizedGrantTypes("password", "client_credentials")
.scopes("webclient","mobileclient");
用戶的信息以下:
auth.inMemoryAuthentication()
.withUser("john.carnell")
.password("password1")
.roles("USER")
.and()
.withUser("william.woodward")
.password("password2")
.roles("USER", "ADMIN");
從上面的截圖能夠看出,Basic Auth一欄中的 Username和 Password字段是客戶端應用程序的信息,
這兩個字段是做爲header傳送的。
注意使用postman的時候,要選擇 x-www-form-urlencoded ,而不能選擇 form-data ,以下圖,不然服務端會報錯
2019-01-29 20:45:56.504 INFO 2436 --- [nio-8901-exec-8] o.s.s.o.provider.endpoint.TokenEndpoint : Handling error: InvalidRequestException, Missing grant type
在本地測試的時候,使用postman第一次發送請求會彈出登陸框,以下圖:
用戶名和密碼要填寫客戶端應用程序的用戶名和密碼,即:
eagleeye和thisissecret
後面又仔細看了下,在發送請求以前,須要點擊Basic Auth欄中的 Refresh headers 按鈕,點了以後,頁面以下所示:
header裏多了一項「Authorization」,它的值是base64編碼的。這樣就不會彈出登陸框了。
圖7-3 在請求OAuth2令牌時,用戶的憑據做爲HTTP表單參數傳入/auth/oauth/token端點
圖7-4展現了從/auth/oauth/token調用返回的JSON淨荷。
圖7-4 客戶端憑據成功確認後返回的淨荷
返回的淨荷包含如下5個屬性。
access_token——OAuth2令牌,它將隨用戶對受保護資源的每一個服務調用一塊兒出示。
token_type——令牌的類型。
OAuth2規範容許定義多個令牌類型,最經常使用的令牌類型是不記名令牌(bearer token)。本章不涉及任何其餘令牌類型。
refresh_token——包含一個能夠提交回OAuth2服務器的令牌,以便在訪問令牌過時後從新頒發一個訪問令牌。
expires_in——這是OAuth2訪問令牌過時前的秒數。在Spring中,受權令牌過時的默認值是12 h。
scope——此OAuth2令牌的有效做用域。
有了有效的OAuth2訪問令牌,就可使用驗證服務中建立的/auth/user端點來檢索與令牌相關聯的用戶的信息了(可使用該端點驗證OAuth2令牌是否有效和經過OAuth2令牌查找用戶的信息)。在本章的後面,全部受保護資源都將調用驗證服務的/auth/user端點來確認令牌並檢
索用戶信息。
圖7-5展現了調用/auth/user端點的結果。如圖7-5所示,注意OAuth2訪問令牌是如何做爲HTTP首部傳入的。
圖7-5 根據發佈的OAuth2令牌查找用戶信息
在圖7-5中,咱們對/auth/user端點發出HTTP GET請求。在任什麼時候候調用OAuth2保護的端點(包括OAuth2的/auth/user端點),都須要傳遞OAuth2訪問令牌。爲此,要始終建立一個名爲Authorization的HTTP首部,並附有Bearer XXXXX的值。在圖7-5所示的調用中,這個HTTP首部的值是Bearer e9decabc-165b-4677-9190-2e0bf8341e0b。傳入的訪問令牌是在圖7-4中調用/auth/oauth/token端點時返回的訪問令牌。
若是OAuth2訪問令牌有效,/auth/user端點就會返回關於用戶的信息,包括分配給他們的角色。例如,從圖7-5能夠看出,用戶john.carnell擁有USER角色。
注意
Spring將前綴ROLE_分配給用戶角色,所以ROLE_USER意味着john.carnell擁有USER角色。
7.3 使用OAuth2保護組織服務
一旦經過OAuth2驗證服務註冊了一個應用程序,而且創建了擁有角色的我的用戶帳戶,就能夠開始探索如何使用OAuth2來保護資源了。雖然建立和管理OAuth2訪問令牌是OAuth2服務器的職責,但在Spring中,定義哪些用戶角色有權執行哪些操做是在單個服務級別上發生的。
要建立受保護資源,須要執行如下操做:
將相應的Spring Security和OAuth2 jar添加到要保護的服務中;
配置服務以指向OAuth2驗證服務;
定義誰能夠訪問服務。
讓咱們從一個最簡單的例子開始,將組織服務建立爲受保護資源,並確保它只能由已經過驗證的用戶來調用。
7.3.1 將Spring Security和OAuth2 jar添加到各個服務
與一般的Spring微服務同樣,咱們必需要向組織服務的Maven organization-service/pom.xml文件添加幾個依賴項。在這裏,須要添加兩個依賴項:Spring Cloud Security和Spring Security OAuth2。Spring Cloud Security jar是核心的安全jar,它包含框架代碼、註解定義和用於在Spring Cloud中實現安全性的接口。Spring Security OAuth2依賴項包含實現OAuth2驗證服務所需的全部類。這兩個依賴項的Maven條目是:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
7.3.2 配置服務以指向OAuth2驗證服務
記住,一旦將組織服務建立爲受保護資源,每次調用服務時,調用者必須將包含OAuth2訪問令牌的 Authentication HTTP首部包含到服務中(調用者必須將OAuth2訪問令牌放到Authentication HTTP頭部中)。而後,受保護資源必須調用該OAuth2服務來查看令牌是否有效。
在組織服務的application.yml文件中以security.oauth2.resource.userInfoUri屬性定義回調URL。
下面是組織服務的application.yml文件中使用的回調配置:
security:
oauth2:
resource:
正如從security.oauth2.resource.userInfoUri屬性看到的,回調URL是/auth/user端點。這個端點在7.2.4節中討論過。
最後,還須要告知組織服務它是受保護資源。一樣,這一點能夠經過向組織服務的引導類添加一個Spring Cloud註解來實現。組織服務的引導類代碼如代碼清單7-4所示,它能夠在organization-service/src/main/java/com/thoughtmechanix/organization/Application.java中找到。
代碼清單7-4 將引導類配置爲受保護資源
// 爲了簡潔,省略了import語句
import org.springframework.security.oauth2.
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
⇽--- @EnableResourceServer註解用於告訴微服務,它是一個受保護資源
@EnableResourceServer
public class Application {
@Bean
public Filter userContextFilter() {
UserContextFilter userContextFilter = new UserContextFilter();
return userContextFilter;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@EnableResourceServer註解告訴Spring Cloud和Spring Security,該服務是受保護資源。@EnableResourceServer強制執行一個過濾器,該過濾器會攔截對該服務的全部傳入調用,檢查傳入調用的HTTP首部中是否存在OAuth2訪問令牌,而後調用security.oauth2.resource.userInfoUri中定義的回調URL來查看令牌是否有效。一旦獲悉令牌是有效的,@EnableResourceServer註解也會應用任何訪問控制規則,以控制什麼人能夠訪問服務。
7.3.3 定義誰能夠訪問服務
咱們如今已經準備好開始圍繞服務定義訪問控制規則了。要定義訪問控制規則,須要擴展 ResourceServerConfigurerAdapter類並覆蓋configure()方法。在組織服務中,ResourceServerConfiguration類位於organization service/src/main/java/com/thoughtmechanix/ organization/security/ResourceServerConfiguration.java。訪問規則的範圍能夠從極其粗粒度(任何已經過驗證的用戶均可以訪問整個服務)到很是細粒度(只有具備此角色的應用程序,才容許經過DELETE方法訪問此URL)。
咱們不會討論Spring Security訪問控制規則的各類組合,只是看一些更常見的例子。這些例子包括保護資源以便:
只有已經過驗證的用戶才能訪問服務URL;
只有具備特定角色的用戶才能訪問服務URL。
1.經過驗證用戶保護服務
接下來要作的第一件事就是保護組織服務,使它只能由已經過驗證的用戶訪問。代碼清單7-5展現瞭如何將此規則構建到ResourceServerConfiguration類中。
代碼清單7-5 限制只有已經過驗證的用戶能夠訪問
// 爲了簡潔,省略了import語句
⇽--- 這個類ResourceServiceConfiguration類須要擴展ResourceServerConfigurerAdapter
@Configuration
public class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
⇽--- 全部訪問規則都是在覆蓋的configure()方法中定義的
@Override
public void configure(HttpSecurity http) throws Exception{
⇽--- 全部訪問規則都是經過傳入方法的HttpSecurity對象配置的
http.authorizeRequests().anyRequest().authenticated();
}
}
全部的訪問規則都將在configure()方法中定義。咱們將使用由Spring傳入的HttpSecurity類來定義規則。在本例中,咱們將限制對組織服務中全部URL的訪問,僅限已經過身份驗證的用戶才能訪問。
若是在訪問組織服務時沒有在HTTP首部中提供OAuth2訪問令牌,將會收到HTTP響應碼401以及一條指示須要對服務進行完整驗證的消息。
圖7-6展現了在沒有OAuth2 HTTP首部的狀況下,對組織服務進行調用的輸出結果。
圖7-6 嘗試調用組織服務將致使調用失敗
接下來,咱們將使用OAuth2訪問令牌調用組織服務。要獲取訪問令牌,須要閱讀7.2.4節,瞭解如何生成OAuth2令牌。咱們須要將access_token字段的值從對/auth/oauth/token端點調用所返回的JSON調用結果中剪切出來,並在對組織服務的調用中粘貼使用它。記住,在調用組織服務時,須要添加一個名爲Authorization的HTTP首部,其值爲Bearer access_token。
圖7-7展現了對組織服務的調用,可是此次使用了傳遞給它的OAuth2訪問令牌。
圖7-7 在對組織服務的調用中傳入OAuth2訪問令牌
這多是使用OAuth2保護端點的最簡單的用例之一。接下來,咱們將在此基礎上進行構建,並將對特定端點的訪問限制在特定角色。
2.經過特定角色保護服務
在接下來的示例中,咱們將鎖定組織服務的DELETE調用,僅限那些具備ADMIN訪問權限的用戶。正如7.2.3節中介紹過的,咱們建立了兩個能夠訪問EagleEye服務的用戶帳戶,即john.carnell和william.woodward。john.carnell帳戶擁有USER角色,而william.woodward帳戶擁有USER和ADMIN角色。
代碼清單7-6展現瞭如何建立configure()方法來限制對DELETE端點的訪問,使得只有那些已經過驗證並具備ADMIN角色的用戶才能訪問。
代碼清單7-6 限制只有ADMIN角色能夠進行刪除
// 爲了簡潔,省略了import語句
@Configuration
public class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception{
http
⇽--- authorizeRequests()表示驗證的請求
.authorizeRequests()
⇽--- antMatchers()容許開發人員限制對受保護的URL和HTTP DELETE動詞的調用
.antMatchers(HttpMethod.DELETE, "/v1/organizations/**")
⇽--- hasRole()方法是一個逗號分隔的可訪問角色列表。
.hasRole("ADMIN")
⇽--- 下面表示任何請求都須要被驗證
.anyRequest()
.authenticated();
}
}
在代碼清單7-6中,咱們將服務中以/v1/organizations開頭的端點的DELETE調用限制爲ADMIN角色:
.authorizeRequests() .antMatchers(HttpMethod.DELETE, "/v1/organizations/**") .hasRole("ADMIN")
antMatcher()方法可使用一個以逗號分隔的端點列表。這些端點可使用通配符風格的符號來定義想要訪問的端點。例如,若是要限制DELETE調用,而無論URL名稱中的版本如何,那麼可使用*來代替URL定義中的版本號:
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/*/organizations/**")
.hasRole("ADMIN")
受權規則定義的最後一部分仍然定義了服務中的其餘端點都須要由已經過驗證的用戶來訪問:
.anyRequest() .authenticated();
{ "error": "access_denied", "error_description": "Access is denied" }
若是使用william.woodward用戶帳戶(密碼:password2)及其OAuth2令牌嘗試徹底相同的調用,會看到返回一個成功的調用(HTTP狀態碼204 —— Not Content),而且該組織將被組織服務刪除。
到目前爲止,咱們已經研究了兩個簡單示例,它們使用OAuth2調用和保護單個服務(組織服務)。然而,一般在微服務環境中,將會有多個服務調用用來執行一個事務。在這些類型的狀況下,須要確保OAuth2訪問令牌在服務調用之間傳播。
7.3.4 傳播OAuth2訪問令牌
爲了演示在服務之間傳播OAuth2令牌,咱們如今來看一下如何使用OAuth2保護許可證服務。記住,許可證服務調用組織服務查找信息。問題在於,如何將OAuth2令牌從一個服務傳播到另外一個服務?
咱們將建立一個簡單的示例,使用許可證服務調用組織服務。這個示例以第6章中的例子爲基礎,兩個服務都在Zuul網關後面運行。
圖7-8展現了一個已經過驗證的用戶的OAuth2令牌如何流經Zuul網關、許可證服務而後到達組織服務的基本流程。
圖7-8 必須在整個調用鏈中攜帶OAuth2令牌
在圖7-8中發生瞭如下活動。
(1)用戶已經向OAuth2服務器進行了驗證,並向EagleEye Web應用程序發出調用。用戶的OAuth2訪問令牌存儲在用戶的會話中。EagleEye Web應用程序須要檢索一些許可數據,並對許可證服務的REST端點進行調用。做爲許可證服務的REST端點的一部分,EagleEye Web應用程序將經過HTTP首部Authorization添加OAuth2訪問令牌。許可證服務只能在Zuul服務網關後面訪問。
(2)Zuul將查找許可證服務端點,而後將調用轉發到其中一個許可證服務的服務器。服務網關須要從傳入的調用中複製HTTP首部Authorization,並確保HTTP首部Authorization被轉發到新端點。
(3)許可證服務將接收傳入的調用。因爲許可證服務是受保護資源,它將使用EagleEye的OAuth2服務來確認令牌,而後檢查用戶的角色是否具備適當的權限。做爲其工做的一部分,許可證服務會調用組織服務。在執行這個調用時,許可證服務須要將用戶的OAuth2訪問令牌傳播到組織服務。
(4)當組織服務接收到該調用時,它將再次使用HTTP首部Authorization的令牌,並使用EagleEye OAuth2服務器來確認令牌。
實現這些流程須要作兩件事。第一件事是須要修改Zuul服務網關,以將OAuth2令牌傳播到許可證服務。在默認狀況下,Zuul不會將敏感的HTTP首部(如Cookie、Set-Cookie和Authorization)轉發到下游服務。要讓Zuul傳播HTTP首部Authorization,須要在Zuul服務網關的application.yml或Spring Cloud Config數據存儲中設置如下配置:
zuul.sensitiveHeaders: Cookie,Set-Cookie
這一配置是黑名單,它包含Zuul不會傳播到下游服務的敏感首部。在上述黑名單中沒有Authorization值就意味着Zuul將容許它經過。若是根本沒有設置zuul.sensitive-Headers屬性,Zuul將自動阻止3個值(Cookie、Set-Cookie和Authorization)被傳播。
Zuul的其餘OAuth2功能呢?
Zuul能夠自動傳播下游的OAuth2訪問令牌,並經過使用@EnableOAuth2Sso註解來針對OAuth2服務的傳入請求進行受權。我特地沒有使用這種方法,由於我在本章的目標是,在不增長其餘複雜性(或調試)的狀況下,展現OAuth2如何工做的基礎知識。雖然Zuul服務網關的配置並不複雜,但它會在本已經擁有許多內容的章節中添加更多內容。若是讀者有興趣讓Zuul服務網關參與單點登陸(Single Sign On,SSO),Spring Cloud Security文檔中有一個簡短而全面的教程,它涵蓋了Spring服務器的創建。
須要作的第二件事就是將許可證服務配置爲OAuth2資源服務,並創建所需的服務受權規則。本節不會詳細討論許可證服務的配置,由於在7.3.3節中已經討論過受權規則。
最後,須要作的就是修改許可證服務中調用組織服務的代碼。咱們須要確保將HTTP首部Authorization注入應用程序對組織服務的調用中。若是沒有Spring Security,那麼開發人員必須編寫一個servlet過濾器以從傳入的許可證服務調用中獲取HTTP首部,而後手動將它添加到許可證服務中的每一個出站服務調用中。Spring OAuth2提供了一個支持OAuth2調用的新REST模板類OAuth2RestTemplate。要使用OAuth2RestTemplate類,須要先將它公開爲一個能夠被自動裝配到調用另外一個受OAuth2保護的服務的服務的bean。咱們能夠在licensing-service/ src/main/java/com/thoughtmechanix/licenses/Application.java中執行上述操做:
@Bean
public OAuth2RestTemplate oauth2RestTemplate(
OAuth2ClientContext oauth2ClientContext,
OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
要實際查看 OAuth2RestTemplate類,能夠查看 licensing-service/src/main/java/com/thoughtmechanix/ licenses/clients/OrganizationRestTemplateClient.java類。
代碼清單7-7展現了OAuth2RestTemplate是如何自動裝配到這個類中的。
代碼清單7-7 使用OAuth2RestTemplate來傳播OAuth2訪問令牌
// 爲了簡潔,省略了import語句
@Component
public class OrganizationRestTemplateClient {
⇽--- OAuth2RestTemplate是標準RestTemplate的加強式替代品,可處理OAuth2訪問令牌的傳播
@Autowired
OAuth2RestTemplate restTemplate;
private static final Logger logger = LoggerFactory.getLogger(OrganizationRestTemplateClient.class);
public Organization getOrganization(String organizationId){
logger.debug("In Licensing Service.getOrganization: {}", UserContext.getCorrelationId());
⇽--- 調用組織服務的方式與標準的RestTemplate徹底相同
ResponseEntity<Organization> restExchange =
restTemplate.exchange(
HttpMethod.GET,
null, Organization.class, organizationId);
return restExchange.getBody();
}
}