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

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

Spring Security 5 的一個關鍵特性是支援編寫與受 OAuth 2 保護的服務整合的應用程式。這包括能夠透過外部服務(如 Facebook 或 GitHub)登入應用程式。

但是,只需少量額外程式碼,您還可以獲得一個 OAuth 2 訪問令牌,可用於對服務的 API 執行授權請求。

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

啟用 OAuth 2 登入

假設您希望允許應用程式使用者使用 Facebook 登入。使用 Spring Security 5,這非常簡單。您只需在專案構建中新增 Spring Security 的 OAuth 2 客戶端支援,然後配置應用程式的 Facebook 憑據即可。

首先,將 Spring Security OAuth 2 客戶端庫新增到您的 Spring Boot 專案構建中,以及 Spring Security starter 依賴項

<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;

Spring 應用程式上下文會自動將 OAuth2AuthorizedClientService 配置為一個 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() 方法從使用者的動態中獲取資料,我們需要設定 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 支援的一小部分操作。如果您只需要獲取使用者的動態,為什麼必須使用一個提供數百種其他操作的大型 API 繫結?同樣,如果您只關心帖子響應的一兩個屬性,為什麼需要處理一個全面對應 Facebook Graph API 功能的 Post 物件?在許多這樣的情況下,編寫一個量身定製以滿足您的應用程式需求的 API 繫結可能會更容易。

此外,Spring Social 的 API 繫結都在底層使用了 RestTemplate。如果您更喜歡使用非阻塞的響應式 API 繫結,那就不行了。改造 API 繫結使其基於 WebClient 並非易事,而且基本上會使這些 API 繫結的維護工作量增加一倍。

但是,如果您自己開發了一個 API 繫結,那麼用響應式 WebClient 替換 RestTemplate 就很容易了,如這裡的 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 支援提供了透過外部服務登入的能力,以及使用從身份驗證中獲得的令牌來使用該服務的 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 社群的所有近期活動。

檢視全部