擴充套件 Spring Social 的服務提供商框架

工程 | Craig Walls | 2011年3月10日 | ...

上週,我向您介紹了 Spring Social 的服務提供商“連線”框架,並向您展示了它如何簡化使用者本地應用程式帳戶與其在軟體即服務 (SaaS) 提供商上的帳戶之間的連線建立。今天,我想向您展示如何擴充套件服務提供商框架,以處理與 Spring Social 不直接支援的提供商的連線。

為 Netflix 擴充套件 Spring Social

假設您正在開發一個電影評論網站,使用者可以在其中閱讀和釋出簡短的電影評論。通常,電影評論會顯示在主頁上,最新的條目排在最前面。但是,如果使用者將其帳戶與 Netflix 帳戶關聯,那麼您就可以向他們展示他們在 Netflix 影碟佇列中的電影評論。為了實現這一點,您希望利用 Spring Social 的服務提供商框架來連線您的使用者帳戶與他們的 Netflix 帳戶。Spring Social 1.0.0.M2 不包含 Netflix 服務提供商或 API 繫結,但可以輕鬆擴充套件以支援未直接支援的提供商。

在本文中,我將向您展示如何基於 Spring Social 的服務提供商框架,啟用與 Netflix 的連線。我們將首先開發一個 Netflix 服務提供商實現,然後構建一個簡單的 API 繫結來支援我們應用程式的需求。用於開發 Netflix 服務提供商的技術可以應用於擴充套件 Spring Social 以支援幾乎任何服務提供商。您可以檢視 GitHub 上的示例程式碼 來進行學習。

瞭解 Netflix 的授權 API

在我們開始開發 Netflix 服務提供商實現之前,我們需要做一些初步研究,以瞭解 Netflix 授權 API 的基本工作原理。

我們需要確定的第一件事是 Netflix 使用哪種授權協議。Netflix API 文件的 身份驗證概述 部分告訴我們,他們使用 OAuth,但並未明確說明使用的是哪個版本的 OAuth 規範。因此,需要進行一些偵探工作。

頁面向下滾動一點(在“那些討厭的 OAuth 引數”標題下),我們看到了關於客戶端金鑰、隨機數和時間戳的提及。這些都不是 OAuth 2 所適用的,所以 Netflix 必須是一個 OAuth 1 提供商。此外,oauth_version 引數設定為“1.0”的描述進一步證實了 Netflix 實現的是 OAuth 1。

現在我們知道 Netflix 使用 OAuth 1。但同樣重要的是要知道他們是實現了規範的 1.0 版本還是 1.0a 版本。服務提供商通常不會在其文件中明確說明這一點,而且在任何一種情況下 oauth_version 的值都應該是“1.0”。然而,有一些蛛絲馬跡表明使用了特定版本的 OAuth 規範。以下是一些表明 OAuth 1.0 正在使用的線索:

  • oauth_callback 引數在授權 URL 上傳送,而不是在請求令牌請求中傳送。
  • 沒有驗證器的概念,並且不需要將 oauth_verifier 引數傳送到訪問令牌 URL。

對於 OAuth 1.0a,請留意以下跡象:

  • oauth_callback 引數在請求令牌請求中傳送,而不是在授權 URL 中傳送。
  • 在回撥中從提供商那裡收到驗證器,並且需要將 oauth_verifier 引數傳送到訪問令牌 URL。

透過在 Netflix 文件中尋找這些線索,我們確定 Netflix 使用的是 OAuth 1.0(而不是 1.0a)。這個資訊很重要,在我們定義服務提供商實現時會很有用。

最後,我們需要知道請求令牌、授權和訪問令牌的 URL 是什麼。頁面再往下(在“發起受保護呼叫”標題下),您會找到細節,告知我們所需的 URL 如下:

  • 請求令牌 URL: http://api.netflix.com/oauth/request_token
  • 授權 URL: https://api-user.netflix.com/oauth/login
  • 訪問令牌 URL: http://api.netflix.com/oauth/access_token

請特別注意請求令牌和訪問令牌 URL 中使用的協議。大多數提供商在這方面很靈活,建議您使用 HTTPS。然而,根據我與 Netflix 打交道的經驗,我發現如果您透過 HTTPS 請求請求令牌或訪問令牌,Netflix 會抱怨請求籤名無效。不過,授權 URL 在 HTTPS 上工作正常。

開發 Netflix 服務提供商實現

