領先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多Spring Security 5 的主要功能之一是支援編寫與 OAuth 2 保護的服務整合的應用程式。 這包括透過外部服務(例如 Facebook 或 GitHub)登入應用程式的能力。
但是,透過一些額外的程式碼,您還可以取得 OAuth 2 存取權杖,該權杖可用於對服務的 API 執行授權請求。
在本文中,我們將研究如何開發一個使用 Spring Security 5 且與 Facebook 整合的 Spring Boot 應用程式。 您可以在 https://github.com/habuma/facebook-security5 找到本文的完整程式碼。
假設您希望讓應用程式的使用者能夠使用 Facebook 登入。 使用 Spring Security 5,這變得非常容易。 您只需將 Spring Security 的 OAuth 2 用戶端支援新增至專案的組建中,然後設定應用程式的 Facebook 認證即可。
首先,將 Spring Security OAuth 2 用戶端程式庫以及 Spring Security starter 依賴項新增至 Spring Boot 專案的組建中
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
然後,您需要設定應用程式的用戶端 ID 和用戶端密碼(您可以透過在 https://developers.facebook.com/ 向 Facebook 註冊您的應用程式來取得)。 所有 OAuth 2 用戶端的屬性都以 spring.security.oauth2.client.registration
為前綴。 具體來說,對於 Facebook,您將在該前綴下新增 facebook.client-id
和 facebook-client-secret
屬性。 在專案的 application.yml
檔案中,它看起來會像這樣
spring:
security:
oauth2:
client:
registration:
facebook:
client-id: YOUR CLIENT ID GOES HERE
client-secret: YOUR CLIENT SECRET GOES HERE
您也可以將這些屬性設定為環境變數、在屬性檔案中設定,或設定為 Spring Boot 支援的任何屬性來源。 當然,您需要將應用程式自己的用戶端 ID 和密碼替換為上述 YAML 中顯示的預留位置文字。
在 OAuth 2 用戶端依賴項到位且這些屬性設定完成後,您的應用程式現在將透過 Facebook 提供驗證。 當您嘗試存取尚未驗證的頁面時,您將看到如下所示的頁面
此頁面為您提供了使用任何已配置的 OAuth 2 用戶端登入的機會。 就我們的目的而言,Facebook 是唯一的選擇。
點擊 Facebook 連結後,您將被重新導向到 Facebook。 如果您尚未登入 Facebook,系統將提示您登入。 登入後,並假設您尚未授權此應用程式,您將看到一個授權提示,如下所示
如果您選擇繼續(透過點擊「繼續」按鈕),您將被重新導向回您的應用程式並進行驗證。 (如果您選擇「取消」,您也將被重新導向回應用程式,但不會成功驗證。)
使用 Facebook 等外部服務進行驗證是傳統應用程式登入的一個不錯的替代方案。 但這只是故事的一半。 用戶登入後,您還可以使用該驗證來存取遠端服務 API 上的資源。
在使用外部 OAuth 2 服務成功驗證後,保留在安全性內容中的 Authentication
物件實際上是一個 OAuth2AuthenticationToken
,它可以與 OAuth2AuthorizedClientService
協同工作,為我們提供一個存取權杖,用於對服務的 API 提出請求。
可以透過多種方式取得 Authentication
,包括透過 SecurityContextHolder
。 取得 Authentication
後,您可以將其轉換為 OAuth2AuthenticationToken
。
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
OAuth2AuthenticationToken oauthToken =
(OAuth2AuthenticationToken) authentication;
將會有一個 OAuth2AuthorizedClientService
自動配置為 Spring 應用程式內容中的 bean,因此您只需將其注入到您將使用它的任何位置即可。
OAuth2AuthorizedClient client =
clientService.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken.getName());
String accessToken = client.getAccessToken().getTokenValue();
對 loadAuthorizedClient()
的呼叫會提供客戶端的註冊 ID,這就是在配置中註冊客戶端憑證的方式——在我們的範例中為「facebook」。 第二個參數是使用者的使用者名稱。 從本質上講,我們要求客戶端服務為給定使用者和給定服務載入 OAuth2AuthorizedClient
。 取得 OAuth2AuthorizedClient
後,只需呼叫 getAccessToken().getTokenValue()
即可請求存取權杖值。
我們可以應用此技術來充實服務的用戶端 API 繫結。 首先,我們將建立一個基本 API 繫結類別來處理確保存取權杖包含在所有請求中的基本任務
public abstract class ApiBinding {
protected RestTemplate restTemplate;
public ApiBinding(String accessToken) {
this.restTemplate = new RestTemplate();
if (accessToken != null) {
this.restTemplate.getInterceptors()
.add(getBearerTokenInterceptor(accessToken));
} else {
this.restTemplate.getInterceptors().add(getNoTokenInterceptor());
}
}
private ClientHttpRequestInterceptor
getBearerTokenInterceptor(String accessToken) {
ClientHttpRequestInterceptor interceptor =
new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] bytes,
ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add("Authorization", "Bearer " + accessToken);
return execution.execute(request, bytes);
}
};
return interceptor;
}
private ClientHttpRequestInterceptor getNoTokenInterceptor() {
return new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] bytes,
ClientHttpRequestExecution execution) throws IOException {
throw new IllegalStateException(
"Can't access the API without an access token");
}
};
}
}
ApiBinding
類別中最重要的部分是 getBearerTokenInterceptor()
方法,該方法為 RestTemplate
建立一個請求攔截器,以確保給定的存取權杖包含在所有對 API 的請求中。 但是,如果給定的存取權杖為 null
,則特殊的請求攔截器甚至不會嘗試提出 API 請求就拋出 IllegalStateException
。 對於大多數需要所有請求都經過授權的 API 來說,這是可以接受的,甚至是理想的行為。
現在我們可以根據 ApiBinding
基底類別編寫 Facebook API 繫結
public class Facebook extends ApiBinding {
private static final String GRAPH_API_BASE_URL =
"https://graph.facebook.com/v2.12";
public Facebook(String accessToken) {
super(accessToken);
}
public Profile getProfile() {
return restTemplate.getForObject(
GRAPH_API_BASE_URL + "/me", Profile.class);
}
public List<Post> getFeed() {
return restTemplate.getForObject(
GRAPH_API_BASE_URL + "/me/feed", Feed.class).getData();
}
}
如您所見,Facebook
類別非常簡單。 所有 OAuth 2 的詳細資訊都包含在 ApiBinding
中,因此該類別可以專注於提出請求以支援應用程式所需的操作。
現在我們只需要配置一個 Facebook
bean。 該 bean 將是請求範圍的,以便允許根據使用者 Authentication
中的存取權杖建立一個實例
@Configuration
public class SocialConfig {
@Bean
@RequestScope
public Facebook facebook(OAuth2AuthorizedClientService clientService) {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
String accessToken = null;
if (authentication.getClass()
.isAssignableFrom(OAuth2AuthenticationToken.class)) {
OAuth2AuthenticationToken oauthToken =
(OAuth2AuthenticationToken) authentication;
String clientRegistrationId =
oauthToken.getAuthorizedClientRegistrationId();
if (clientRegistrationId.equals("facebook")) {
OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(
clientRegistrationId, oauthToken.getName());
accessToken = client.getAccessToken().getTokenValue();
}
}
return new Facebook(accessToken);
}
}
此外,由於來自 Facebook
API 繫結的 getFeed()
方法會從使用者的 feed 擷取資料,因此我們需要在驗證使用者時設定 spring.security.oauth2.client.registration.facebook.scope
以指定「user_posts」範圍
spring:
security:
oauth2:
client:
registration:
facebook:
client-id: YOUR CLIENT ID GOES HERE
client-secret: YOUR CLIENT SECRET GOES HERE
scope: user_posts
您可能想知道這與 Spring Social 有什麼關係,Spring Social 也提供對使用外部服務登入以及 Facebook 的 API 繫結的支援。
Spring Social 透過 ProviderSignInController
和 SocialAuthenticationFilter
提供登入支援。 這兩種實作都利用 ConnectionFactory
為外部服務提供 ServiceProvider
。 Spring Social 的每個 API 繫結都必須提供 ConnectionFactory
和 ServiceProvider
的 API 特定實作。 這將 Spring Social 限制為僅支援那些具有 ConnectionFactory
和 ServiceProvider
實作的服務的登入。
相比之下,Spring Security 5 能夠透過簡單地在配置中提供服務詳細資訊來支援與幾乎任何 OAuth 2 或 OpenID Connect 服務的登入。 開箱即用,Spring Security 5 提供 Facebook、Google、GitHub 和 Okta 的基準配置(您只需要指定用戶端 ID 和密碼)。 但是,如果您必須與另一個服務整合,您只需在應用程式配置中指定服務的詳細資訊(例如授權 URL)。
至於 API 繫結,Spring Social 的 API 繫結範圍廣泛,涵蓋了其目標 API 提供的許多內容。 但實際上,大多數應用程式只需要 Spring Social 支援的操作的一小部分。 如果您只需要擷取使用者的 feed,為什麼必須使用提供數百種其他操作的大型 API 繫結? 同樣地,如果您只關心 post 回應的一個或兩個屬性,為什麼要處理一個對於 Facebook 的 Graph API 提供的內容而言是全面的 Post
物件? 在許多這種情況下,編寫您自己的 API 繫結可能會更容易,該繫結是為您的應用程式的需求量身定制的。
此外,Spring Social 的 API 繫結都使用 RestTemplate
在幕後運作。 如果您寧願使用非阻塞的反應式 API 繫結,那麼您就不走運了。 改造 API 繫結以基於 WebClient
並非易事,並且基本上會使這些 API 繫結的維護工作量增加一倍。
但是,如果您已經開發了自己的 API 繫結,則可以很容易地將 RestTemplate
替換為反應式 WebClient
,如這裡的 ReactiveApiBinding
中所示
public abstract class ReactiveApiBinding {
protected WebClient webClient;
public ReactiveApiBinding(String accessToken) {
Builder builder = WebClient.builder();
if (accessToken != null) {
builder.defaultHeader("Authorization", "Bearer " + accessToken);
} else {
builder.exchangeFunction(
request -> {
throw new IllegalStateException(
"Can't access the API without an access token");
});
}
this.webClient = builder.build();
}
}
您甚至可以在同一個 API 繫結中混合搭配 WebClient
和 RestTemplate
,在需要時應用非阻塞 WebClient
,在同步請求足夠時應用 RestTemplate
。
Spring Security 5 在用戶端對 OAuth 2 的支援,提供了透過外部服務登入以及使用從身份驗證取得的 Token 來使用該服務 API 的能力。這只是協調 Spring OAuth 方案的第一步,目前該方案分散在 Spring Social 和 Spring Security OAuth 等多個專案中。
未來版本的 Spring Security 將繼續改進 OAuth 2 的用戶端支援,並採取措施協調 Spring 在 OAuth 安全性伺服器端的故事。實際上,目前正在進行的 Spring Security 5.1.0 工作旨在使 API 的使用更加容易,有效地消除了對 ApiBinding
類別的需求,以及本文中顯示的 Facebook
Bean 配置中的大部分底層程式碼。敬請期待!