車(chē)商城網(wǎng)站建設(shè)新媒體運(yùn)營(yíng)培訓(xùn)課程
本專(zhuān)欄將從基礎(chǔ)開(kāi)始,循序漸進(jìn),以實(shí)戰(zhàn)為線索,逐步深入SpringSecurity相關(guān)知識(shí)相關(guān)知識(shí),打造完整的SpringSecurity學(xué)習(xí)步驟,提升工程化編碼能力和思維能力,寫(xiě)出高質(zhì)量代碼。希望大家都能夠從中有所收獲,也請(qǐng)大家多多支持。
專(zhuān)欄地址:SpringSecurity專(zhuān)欄
本文涉及的代碼都已放在gitee上:gitee地址
如果文章知識(shí)點(diǎn)有錯(cuò)誤的地方,請(qǐng)指正!大家一起學(xué)習(xí),一起進(jìn)步。
專(zhuān)欄匯總:專(zhuān)欄匯總
文章目錄
- 13.1 編寫(xiě)你自己的authorization server實(shí)現(xiàn)
- 13.2 定義用戶管理
- 13.3 向authorization server注冊(cè)clients
- 13.4 使用密碼授予類(lèi)型
- 13.5 使用授權(quán)碼授予類(lèi)型
- 13.6 使用客戶端憑證授予類(lèi)型
- 13.7 使用刷新令牌授予類(lèi)型
本章包括
- 實(shí)現(xiàn)OAuth 2authorization server
- 管理authorization server的客戶端
- 使用OAuth 2授權(quán)類(lèi)型
在本章中,我們將討論用Spring Security實(shí)現(xiàn)一個(gè)authorization server。正如你在第12章中學(xué)到的,authorization server是OAuth 2架構(gòu)中的一個(gè)組件(圖13.1)。authorization server的作用是驗(yàn)證用戶并向客戶端提供一個(gè)令牌。客戶端使用這個(gè)令牌來(lái)代表用戶訪問(wèn)資源服務(wù)器所暴露的資源。你還了解到,OAuth 2框架定義了多個(gè)獲取令牌的流程。我們稱(chēng)這些流程為授予。你可以根據(jù)你的情況選擇不同的授予方式之一。authorization server的行為會(huì)根據(jù)所選擇的授予而有所不同。在本章中,你將學(xué)習(xí)如何用Spring Security為最常見(jiàn)的OAuth 2授權(quán)類(lèi)型配置authorization server。
- 授權(quán)碼(Authorization code grant)授予類(lèi)型
- 密碼授予(Password grant)類(lèi)型
- 客戶憑證授予(Client credentials grant)類(lèi)型
你還將學(xué)習(xí)如何配置authorization server來(lái)發(fā)行刷新令牌??蛻舳耸褂盟⑿铝钆苼?lái)獲得新的訪問(wèn)令牌。如果一個(gè)訪問(wèn)令牌過(guò)期了,客戶端必須獲得一個(gè)新的。要做到這一點(diǎn),客戶有兩個(gè)選擇:使用用戶憑證重新認(rèn)證或使用刷新令牌。我們?cè)?2.3.4節(jié)中討論了使用刷新令牌比用戶憑證的優(yōu)勢(shì)。
圖13.1 authorization server是OAuth 2的角色之一。它識(shí)別資源所有者,并向客戶提供一個(gè)訪問(wèn)令牌。客戶端需要該訪問(wèn)令牌來(lái)代表用戶訪問(wèn)資源。
Spring Security團(tuán)隊(duì)宣布正在開(kāi)發(fā)一個(gè)新的authorization server:http://mng.bz/4Be5 https://spring.io/projects/spring-authorization-server。可以通過(guò)這個(gè)鏈接了解不同的Spring Security項(xiàng)目中所實(shí)現(xiàn)的功能:http://mng.bz/Qx01。
在本章,我我們實(shí)現(xiàn)一個(gè)自定義的authorization server可以幫助你更好地理解這個(gè)組件的工作原理。 當(dāng)然,這也是目前實(shí)現(xiàn)authorization server的唯一方法。
我看到開(kāi)發(fā)人員在他們的項(xiàng)目中應(yīng)用這種方法。如果你不得不和一個(gè)這樣實(shí)現(xiàn)authorization server的項(xiàng)目打交道,那么在你使用新的實(shí)現(xiàn)之前,你還是要理解它。
你可以使用第三方工具,如Keycloak或Okta,而不是實(shí)現(xiàn)一個(gè)自定義的authorization server(authorization server)。在第18章中,我們將在我們的實(shí)踐案例中使用Keycloak。但根據(jù)我的經(jīng)驗(yàn),有時(shí)利益相關(guān)者不會(huì)接受使用這樣的解決方案,你需要去實(shí)現(xiàn)自定義代碼。讓我們?cè)诒菊孪旅娴恼鹿?jié)中學(xué)習(xí)如何做到這一點(diǎn)并更好地理解authorization server。
13.1 編寫(xiě)你自己的authorization server實(shí)現(xiàn)
沒(méi)有authorization server就沒(méi)有OAuth 2的流程。正如我前面所說(shuō),OAuth 2主要是為了獲得一個(gè)訪問(wèn)令牌。而authorization server是OAuth 2架構(gòu)中發(fā)放訪問(wèn)令牌的組成部分。所以你首先需要知道如何實(shí)現(xiàn)它。然后,在第14章和第15章中,你將學(xué)習(xí)資源服務(wù)器如何根據(jù)客戶端從authorization server獲得的訪問(wèn)令牌來(lái)授權(quán)請(qǐng)求。讓我們開(kāi)始構(gòu)建一個(gè)authorization server。首先,你需要?jiǎng)?chuàng)建一個(gè)新的Spring Boot項(xiàng)目,并添加以下代碼片斷中的依賴項(xiàng)。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency
在項(xiàng)目中,你還需要為spring-cloud-dependencies添加dependencyManagement標(biāo)簽。接下來(lái)的代碼片段顯示了這一點(diǎn)。
<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Hoxton.SR12</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
我們現(xiàn)在可以定義一個(gè)配置類(lèi),我稱(chēng)之為AuthServerConfig。除了經(jīng)典的@Configuration注解,我們還需要給這個(gè)類(lèi)加上@EnableAuthorizationServer注解。這樣,我們就指示Spring Boot啟用OAuth 2authorization server的特定配置。我們可以通過(guò)擴(kuò)展AuthorizationServerConfigurerAdapter類(lèi)和重寫(xiě)特定的方法來(lái)定制這種配置,我們將在本章討論。下面列出了AuthServerConfig類(lèi)。
代碼清單13.1 AuthServerConfig類(lèi)
package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {
}
我們已經(jīng)有了authorization server的最低配置。然而,為了使其可用,我們?nèi)匀恍枰獙?shí)現(xiàn)用戶管理,注冊(cè)至少一個(gè)客戶端,并決定支持哪些授權(quán)類(lèi)型。
13.2 定義用戶管理
在本節(jié)中,我們將討論用戶管理。authorization server是處理OAuth 2框架中的用戶認(rèn)證的組件。因此,它自然需要管理用戶。幸運(yùn)的是,用戶管理的實(shí)現(xiàn)與你在第3章和第4章中學(xué)到的并沒(méi)有改變。我們繼續(xù)使用UserDetails、UserDetailsService和UserDetailsManager接口來(lái)管理憑證。而為了管理密碼,我們繼續(xù)使用PasswordEncoder接口。在這里,這些接口具有相同的角色,并且與你在第3章和第4章中學(xué)到的相同。
圖13.2提醒你在Spring Security的認(rèn)證過(guò)程中的主要組件。你應(yīng)該觀察到與我們之前描述的認(rèn)證架構(gòu)不同的是,我們?cè)谶@張圖中不再有SecurityContext了。發(fā)生這種變化是因?yàn)檎J(rèn)證的結(jié)果并不存儲(chǔ)在SecurityContext中。認(rèn)證是通過(guò)TokenStore的令牌來(lái)管理的。你會(huì)在第14章中了解到更多關(guān)于TokenStore的信息,在那里我們討論資源服務(wù)器。
圖13.2 認(rèn)證過(guò)程。一個(gè)過(guò)濾器攔截用戶請(qǐng)求,并將認(rèn)證責(zé)任委托給 authentication manager。此外, authentication manager使用一個(gè)authentication provider來(lái)實(shí)現(xiàn)認(rèn)證邏輯。為了找到用戶,authentication provider使用UserDetailsService,為了驗(yàn)證密碼,認(rèn)證提供者使用PasswordEncoder。
讓我們來(lái)看看如何在我們的authorization server中實(shí)現(xiàn)用戶管理。我總是喜歡把配置類(lèi)的責(zé)任分開(kāi)。為此,我選擇在我們的應(yīng)用程序中定義第二個(gè)配置類(lèi),在那里我只寫(xiě)用戶管理所需的配置。我把這個(gè)類(lèi)命名為WebSecurityConfig,你可以在下面的代碼中看到它的實(shí)現(xiàn)。
package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;@Configuration
//擴(kuò)展WebSecurityConfigurerAdapter以訪問(wèn)AuthenticationManager實(shí)例。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic UserDetailsService uds() {InMemoryUserDetailsManager uds = new InMemoryUserDetailsManager();UserDetails user = User.withUsername("john").password("12345").authorities("read").build();uds.createUser(user);return uds;}@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}//將AuthenticationManager實(shí)例作為Spring上下文中的一個(gè)Bean加入。@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}
我們現(xiàn)在可以修改AuthServerConfig類(lèi),將Authentication- Manager注冊(cè)到authorization server上。接下來(lái)的代碼顯示了你需要在AuthServerConfig類(lèi)中進(jìn)行的修改。
package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {//從上下文中注入AuthenticationManager實(shí)例@Autowiredprivate AuthenticationManager authenticationManager;//重寫(xiě)configure()方法以設(shè)置AuthenticationManager。@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager);}
}
有了這些配置,我們現(xiàn)在有了可以在我們的認(rèn)證服務(wù)器上進(jìn)行認(rèn)證的用戶。但是,OAuth 2的架構(gòu)意味著用戶要授予客戶端權(quán)限。代表用戶使用資源的是客戶端。在第13.3節(jié),你將學(xué)習(xí)如何為authorization server配置客戶端。
13.3 向authorization server注冊(cè)clients
在這一節(jié)中,你將學(xué)習(xí)如何讓authorization server知道你的客戶端。 為了調(diào)用authorization server,在OAuth 2架構(gòu)中作為客戶端的應(yīng)用程序需要有自己的憑證。authorization server也會(huì)管理這些憑證,只允許來(lái)自已知客戶端的請(qǐng)求(圖13.3)。
圖13.3 authorization server存儲(chǔ)用戶和客戶的憑證。它使用客戶端的憑證,因此它只允許已知的應(yīng)用程序被它授權(quán)。
你還記得我們?cè)诘谑麻_(kāi)發(fā)的客戶端程序嗎?我們用GitHub作為我們的認(rèn)證服務(wù)器。GitHub需要知道這個(gè)客戶端程序,所以我們做的第一件事就是在GitHub上注冊(cè)這個(gè)程序。然后我們收到了一個(gè)客戶ID和一個(gè)客戶密碼:客戶憑證。我們配置了這些憑證,然后我們的應(yīng)用就用它們與授權(quán)服務(wù)器(GitHub)進(jìn)行認(rèn)證。在這種情況下也是如此。我們的authorization server需要知道它的客戶,因?yàn)樗邮軄?lái)自客戶的請(qǐng)求。在這里,這個(gè)過(guò)程應(yīng)該變得很熟悉。定義授權(quán)服務(wù)器的客戶端的接口是ClientDetails。定義通過(guò)ID檢索ClientDetails的對(duì)象的接口是ClientDetailsService。
這些名字聽(tīng)起來(lái)很熟悉嗎?這些接口的工作方式與UserDetails和UserDetailsService接口類(lèi)似,但這些接口代表的是客戶端。你會(huì)發(fā)現(xiàn),我們?cè)诘?章中討論的許多東西對(duì)ClientDetails和ClientDetailsService的作用是相似的。例如,我們的InMemoryClientDetailsService是ClientDetailsService接口的一個(gè)實(shí)現(xiàn),它在內(nèi)存中管理ClientDetails。它的工作原理類(lèi)似于InMemoryUserDetailsManager類(lèi)的UserDetails。同樣地,JdbcClientDetailsService與JdbcUserDetailsManager類(lèi)似。圖13.4顯示了這些類(lèi)和接口,以及它們之間的關(guān)系。
圖13.4 我們用來(lái)定義authorization server客戶端管理的類(lèi)和接口之間的依賴關(guān)系
我們可以把這些相似之處總結(jié)為幾點(diǎn),你可以很容易地記住:
- ClientDetails是為客戶提供的,正如UserDetails是為用戶提供的。
- ClientDetailsService對(duì)客戶來(lái)說(shuō)就像UserDetailsService對(duì)用戶來(lái)說(shuō)一樣。
- InMemoryClientDetailsService對(duì)客戶來(lái)說(shuō),就像InMemoryUserDetailsManager對(duì)用戶來(lái)說(shuō)一樣。
- JdbcClientDetailsService對(duì)客戶而言,就像JdbcUserDetails- Manager對(duì)用戶而言一樣。
代碼清單13.5向你展示了如何使用InMemoryClientDetailsService定義一個(gè)客戶端配置并進(jìn)行設(shè)置。我在清單中使用的BaseClientDetails類(lèi)是Spring Security提供的ClientDetails接口的一個(gè)實(shí)現(xiàn)。在代碼清單13.6中,你可以找到一種更簡(jiǎn)短的方法來(lái)編寫(xiě)同樣的配置。
代碼清單13.5 使用InMemoryClientDetailsService來(lái)配置一個(gè)客戶端
package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {//Omitted code//重寫(xiě)configure()方法來(lái)設(shè)置ClientDetailsService實(shí)例。@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {//使用ClientDetailsService的實(shí)現(xiàn)創(chuàng)建一個(gè)實(shí)例。InMemoryClientDetailsService service = new InMemoryClientDetailsService();//創(chuàng)建一個(gè)ClientDetails的實(shí)例,并設(shè)置所需的關(guān)于客戶的細(xì)節(jié)。BaseClientDetails cd = new BaseClientDetails();cd.setClientId("client");cd.setClientSecret("secret");cd.setScope(List.of("read"));cd.setAuthorizedGrantTypes(List.of("password"));//在InMemoryClientDetailsService中添加ClientDetails實(shí)例。service.setClientDetailsStore(Map.of("client", cd));//配置ClientDetailsService,供我們的authorization server使用。clients.withClientDetails(service);}}
清單13.6提出了一個(gè)更短的方法來(lái)編寫(xiě)相同的配置。這使我們能夠避免重復(fù),寫(xiě)出更干凈的代碼。
清單13.6 在內(nèi)存中配置ClientDetails
package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService;@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {//Omitted code@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {//使用ClientDetailsService實(shí)現(xiàn)來(lái)管理存儲(chǔ)在內(nèi)存中的ClientDetails。clients.inMemory()//構(gòu)建并添加一個(gè)ClientDetails的實(shí)例。.withClient("client").secret("secret").authorizedGrantTypes("password", "refresh_token").scopes("read");}}
為了少寫(xiě)代碼,我更喜歡使用較短的版本,而不是代碼清單13.5中更詳細(xì)的版本。但是,如果你寫(xiě)的實(shí)現(xiàn)是將客戶的詳細(xì)信息存儲(chǔ)在一個(gè)數(shù)據(jù)庫(kù)中,這主要是現(xiàn)實(shí)世界中的情況,那么最好是使用代碼清單13.5中的實(shí)現(xiàn)。
注意 正如我們對(duì)UserDetailsService所做的那樣,在這個(gè)例子中,我們使用一個(gè)在內(nèi)存中管理細(xì)節(jié)的實(shí)現(xiàn)。這種方法只適用于例子和研究目的。在現(xiàn)實(shí)世界中,你會(huì)使用一個(gè)持久化這些細(xì)節(jié)的實(shí)現(xiàn),通常是在一個(gè)數(shù)據(jù)庫(kù)中。
13.4 使用密碼授予類(lèi)型
在這一節(jié)中,我們使用了帶有OAuth 2密碼授予的authorization server。好吧,我們主要是測(cè)試它是否達(dá)到預(yù)期,因?yàn)橥ㄟ^(guò)我們?cè)诘?3.2和13.3節(jié)所做的實(shí)現(xiàn),我們已經(jīng)有一個(gè)使用密碼授予類(lèi)型的authorization server。我告訴過(guò)你,這很容易!圖13.5提醒了你密碼授予類(lèi)型和授權(quán)服務(wù)器在這個(gè)流程中的位置。
圖13.5 密碼授予類(lèi)型。授權(quán)服務(wù)器收到用戶的憑證并對(duì)用戶進(jìn)行認(rèn)證。如果憑證是正確的,授權(quán)服務(wù)器會(huì)發(fā)出一個(gè)訪問(wèn)令牌,客戶可以用它來(lái)調(diào)用屬于被認(rèn)證用戶的資源。
細(xì)節(jié)作為查詢參數(shù)。正如你在第12章所知道的,我們需要在這個(gè)請(qǐng)求中發(fā)送的參數(shù)是:
- grant_type的值為password
- 用戶名和密碼,這是用戶憑證
- scope,也就是授予的權(quán)力
在下一個(gè)代碼片斷中,你看到了cURL命令。
#如果網(wǎng)址攜帶了"&“拼接的多個(gè)參數(shù),如果不做處理,”&“后面的參數(shù)無(wú)法取到。
#這個(gè)時(shí)候需要對(duì)”&“進(jìn)行轉(zhuǎn)義,包括兩個(gè)步驟:
#1.使用英文模式下輸入的單引號(hào)將參數(shù)包含。
#2.使用^符號(hào)對(duì)”&"符號(hào)進(jìn)行轉(zhuǎn)義。
curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=password^&username=john^&password=12345^&scope=read
運(yùn)行這個(gè)命令,你會(huì)得到這樣的響應(yīng)
{"access_token":"de1c975a-5268-49df-a910-20e2b970da59","token_type":"bearer","refresh_token":"53c4e171-204b-42da-9e56-5b185a9ba915","expires_in":43199,"scope":"read"}
觀察響應(yīng)中的訪問(wèn)令牌。在Spring Security的默認(rèn)配置下,令牌是一個(gè)簡(jiǎn)單的UUID??蛻舳爽F(xiàn)在可以使用這個(gè)令牌來(lái)調(diào)用資源服務(wù)器所暴露的資源。在第13.2節(jié)中,你學(xué)到了如何實(shí)現(xiàn)資源服務(wù)器,同時(shí)也學(xué)到了更多關(guān)于自定義令牌的知識(shí)。
13.5 使用授權(quán)碼授予類(lèi)型
在這一節(jié)中,我們將討論如何為授權(quán)碼授予類(lèi)型配置授權(quán)服務(wù)器。你在第12章開(kāi)發(fā)的客戶端應(yīng)用程序中使用了這種授予類(lèi)型,你知道它是最常用的OAuth 2授予類(lèi)型之一。 了解如何配置你的授權(quán)服務(wù)器以適應(yīng)這種授予類(lèi)型是很有必要的,因?yàn)槟愫芸赡軙?huì)在現(xiàn)實(shí)世界的系統(tǒng)中發(fā)現(xiàn)這種需求。因此,在這一節(jié)中,我們寫(xiě)一些代碼來(lái)證明如何讓它與Spring Security一起工作。從圖13.6中,你可以回憶起授權(quán)代碼授予類(lèi)型是如何工作的,以及授權(quán)服務(wù)器如何與這個(gè)流程中的其他組件進(jìn)行交互。
圖13.6 在授權(quán)代碼授予類(lèi)型中,客戶端將用戶重定向到授權(quán)服務(wù)器進(jìn)行認(rèn)證。用戶直接與授權(quán)服務(wù)器進(jìn)行交互,一旦通過(guò)認(rèn)證,授權(quán)服務(wù)器就會(huì)向客戶端返回一個(gè)重定向URI。當(dāng)它回?fù)芙o客戶端時(shí),它也提供一個(gè)授權(quán)碼。客戶端使用授權(quán)碼來(lái)獲得一個(gè)訪問(wèn)令牌。
正如你在第13.3節(jié)中所學(xué)到的,這完全是關(guān)于你如何注冊(cè)客戶的問(wèn)題。所以,你需要做的就是在客戶端注冊(cè)中設(shè)置另一種授予類(lèi)型,如清單13.7所示。對(duì)于授權(quán)碼授予類(lèi)型,你還需要提供重定向URI。這是授權(quán)服務(wù)器在完成認(rèn)證后將用戶重定向到的URI。在調(diào)用重定向URI時(shí),授權(quán)服務(wù)器也會(huì)提供訪問(wèn)代碼。
代碼清單13.7 設(shè)置授權(quán)碼授予類(lèi)型
package com.laurentiuspilca.ssia.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {@Autowiredprivate AuthenticationManager authenticationManager;@Overridepublic void configure(ClientDetailsServiceConfigurer clients)throws Exception {clients.inMemory().withClient("client").secret("secret").authorizedGrantTypes("authorization_code").scopes("read").redirectUris("http://localhost:9090/home");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager);}}
你可以有多個(gè)客戶,每個(gè)客戶可能使用不同的權(quán)限。但也有可能為一個(gè)客戶設(shè)置多個(gè)授權(quán)。授權(quán)服務(wù)器會(huì)根據(jù)客戶的請(qǐng)求采取行動(dòng)??匆幌孪旅娴拇a,看看你如何為不同的客戶配置不同的授權(quán)。
清單13.8 配置具有不同授予類(lèi)型的客戶端
@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {// Omitted code@Overridepublic void configure(ClientDetailsServiceConfigurer clients)throws Exception {clients.inMemory().withClient("client1").secret("secret1")//ID為client1的客戶只能使用authority_code授權(quán)。.authorizedGrantTypes("authorization_code").scopes("read").redirectUris("http://localhost:9090/home").and().withClient("client2").secret("secret2")//ID為client2的客戶可以使用授權(quán)碼、密碼和刷新令牌中的任何一種。.authorizedGrantTypes("authorization_code", "password", "refresh_token").scopes("read").redirectUris("http://localhost:9090/home");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager);}
}
為一個(gè)客戶使用多種授權(quán)類(lèi)型
正如你所學(xué)到的,可以為一個(gè)客戶允許多種授予類(lèi)型。但是,你必須謹(jǐn)慎對(duì)待這種方法,因?yàn)閺陌踩慕嵌葋?lái)看,它可能會(huì)暴露出你在架構(gòu)中使用了錯(cuò)誤的做法。授予類(lèi)型是客戶端(應(yīng)用程序)獲得訪問(wèn)令牌的流程,這樣它就可以訪問(wèn)一個(gè)特殊的資源。當(dāng)你在這樣的系統(tǒng)中實(shí)現(xiàn)客戶端時(shí)(就像我們?cè)诘?2章中做的那樣),你要根據(jù)你使用的授予類(lèi)型來(lái)編寫(xiě)邏輯。
那么,在授權(quán)服務(wù)器端為同一個(gè)客戶分配多個(gè)授權(quán)類(lèi)型的原因是什么呢?我在一些系統(tǒng)中看到,我認(rèn)為這是一種不好的做法,最好避免,那就是共享客戶證書(shū)。共享客戶憑證意味著不同的客戶程序共享相同的客戶憑證。
在共享客戶端憑證時(shí),多個(gè)客戶端使用相同的憑證從授權(quán)服務(wù)器獲得訪問(wèn)令牌。
在OAuth 2的流程中,客戶端,即使它是一個(gè)應(yīng)用程序,也作為一個(gè)獨(dú)立的組件,有自己的憑證,用來(lái)識(shí)別自己。因?yàn)槟悴环窒碛脩魬{證,所以你也不應(yīng)該分享客戶端憑證。即使所有定義客戶的應(yīng)用程序都是同一個(gè)系統(tǒng)的一部分,也不能阻止你在授權(quán)服務(wù)器層面上將這些客戶注冊(cè)為獨(dú)立的客戶。在授權(quán)服務(wù)器上單獨(dú)注冊(cè)客戶端會(huì)帶來(lái)以下好處。
- 它提供了從每個(gè)應(yīng)用程序單獨(dú)審計(jì)事件的可能性。 當(dāng)你記錄事件時(shí),你知道哪個(gè)客戶端產(chǎn)生了這些事件。
- 它允許更強(qiáng)的隔離性。如果一對(duì)憑證丟失,只有一個(gè)客戶端受到影響。
- 允許范圍的分離。你可以給一個(gè)以特定方式獲得令牌的客戶分配不同的范圍(授予的權(quán)限)。
范圍分離是最基本的,如果管理不當(dāng),會(huì)導(dǎo)致奇怪的情況。讓我們假設(shè)你定義了一個(gè)客戶端,就像下一個(gè)代碼片斷中介紹的那樣。
clients.inMemory().withClient("client").secret("secret").authorizedGrantTypes("authorization_code","client_credentials").scopes("read")
這個(gè)客戶端被配置為授權(quán)代碼和客戶端憑證授予類(lèi)型。 使用其中任何一種,客戶端都會(huì)獲得一個(gè)訪問(wèn)令牌,這為它提供了讀取權(quán)限。這里奇怪的是,客戶端可以通過(guò)認(rèn)證用戶或只使用自己的憑證來(lái)獲得相同的令牌。這沒(méi)有道理,甚至可以說(shuō)這是一個(gè)安全漏洞。即使這對(duì)你來(lái)說(shuō)聽(tīng)起來(lái)很奇怪,我在一個(gè)被要求審計(jì)的系統(tǒng)中也看到了這種做法。為什么那個(gè)系統(tǒng)的代碼是這樣設(shè)計(jì)的?最有可能的是,開(kāi)發(fā)人員并不了解授予類(lèi)型的目的,而是使用了他們?cè)诰W(wǎng)上找到的一些代碼。請(qǐng)確保你避免此類(lèi)錯(cuò)誤。要小心。為了指定授予類(lèi)型,你使用字符串,而不是枚舉值,這種設(shè)計(jì)可能導(dǎo)致錯(cuò)誤。是的,你可以寫(xiě)一個(gè)像這個(gè)代碼片斷中提出的配置。
clients.inMemory().withClient("client").secret("secret").authorizedGrantTypes("password", "hocus_pocus").scopes("read")
只要你不嘗試使用 "hocus_pocus "授予類(lèi)型,該應(yīng)用程序?qū)?shí)際工作。
讓我們使用代碼清單13.9中的配置來(lái)啟動(dòng)應(yīng)用程序。當(dāng)我們想接受授權(quán)碼授予類(lèi)型時(shí),服務(wù)器還需要提供一個(gè)客戶端重定向用戶登錄的頁(yè)面。我們使用你在第五章學(xué)到的formlogin配置來(lái)實(shí)現(xiàn)這個(gè)頁(yè)面。你需要重寫(xiě)configure()方法,如下所示。
@Configuration
public class WebSecurityConfigextends WebSecurityConfigurerAdapter {// Omitted code@Overrideprotected void configure(HttpSecurity http)throws Exception {http.formLogin();}
}
AuthServerConfig類(lèi)代碼如下
package com.hashnode.proj0001firstspringsecurity.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {@Autowiredprivate AuthenticationManager authenticationManager;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("client").secret("secret").authorizedGrantTypes("authorization_code", "refresh_token").scopes("read").redirectUris("http://localhost:9090/home");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager);}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) {security.checkTokenAccess("isAuthenticated()");}}
現(xiàn)在你可以啟動(dòng)應(yīng)用程序,并在瀏覽器中訪問(wèn)鏈接,如下面的代碼段所示。然后你會(huì)被重定向到圖13.7所示的登錄頁(yè)面。
http://localhost:8080/oauth/authorize?response_type=code&client_id=client&scope=read
圖13.7 授權(quán)服務(wù)器將你重定向到登錄頁(yè)面。在它驗(yàn)證了你的身份后,它把你重定向到所提供的重定向URI。
登錄后,授權(quán)服務(wù)器會(huì)明確要求你授予或拒絕所請(qǐng)求的作用域。圖13.8顯示了這個(gè)表格。
圖13.8 認(rèn)證后,授權(quán)服務(wù)器要求你確認(rèn)你要授權(quán)的范圍。
一旦你授予了這些作用域,授權(quán)服務(wù)器就會(huì)把你重定向到重定向URI并提供一個(gè)訪問(wèn)令牌。在下一個(gè)代碼片段中,你會(huì)發(fā)現(xiàn)授權(quán)服務(wù)器將我重定向到的URL。觀察一下客戶端通過(guò)請(qǐng)求中的查詢參數(shù)得到的訪問(wèn)代碼:
http://localhost:9090/home?code=QSEJW9
其中code是授權(quán)碼
你的應(yīng)用程序現(xiàn)在可以使用授權(quán)碼來(lái)獲得一個(gè)調(diào)用/oauth/token端點(diǎn)的令牌。
curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=authorization_code^&scope=read^&code=QSEJW9
響應(yīng)如下
{"access_token":"72e70268-80ea-428f-838e-5ef881ef3eac","token_type":"bearer","refresh_token":"51d2ff63-0b2d-4de5-ac98-e7c15ffc6aec","expires_in":43199,"scope":"read"}
請(qǐng)注意,一個(gè)授權(quán)碼只能使用一次。如果你試圖再次使用相同的代碼調(diào)用/oauth/ token端點(diǎn),你會(huì)收到像下一個(gè)代碼片斷中顯示的錯(cuò)誤。你只能通過(guò)要求用戶再次登錄來(lái)獲得另一個(gè)有效的授權(quán)碼。
{"error":"invalid_grant","error_description":"Invalid authorization code: QSEJW9"}
13.6 使用客戶端憑證授予類(lèi)型
在這一節(jié)中,我們將討論實(shí)現(xiàn)客戶憑證授予類(lèi)型。你可能還記得在第12章中,我們將這種授予類(lèi)型用于后端到后端的認(rèn)證。在這種情況下,它不是強(qiáng)制性的,但有時(shí)我們會(huì)看到這種授予類(lèi)型是我們?cè)诘?章討論的API密鑰認(rèn)證方法的替代品。當(dāng)我們保護(hù)一個(gè)與特定用戶無(wú)關(guān)的、客戶需要訪問(wèn)的api時(shí),我們也可能使用客戶憑證授予類(lèi)型。比方說(shuō),你想實(shí)現(xiàn)一個(gè)返回服務(wù)器狀態(tài)的api。客戶端調(diào)用這個(gè)api來(lái)檢查連接情況,并最終向用戶顯示連接狀態(tài)或錯(cuò)誤信息。因?yàn)檫@個(gè)api只代表客戶端和資源服務(wù)器之間的交易,而不涉及任何用戶特定的資源,所以客戶端應(yīng)該能夠調(diào)用它而不需要用戶進(jìn)行認(rèn)證。對(duì)于這種情況,我們使用客戶端憑證授予類(lèi)型。圖13.9提醒你客戶端憑證授予類(lèi)型是如何工作的,以及授權(quán)服務(wù)器如何與這個(gè)流程中的其他組件進(jìn)行交互。
客戶端憑證授予類(lèi)型不涉及用戶。一般來(lái)說(shuō),我們使用這種授予類(lèi)型在兩個(gè)后端解決方案之間進(jìn)行認(rèn)證??蛻舳酥恍枰膽{證來(lái)驗(yàn)證和獲得訪問(wèn)令牌。
正如你所期望的那樣,要使用客戶憑證授予類(lèi)型,必須用這個(gè)授予類(lèi)型注冊(cè)一個(gè)客戶。在接下來(lái)的代碼中,你可以找到客戶端的配置,它使用了這種授予類(lèi)型。
@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {// Omitted code@Overridepublic void configure(ClientDetailsServiceConfigurer clients)throws Exception {clients.inMemory().withClient("client").secret("secret").authorizedGrantTypes("client_credentials").scopes("info");}
}
你現(xiàn)在可以啟動(dòng)應(yīng)用程序,并調(diào)用/oauth/token api來(lái)獲取訪問(wèn)令牌。下一個(gè)代碼片斷向你展示了如何獲得這個(gè):
curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=client_credentials^&scope=info
響應(yīng)為:
{"access_token":"e0e7f45f-48a9-48c0-97cc-04e24329334e","token_type":"bearer","expires_in":43199,"scope":"info"}
對(duì)客戶憑證授予類(lèi)型要小心。這種授予類(lèi)型只要求客戶端使用其憑證。需要確保你不能像下圖那樣授予權(quán)限,否則,你可能允許客戶端訪問(wèn)用戶的資源而不需要用戶的許可。圖13.10展示了這樣一個(gè)設(shè)計(jì),開(kāi)發(fā)者通過(guò)允許客戶端調(diào)用用戶的資源api而不需要用戶首先進(jìn)行認(rèn)證,從而造成了安全漏洞。
圖13.10 開(kāi)發(fā)者想為客戶端提供調(diào)用/info端點(diǎn)的可能性,而不需要得到用戶的許可。但由于這些使用了相同的范圍,他們現(xiàn)在也允許客戶端調(diào)用/transactions api,這是一個(gè)用戶的資源。
13.7 使用刷新令牌授予類(lèi)型
在這一節(jié)中,我們將討論在用Spring Security開(kāi)發(fā)的授權(quán)服務(wù)器中使用刷新令牌。你可能還記得第12章,當(dāng)與另一種授予類(lèi)型一起使用時(shí),刷新令牌具有一些優(yōu)勢(shì)。你可以在授權(quán)碼授予類(lèi)型和密碼授予類(lèi)型中使用刷新令牌(圖13.11)。
圖13.11 當(dāng)用戶認(rèn)證時(shí),除了訪問(wèn)令牌,客戶端還收到一個(gè)刷新令牌。客戶端使用刷新令牌來(lái)獲得新的訪問(wèn)令牌。
如果你想讓你的授權(quán)服務(wù)器支持刷新令牌,你需要在客戶端的授予列表中加入刷新令牌授予。
@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {// Omitted code@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("client").secret("secret")//在客戶端的授權(quán)授予類(lèi)型列表中添加 refresh_token。.authorizedGrantTypes("password","refresh_token").scopes("read");}
}
現(xiàn)在試試你在第13.4節(jié)中使用的同樣的cURL命令。你會(huì)看到響應(yīng)是類(lèi)似的,但現(xiàn)在包括一個(gè)刷新令牌。
curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=password^&username=john^&password=12345^&scope=read
響應(yīng)為
{"access_token":"8c3b248d-5cc1-42e7-a56b-5aabeb01c7b8","token_type":"bearer","refresh_token":"d88665e3-f10e-4045-85ed-97c1ffc1785d","expires_in":43199,"scope":"read"}
總結(jié)
- ClientRegistration接口定義了Spring Security中的OAuth 2客戶端注冊(cè)。ClientRegistrationRepository接口描述了負(fù)責(zé)管理客戶端注冊(cè)的對(duì)象。這兩個(gè)接口允許你自定義你的授權(quán)服務(wù)器如何管理客戶端注冊(cè)。
- 對(duì)于用Spring Security實(shí)現(xiàn)的授權(quán)服務(wù)器來(lái)說(shuō),客戶端的注冊(cè)決定了授予類(lèi)型。同一個(gè)授權(quán)服務(wù)器可以為不同的客戶提供不同的授予類(lèi)型。這意味著你不需要在你的授權(quán)服務(wù)器中實(shí)現(xiàn)特定的東西來(lái)定義多種授予類(lèi)型。
- 對(duì)于授權(quán)碼授予類(lèi)型,授權(quán)服務(wù)器必須向用戶提供登錄的可能性。這一要求是由于在授權(quán)代碼流程中,用戶(資源所有者)直接在授權(quán)服務(wù)器上授權(quán)給客戶端訪問(wèn)的結(jié)果。
- 一個(gè)客戶端注冊(cè)可以請(qǐng)求多種授予類(lèi)型。這意味著一個(gè)客戶可以在不同情況下使用,例如,密碼和授權(quán)碼授予類(lèi)型。
- 我們使用客戶端證書(shū)授予類(lèi)型來(lái)進(jìn)行后端到后端的授權(quán)。 技術(shù)上有可能,但不常見(jiàn)的是,客戶端請(qǐng)求客戶端證書(shū)授予類(lèi)型與另一個(gè)授予類(lèi)型一起。
- 我們可以將刷新令牌與授權(quán)碼授予類(lèi)型和密碼授予類(lèi)型一起使用。通過(guò)將刷新令牌添加到客戶注冊(cè)中,我們指示授權(quán)服務(wù)器在訪問(wèn)令牌之外也發(fā)放一個(gè)刷新令牌??蛻舳耸褂盟⑿铝钆苼?lái)獲得新的訪問(wèn)令牌,而不需要再次驗(yàn)證用戶。