要建立新的服務提供商實現,我們需要擴充套件 AbstractOAuth1ServiceProviderAbstractOAuth2ServiceProvider。這兩個類分別提供了 OAuth 1.0/1.0a 和 OAuth 2 的特定 OAuth 版本的基礎功能。由於 Netflix 是一個 OAuth 1.0 提供商,我們的 NetFlixServiceProvider 需要擴充套件 AbstractOAuth1ServiceProvider


package org.springframework.social.movies.netflix;
import org.springframework.social.connect.oauth1.AbstractOAuth1ServiceProvider;
import org.springframework.social.connect.support.ConnectionRepository;
import org.springframework.social.oauth1.OAuth1Template;

public final class NetFlixServiceProvider extends AbstractOAuth1ServiceProvider<NetFlixApi> {

    public NetFlixServiceProvider(String consumerKey, String consumerSecret, ConnectionRepository connectionRepository) {
        super("netflix", connectionRepository, consumerKey, consumerSecret, 
            new OAuth1Template(consumerKey, consumerSecret, 
                "http://api.netflix.com/oauth/request_token",
                "https://api-user.netflix.com/oauth/login?oauth_token={requestToken}" +
                    "&oauth_callback={redirectUri}&oauth_consumer_key=" + consumerKey,
                "http://api.netflix.com/oauth/access_token", 
                 OAuth1Version.CORE_10));
    }

    @Override
    protected NetFlixApi getApi(String consumerKey, String consumerSecret, String accessToken, String secret) {
        return new NetFlixTemplate(consumerKey, consumerSecret, accessToken, secret);
    }
	
}

擴充套件 Spring Social 的抽象服務提供商類時,您必須做兩件事:在建構函式中設定提供商的特定資訊,並實現 getApi() 方法。

抽象基類包含了與服務提供商連線的所有機制。但您必須透過將提供商的特定資訊傳遞給 super() 建構函式來設定它。在這裡,NetFlixServiceProvider 建構函式呼叫 super() 建構函式,傳入“netflix”作為提供商 ID,給定的連線儲存庫,客戶端 ID 和客戶端金鑰,以及一個 OAuth1Template 例項,該例項應被用於協商與提供商的身份驗證。

此處提供的 OAuth1Template 使用客戶端 ID 和金鑰構建,並且還接收了我們在初步研究期間收集到的三個 URL(請求令牌、授權和訪問令牌)。請注意,授權 URL 被引數化以接受請求令牌和重定向 URI。ConnectController 將在授權流程中提供這些詳細資訊。另請注意,授權 URL 還接受一個 oauth_consumer_key 引數。這似乎是 Netflix 的一項特定要求;OAuth 1.0 規範沒有此類要求,而且我還沒有遇到過其他需要此引數的提供商。

大多數 OAuth 1 服務提供商都實現了 OAuth 1.0a 規範。因此,OAuth1Template 預設假定它將處理 OAuth 1.0a。然而,Netflix 是一個基於 OAuth 1.0 的提供商。傳遞給 OAuth1Template 建構函式的最後一個引數指定了它不應假定為 1.0a,而應按照 OAuth 1.0 條款與提供商協商。如果 Netflix 是一個 OAuth 1.0a 提供商,此引數可以設定為 OAuth1Version.CORE_10_REVISION_A 或完全省略。

服務提供商實現所需的另一個要求是實現 getApi() 方法。對於 OAuth 1 提供商,此方法接收四個 String 引數,包含應用程式的客戶端 ID/金鑰對和訪問令牌/金鑰對。在這裡,這些值用於建立並返回一個 NetFlixTemplate 的新例項(稍後將詳細介紹此類)。

雖然 NetFlixServiceProvider 只演示瞭如何為 OAuth 1 開發服務提供商實現,但擴充套件 AbstractOAuth2ServiceProvider 來建立 OAuth 2 服務提供商的模式差別不大。主要區別在於:

  • 客戶端 ID 和金鑰不會透過 super() 建構函式傳遞。
  • 建立的是 OAuth2Template 例項而不是 OAuth1Template(並且不需要請求令牌 URL)。
  • getApi() 方法僅接收訪問令牌值來構建 API 繫結。

請檢視 FacebookServiceProviderGitHubServiceProviderGowallaServiceProvider 來了解如何建立基於 OAuth 2 的服務提供商實現。有關更多 OAuth 1 服務提供商的示例,您還可以檢視 TwitterServiceProviderLinkedInServiceProviderTripItServiceProvider

建立 Netflix API 繫結

