扩展 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 社区所有即将举行的活动。

查看所有