Spring Cloud入門教程(十一):微服務安全(Security)
Spring Cloud入門教程系列:
- ofollow,noindex">Spring Cloud入門教程(一):服務治理(Eureka)
- Spring Cloud入門教程(二):客戶端負載均衡(Ribbon)
- Spring Cloud入門教程(三):宣告式服務呼叫(Feign)
- Spring Cloud入門教程(四):微服務容錯保護(Hystrix)
- Spring Cloud入門教程(五):API服務閘道器(Zuul) 上
- Spring Cloud入門教程(六):API服務閘道器(Zuul) 下
- Spring Cloud入門教程(七):分散式鏈路跟蹤(Sleuth)
- Spring Cloud入門教程(八):統一配置中心(Config)
- Spring Cloud入門教程(九):基於訊息驅動開發(Stream)
- Spring Cloud入門教程(十):訊息匯流排(Bus)
本人和同事撰寫的 《Spring Cloud微服務架構開發實戰》 一書也在京東、噹噹等書店上架,大家可以點選 這裡 前往購買,多謝大家支援和捧場!
安全,幾乎在任何應用開發中都是繞不過去的一個基礎功能。當我們將應用轉移到微服務架構時,安全將會更加複雜。在2016年David Borsos在倫敦的微服務大會上提出了以下四種方案:
-
單點登入(SSO): 每個微服務都需要和認證服務互動,但這將產生大量非常瑣碎的網路流量和重複的工作,當動在應用中存在數十個或更多微服務時,該方案的弊端就非常明顯;
-
分散式會話(Session)方案: 該方案將使用者認證的資訊儲存在共享儲存中(如:Redis),並使用使用者會話的ID作為key來實現的簡單分散式雜湊對映。當用戶訪問微服務時,可以通過會話的ID從從共享儲存中獲取使用者認證資訊。該方案在大部分時候非常不錯,但其主要缺點在於共享儲存需要一定保護機制,此時相應的實現就會相對複雜;
-
客戶端令牌(Token)方案: 令牌在客戶端生成,並由認證伺服器進行簽名,令牌中包含足夠的資訊,以便各微服務可以使用。令牌會附加到每個請求上,為微服務提供使用者身份驗證。該解決方案的安全性相對較好,但由於令牌由客戶端生成並儲存,因此身份驗證登出非常麻煩,一個折衷解決方案就是通過短期令牌和頻繁檢查認證服務來驗證令牌是否有效等。對於客戶端令牌JSON Web Tokens(JWT)是一個非常好的選擇;
-
客戶端令牌與API閘道器結合: 使用該方案意味著所有請求都通過閘道器,從而有效地隱藏了微服務。在請求時,閘道器將原始使用者令牌轉換為內部會話。這樣也就可以閘道器對令牌進行登出,從而解決上一種方案存在的問題。
在本文中我們將著重介紹基於令牌的解決方案,而基於令牌的解決方案最好的選擇就是OAuth2.0。
1. OAuth2.0
關於OAuth2.0在維基百科中描述如下:
開放授權(OAuth)是一個開放標準,允許使用者讓第三方應用訪問該使用者在某一網站上儲存的私密的資源(如照片,視訊,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用。
OAuth允許使用者提供一個令牌,而不是使用者名稱和密碼來訪問他們存放在特定服務提供者的資料。每一個令牌授權一個特定的網站(例如,視訊編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相簿中的視訊)。這樣,OAuth讓使用者可以授權第三方網站訪問他們儲存在另外服務提供者的某些特定資訊,而非所有內容。 OAuth 2.0是OAuth協議的下一版本,但不向下相容OAuth 1.0。OAuth 2.0關注客戶端開發者的簡易性,同時為Web應用,桌面應用和手機,和起居室裝置提供專門的認證流程。
對於讓我們首先了解一下OAuth2.0中的幾個關鍵術語:
- Resource Owner : 資源所有者,我們可以直接理解為:使用者(User);
- User Agent : 使用者代理,對於Web應用可以直接理解為瀏覽器;
- Authorization server : 認證伺服器,即提供使用者認證和授權的伺服器,可以是獨立伺服器;
- Resource server : 資源伺服器,這裡我們可以理解為需要保護的微服務。
然後,讓我們看一下OAuth2.0的認證流程圖(摘自RFC6749):

Security-OAuth2-010.png
認證流程步驟如下:
- (A)使用者開啟客戶端以後,客戶端請求使用者給予授權;
- (B)使用者同意授權給客戶端;
- (C)客戶端使用上一步獲得的授權,向認證伺服器申請令牌;
- (D)認證伺服器對客戶端進行認證以後,確認無誤,同意發放令牌;
- (E)客戶端使用令牌,向資源伺服器申請獲取資源;
- (F)資源伺服器確認令牌無誤,同意向客戶端開放資源。
從流程上可以得知客戶端必須在得到使用者授權後才能夠從認證服務中獲取到令牌。OAuth2.0針對客戶端授權提供了下面四種授權方式:
- 授權碼模式(authorization code) : 該種模式是功能最完整、流程最嚴密的授權模式;
- 簡化模式(implicit) : 該模式不需要通過第三方應用程式的伺服器,跳過了"授權碼"這個步驟,直接在瀏覽器中向認證伺服器申請令牌,因此稱為簡化模式;
- 密碼模式(password) : 使用者向客戶端提供自己的使用者名稱和密碼,客戶端通過這些資訊直接向認證伺服器獲取授權;
- 客戶端模式(client credentials) : 指客戶端以自己的名義,而不是以使用者的名義向認證伺服器獲取認證,這種方式下認證伺服器會將客戶端作為一個使用者來對待。
2. 實現微服務安全
下面讓我們著手來實現示例專案的安全管控。
2.1 搭建認證伺服器
首先,我們會搭建一個認證伺服器(Auth Server)。該伺服器也是一個標準的Spring Boot框架應用。
2.1.1 編寫Maven檔案
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>twostepsfromjava.cloud</groupId> <artifactId>twostepsfromjava-cloud-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <relativePath>../parent</relativePath> </parent> <artifactId>auth-server</artifactId> <name>MS Blog Projects(Security): Auth Server</name> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <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> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
在該配置檔案中我們需要引入 spring-cloud-security
和 spring-security-oauth2
依賴。
2.1.2 實現客戶端管理
對於OAuth2.0應用來說需要實現一個客戶端認證管理,這裡我們直接繼承 AuthorizationServerConfigurerAdapter
,並通過記憶體管理的方式增加了一個客戶端應用,程式碼如下:
package io.twostepsfromjava.cloud.auth.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; /** * OAuth2Server 配置 * * @author CD826([email protected]) * @since 1.0.0 */ @Configuration public class OAuthConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("malldemo") .secret("pgDBd99tOX8d") .authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials") .scopes("webmall"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } }
客戶端ID我們設定為 malldemo
,secret則設定為: pgDBd99tOX8d
,同時我們還為客戶端授權了 authorization_code
, refresh_token
, implicit
, password
, client_credentials
認證模式。並且在接下來的示例中我們會使用授權碼模式和密碼模式來進行測試。
2.1.3 實現使用者認證和授權的管理
對於使用過Spring Security的同學來說對Security中使用者認證和授權的管理應該不會陌生,這裡也不再細講,不熟悉的同學可以自行搜尋來了解。示例中程式碼如下:
package io.twostepsfromjava.cloud.auth.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; /** * OAuth2 安全配置 * * @author CD826([email protected]) * @since 1.0.0 */ @Configuration @Order(org.springframework.boot.autoconfigure.security.SecurityProperties.ACCESS_OVERRIDE_ORDER) public class OAuthWebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception{ return super.authenticationManagerBean(); } @Override @Bean public UserDetailsService userDetailsServiceBean() throws Exception { return super.userDetailsServiceBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user001") .password("pwd001") .roles("USER") .and() .withUser("admin") .password("pwdAdmin") .roles("USER", "ADMIN"); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.authorizeRequests() .anyRequest() .authenticated() .and() .httpBasic(); } }
在上面的程式碼中,我們依然通過記憶體方式進行管理,並建立了兩個使用者:
- user001: 是一個普通使用者,只有
USER
角色; - admin: 是一個管理員使用者,擁有
USER
和ADMIN
角色。
在上面的程式碼中我們還指定了所有訪問都需要認真,並且開啟了httpBasic認證,通過這個當一個未認證使用者訪問時就可以通過瀏覽器彈出一個認證對話方塊,可以讓使用者輸入使用者名稱和密碼進行認證。
2.1.4 實現應用引導類
對於我們所要實現的認證伺服器來說最重要的就是需要在應用引導類中增加 @EnableAuthorizationServer
註解,通過該註解就可以啟動Spring Cloud Security,並且為我們提供一系列端點,從而實現OAuth2.0的認證。這些端點分別為:
/oauth/authorize /oauth/token /oauth/confirm_access /oauth/error /oauth/check_token /oauth/token_key
2.1.5 實現使用者資訊載入端點
對於認證服務來說我們還需要提供一個使用者資訊載入端點,這樣其它微服務就可以使用令牌從認證伺服器獲取認證使用者的資訊,從而能夠實現使用者認證及鑑權處理。具體實現程式碼如下:
package io.twostepsfromjava.cloud.auth.api; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; /** * 獲取當前認證使用者的資訊 * * @author CD826([email protected]) * @since 1.0.0 */ @RestController public class AuthEndpoint { protected Logger logger = LoggerFactory.getLogger(AuthEndpoint.class); @RequestMapping(value = { "/auth/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; } }
該段程式碼將從Spring Security中獲取到當前使用者資訊,並轉化成一個Map物件返回。
2.1.6 編寫配置檔案
# 定義應用埠 server.port=8290 # 定義應用名稱 spring.application.name=authserver logging.level.org.springframework=INFO logging.level.io.twostepsfromjava=DEBUG
2.2 完善使用者微服務
接下來我們將對使用者微服務進行修改,增加安全處理功能。
2.2.1 引入Security依賴
對於使用者微服務來說是一個Resource server,也就是資源伺服器,當訪問到某些資源時需要進行使用者認證及鑑權,因此需要引入對Spring Cloud Security的依賴:
<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>
2.2.2 完善應用引導類修改
在Spring Cloud Security中我們只需要對需要進行安全管理的應用增加 @EnableResourceServer
註解來開啟安全管控:
package io.twostepsfromjava.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; /** * TwoStepsFromJava Cloud -- User Service 伺服器 * * @author CD826([email protected]) * @since 1.0.0 */ @EnableDiscoveryClient @EnableResourceServer @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
當引入了 spring-cloud-security
並在應用引導類中增加了 @EnableResourceServer
註解時,應用就會開啟預設的安全管控處理,如果你想在自己的應用中增加更細的安全管控,那麼需要繼承 WebSecurityConfigurerAdapter
並實現安全相關配置,這個在這裡就不細講了。
2.2.3 完善應用配置
我們需要在應用配置檔案中增加獲取認證使用者資訊的端點,具體配置如下:
# 保持原來的配置不變 # OAuth2 security.oauth2.resource.user-info-uri=http://localhost:8290/auth/user
所配置的端點也就是之前我們在認證伺服器所實現的獲取當前登入使用者資訊的端點。
2.2.4 增加獲取當前使用者資訊的端點
為了進行測試,我們需要在使用者微服務中增加一個獲取當前已登入使用者資訊的端點,程式碼如下:
@RequestMapping(value = "/my", method = RequestMethod.GET) public UserDto myDetail() { Map curUser = (Map) SecurityContextHolder.getContext() .getAuthentication() .getPrincipal(); String userName = (String)curUser.get("username"); return new UserDto(userName, userName, "/avatar/default.png", ""); }
這裡的程式碼非常簡單,就是直接從 SecurityContextHolder
中獲取到當前已登入使用者的資訊,並構造成一個 UserDto
,然後返回。
Ok,到此使用者微服務的改造就完成了。如果你現在啟動使用者微服務並進行訪問上面所提供的端點,就會看到如下返回:

未認證訪問使用者微服務
也就是說,現在需要許可權才可以訪問該端點。那麼我們如何提供許可權呢?
之前在講OAuth2.0的時候我們提到客戶端授權模式有四種,那麼我們來看看如何使用這些授權模式來實現具體的使用者認證處理。
2.3 通過授權碼模式(authorization code)訪問
首先,讓我們來看看如何通過OAuth2.0所提供的最全面的授權流程授權碼模式來實現使用者認證。
我們需要依次啟動:服務治理伺服器(Eureak Server)、認證伺服器(Auth Server)和使用者微服務。
第一步,我們首先來構造獲取訪問令牌的Url:
http://localhost:8290/oauth/authorize?response_type=code&client_id=malldemo&redirect_uri=http://localhost:8260&scope=webmall&state=63879
對於該Url說明如下:
-
/oauth/authorize
: 這個是獲取授權碼的端點; -
client_id
: 客戶端的ID,在請求的Url中必選包含。請注意一下我們這裡給的值為:malldemo
,這個就是我們在認證伺服器中配置客戶端列表是所配置的; -
response_type
: 表示授權型別,也是必選的,對於授權碼模式,這裡固定填寫為:code
; -
scope
: 表示申請的許可權範圍,可以不填。這個是指當有不同的客戶端時所授權是否複用; -
redirect_uri
: 授權成功後重定向到的URI; -
state
: 表示客戶端的當前狀態,可以指定任意值,不論授權是否成功認證伺服器都會原封不動地返回這個值。
我們,接下來可以直接在瀏覽器中請求這個地址,然後返回的頁面如下:

使用者登入截圖
這個一個瀏覽器自身的使用者登入視窗。因為,我們沒有登入過,所以認證伺服器通過 httpBasic
開啟瀏覽器登入模式,這樣當用戶尚未認證時就會彈出如上的登入視窗。
我們在登入視窗中輸入 user001
和 pwd001
也就是之前認證伺服器中所配置的使用者列表中的一個,然後就會跳轉到如下頁面:

使用者授權截圖
這裡就是使用者是否授權的介面。在該介面中我們可以看到所傳入的客戶端應用的ID和授權範圍都會顯示出來。這裡我們點選【Approve】,然後瀏覽器就會跳轉到 redirect_uri
所指定的地址,這時候仔細觀察所跳轉回來的地址,如下:
http://localhost:8260/?code=tn8n8F&state=63879
可以發現在地址包含了兩個引數:
code state
Ok,第一步我們已經獲取到了授權碼。那麼第二步就是根據這個授權碼來獲取訪問令牌,在獲取訪問令牌時我們使用Postman來進行,介面截圖如下:

獲取訪問令牌
這該截圖中我們需要填寫以下引數:
/oauth/token grant_type code redirect_uri client_id
此外,在上面的截圖中我們還需要填寫授權認證資訊,這個因為我們認證伺服器開啟了許可權驗證,這裡填寫客戶端的ID和secret即可。此時伺服器就會把客戶端作為一個使用者來對待,從而能夠訪問 /oauth/token
端點。
P.S. 這裡這麼做主要時簡化測試方法,使得我們上一步訪問時可以彈出認證對話方塊。但實際生產使用時不應這麼做,需要自己定義使用者登入頁面、授權頁面。
上面的請求,我們可以獲得如下返回:

獲取到訪問令牌
返回的內容如下:
-
access_token
: 這個是所獲取訪問令牌; -
token_type
: 令牌型別,Spring Cloud OAuth預設返回的值為bearer
; -
expires_in
: 表示令牌過期時間,單位為秒,預設為12小時; -
refresh_token
: 更新令牌,過期後可以用來獲取下一次的訪問令牌; -
scope
: 許可權範圍,一般與客戶端申請範圍一致。
有了訪問令牌下一步我就可以訪問使用者微服務了,訪問方式如下:

通過訪問令牌訪問使用者微服務
訪問時我們需要在Header中指定 Authorization
,並且將值設定為上一步所獲取到的訪問令牌即可,這樣我們就可以得到正確的資料返回了,如上圖所示。
這樣我們就完成了通過授權碼模式實現使用者認證的測試。
2.4 通過密碼模式(password)訪問
如果認證伺服器是第三方的,那麼使用上面的流程問題不大。如果認證伺服器是我們自己搭建的,比如該示例,那麼上面的授權顯有點複雜。接下來讓我們看看如何使用密碼模式來簡化。
我們按照下圖方式來直接請求認證伺服器獲取訪問令牌:

通過密碼獲取使用者授權-1

通過密碼獲取使用者授權-2
在上圖中我們需要同時設定客戶端的ID、secret及訪問使用者的使用者名稱和密碼,並將 grant_type
設定為 password
,這樣就可以直接獲取到訪問令牌,如下圖所示:

通過密碼獲取訪問令牌
然後,我們根據所獲取到的訪問令牌再次訪問使用者微服務,可以獲取如下介面:

訪問使用者微服務
這說明,使用者認證也是成功的。
可見,通過密碼模式可以大大簡化使用者認證流程,但是需要使用者信任客戶端的情況下才會提供,因此這種方式適合客戶端與認證伺服器是同一個應用的情況下。
對於其它客戶端授權模式這裡就不再一一進行測試了。本文中所提到的示例你可以在 這裡 下載。