隨著服務提供商實現的完成,我們現在將注意力轉向建立 Netflix REST API 的繫結。對於我們當前的需求,我們需要一種方法來讀取使用者的影碟佇列。為了定義該操作,我們建立了 NetFlixApi 介面,該介面定義了服務 API。


public interface NetFlixApi {

    List<CatalogTitle> searchForTitles(String searchTerms);

    List<QueueItem> getDiscQueue();

}

這遠非對 Netflix REST API 的完整繫結。但它足以滿足我們的目的。searchForTitles() 方法可用於幫助使用者選擇他們想寫評論的電影。getDiscQueue() 方法將用於檢索使用者影碟佇列中的專案。現在我們需要建立一個實現類。NetFlixTemplate 使用 Spring 的 RestTemplate 來呼叫 Netflix 的 REST API。


package org.springframework.social.netflix;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.social.oauth1.ProtectedResourceClientFactory;
import org.springframework.web.client.RestTemplate;

public class NetFlixTemplate implements NetFlixApi {

    private final RestTemplate restTemplate;

    private final String userBaseUrl;

    public NetFlixTemplate(String apiKey, String apiSecret, String accessToken, 
            String accessTokenSecret) {
        this.restTemplate = 
                ProtectedResourceClientFactory.create(apiKey, apiSecret, accessToken, accessTokenSecret);
        this.userBaseUrl = getUserBaseUrl();
    }

    public List<CatalogTitle> searchForTitles(String searchTerm) {
        Map<String, Object> resultMap = restTemplate.getForObject(SEARCH_TITLES_URL, Map.class, searchTerm);
        List<CatalogTitle> titles = new ArrayList<CatalogTitle>();

        // extract CatalogTitle objects from resultMap

        return titles;
    }

    public List<QueueItem> getDiscQueue() {
        Map<String, Object> resultMap = restTemplate.getForObject(userBaseUrl + QUEUE_PATH, Map.class);
        List<QueueItem> queueItems = new ArrayList<QueueItem>();

        // extract QueueItem objects from resultMap

        return queueItems;
    }

    private String getUserBaseUrl() {
        Map<String, Map<String, Map<String, String>>> result = 
                restTemplate.getForObject(CURRENT_USER_URL, Map.class);
        return result.get("resource").get("link").get("href");
    }

    private static final String SEARCH_TITLES_URL = 
            "http://api.netflix.com/catalog/titles?term={term}&max_results=5&output=json";
    
    private static final String CURRENT_USER_URL = 
            "http://api.netflix.com/users/current?output=json";
    
    private static final String QUEUE_PATH = "/queues/disc?output=json";
}

請注意,儘管 NetFlixTemplate 使用 RestTemplate,但它並沒有為自己建立 RestTemplate 例項。相反,它使用 ProtectedResourceClientFactory 來建立一個可用的 RestTemplate 例項。由 ProtectedResourceClientFactory 建立的 RestTemplate 將設定為使用 OAuth 憑據為它發出的每個請求籤名,並帶有“Authorization”頭。

searchForTitles()getDiscQueue() 都使用可用的 RestTemplate 來針對 Netflix REST API 執行各自的操作。URL 中的 output 引數告訴 Netflix API,我們更希望收到 JSON 響應而不是 XML。在每種情況下,呼叫 getForObject() 都會返回一個 Map,該 Map 映象了 JSON 響應的結構。然後從 Map 中提取相關資訊,以生成返回給呼叫者的列表。(為簡潔起見,我已將 Map 解析的細節從上面的列表省略了。請檢視 GitHub 以獲取 NetFlixTemplate 的完整實現。)

Netflix REST API 中所有面向用戶的操作,包括檢索使用者影碟佇列的呼叫,其 URL 都以“http://api.netflix.com/users/{user ID}”開頭。雖然 NetFlixTemplate 無法輕鬆獲取使用者的 Netflix ID,但可以透過“/users/current”API 呼叫檢索使用者的基本 URL(包括他們的 Netflix ID)。getUserBaseUrl() 方法呼叫“/users/current”來檢索使用者的基本 URL。為了避免在每次呼叫前都檢索基本 URL,建構函式會呼叫一次 getUserBaseUrl() 方法,並將基本 URL 儲存在成員變數中,以便在構建面向使用者的操作的 URL 時使用。

現在我們已經有了 Netflix 服務提供商和 API 繫結,我們可以圍繞它們構建其餘的電影評論應用程式。為了說明 getDiscQueue() 方法可能如何使用,請看下面螢幕截圖的右側欄。

