使用 Spring Security 5 整合 OAuth 2 保護的服務,例如 Facebook 和 GitHub

工程 | Craig Walls | 2018 年 3 月 6 日 | ...

Spring Security 5 的主要功能之一是支援編寫與 OAuth 2 保護的服務整合的應用程式。 這包括透過外部服務(例如 Facebook 或 GitHub)登入應用程式的能力。

但是,透過一些額外的程式碼,您還可以取得 OAuth 2 存取權杖,該權杖可用於對服務的 API 執行授權請求。

在本文中,我們將研究如何開發一個使用 Spring Security 5 且與 Facebook 整合的 Spring Boot 應用程式。 您可以在 https://github.com/habuma/facebook-security5 找到本文的完整程式碼。

啟用 OAuth 2 登入

假設您希望讓應用程式的使用者能夠使用 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-idfacebook-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 提供驗證。 當您嘗試存取尚未驗證的頁面時,您將看到如下所示的頁面

FB Link

此頁面為您提供了使用任何已配置的 OAuth 2 用戶端登入的機會。 就我們的目的而言,Facebook 是唯一的選擇。

點擊 Facebook 連結後,您將被重新導向到 Facebook。 如果您尚未登入 Facebook,系統將提示您登入。 登入後,並假設您尚未授權此應用程式,您將看到一個授權提示,如下所示

FB Authorities

如果您選擇繼續(透過點擊「繼續」按鈕),您將被重新導向回您的應用程式並進行驗證。 (如果您選擇「取消」,您也將被重新導向回應用程式,但不會成功驗證。)

使用 Facebook 等外部服務進行驗證是傳統應用程式登入的一個不錯的替代方案。 但這只是故事的一半。 用戶登入後,您還可以使用該驗證來存取遠端服務 API 上的資源。

存取 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

更靈活的 API 繫結

您可能想知道這與 Spring Social 有什麼關係,Spring Social 也提供對使用外部服務登入以及 Facebook 的 API 繫結的支援。

Spring Social 透過 ProviderSignInControllerSocialAuthenticationFilter 提供登入支援。 這兩種實作都利用 ConnectionFactory 為外部服務提供 ServiceProvider。 Spring Social 的每個 API 繫結都必須提供 ConnectionFactoryServiceProvider 的 API 特定實作。 這將 Spring Social 限制為僅支援那些具有 ConnectionFactoryServiceProvider 實作的服務的登入。

相比之下,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 繫結中混合搭配 WebClientRestTemplate,在需要時應用非阻塞 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 配置中的大部分底層程式碼。敬請期待!

取得 Spring 電子報

隨時關注 Spring 電子報

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進制檔案。

了解更多

即將舉辦的活動

查看 Spring 社群中所有即將舉辦的活動。

查看全部