在這裡,顯示了使用者影碟佇列中的電影列表以及這些電影的近期評論。此時,很容易設想對此應用程式進行進一步增強,例如允許使用者在考慮其他使用者的評論時修改他們的佇列。

使用現有的 API 繫結

在 Netflix 示例中,我選擇建立自己的 API 繫結。但是,如果已經存在一個繫結到您喜歡的服務提供商的庫,那麼您沒有理由不能在 Spring Social 的服務提供商框架進行連線處理的同時,使用它來與提供商的 API 進行互動。

例如,儘管 Spring Social 提供了對 Twitter REST API 的 Java 繫結,但您可能更喜歡使用另一個繫結實現,如 Twitter4J。Twitter4J 提供了對 Twitter 服務 API 的全面 Java 繫結,但它不處理授權流程或連線管理。如果您想在 Spring Social 的連線管理功能的同時使用 Twitter4J 的 API,您可以透過建立一個使用 Twitter4J 作為 API 繫結的服務提供商來實現。

為此,您需要建立一個服務提供商實現,其 getApi() 方法使用 TwitterFactory 來構建 Twitter4J 例項,而不是 TwitterTemplate。以下是基於 Twitter4J 的服務提供商實現可能的樣子:


package org.springframework.social.showcase.twitter;
import java.util.Properties;
import org.springframework.social.connect.oauth1.AbstractOAuth1ServiceProvider;
import org.springframework.social.connect.support.ConnectionRepository;
import org.springframework.social.oauth1.OAuth1Template;
import twitter4j.Twitter;
import twitter4j.TwitterFactory;
import twitter4j.conf.Configuration;
import twitter4j.conf.PropertyConfiguration;

public final class Twitter4JServiceProvider extends AbstractOAuth1ServiceProvider<Twitter> {

    public Twitter4JServiceProvider(String consumerKey, String consumerSecret, ConnectionRepository connectionRepository) {
        super("twitter", connectionRepository, consumerKey, consumerSecret, new OAuth1Template(consumerKey, consumerSecret,
            "https://twitter.com/oauth/request_token",
            "https://twitter.com/oauth/authorize?oauth_token={requestToken}",
            "https://twitter.com/oauth/access_token"));
    }

    @Override
    protected Twitter getApi(String consumerKey, String consumerSecret, String accessToken, String secret) {
        Properties props = new Properties();
        props.setProperty(PropertyConfiguration.OAUTH_CONSUMER_KEY, consumerKey);
        props.setProperty(PropertyConfiguration.OAUTH_CONSUMER_SECRET, consumerSecret);
        props.setProperty(PropertyConfiguration.OAUTH_ACCESS_TOKEN, accessToken);
        props.setProperty(PropertyConfiguration.OAUTH_ACCESS_TOKEN_SECRET, secret);
        Configuration conf = new PropertyConfiguration(props);
        return new TwitterFactory(conf).getInstance();
    }

}

如您所見,Twitter4JServiceProvider 看起來與 Spring Social 的 TwitterServiceProvider 非常相似,也與之前建立的 NetFlixServiceProvider 非常相似。關鍵區別在於 Twitter4JServiceProvider 被引數化為 Twitter 服務提供商,並且 getApi() 方法構建了一個 Twitter4J Twitter 例項。

Twitter4JServiceProvider 的程式碼以及使用它的示例可以在 GitHub 的 Spring Social Samples 儲存庫 中找到。

總結

儘管 Spring Social 1.0.0.M2 主要關注少數精選的 SaaS 提供商,但其服務提供商框架易於擴充套件,使您能夠基於 Spring Social 構建對其他提供商的支援。此外,該框架不僅限於為 Spring Social 特定的 API 繫結開發服務提供商實現——您還可以使用它來為現有的 API 繫結建立連線。

在談論擴充套件 Spring Social 時,您可能還想探索的另一個領域是建立 ConnectionRepository 介面的新實現。Spring Social 1.0.0.M2 提供了一個基於 JDBC 的實現,但還有其他持久化連線的可能性。例如,Spring Android 專案定義了一個 SqliteConnectionRepository,允許將連線寫入儲存在 Android 裝置本地的 SQLite 資料庫。另外,看看 NoSQL 連線儲存庫會是什麼樣子也會很有趣。

我們期待看到您如何擴充套件 Spring Social。如果您建立了一個有用或有趣的 Spring Social 擴充套件,請在 論壇 中告訴我們,或者向 GitHub 傳送一個 pull request。我們已經收到了社群的一些 pull request,並且正在努力將它們整合到 Spring Social 中。非常感謝這些貢獻!

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有