扩展 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、给定的连接存储库、消费者密钥和消费者密钥秘密以及 OAuth1Template 的实例传递进去,该实例应用于协商与提供商的身份验证。

此处给出的 OAuth1Template 使用消费者密钥和密钥秘密构造,并且还提供了我们初步研究中收集到的三个 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 参数,这些参数包含应用程序的消费者密钥/密钥秘密对和访问令牌/密钥秘密对。在这里,这些值用于创建并返回 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 的绑定。为了满足我们的直接需求,我们需要一种读取用户光盘队列的方法。为了定义该操作,我们创建了定义服务 API 的 NetFlixApi 接口


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 的 OAuth 就绪实例。由 ProtectedResourceClientFactory 创建的 RestTemplate 将设置为使用 OAuth 凭据对发出的每个请求使用“Authorization”标头进行签名。

searchForTitles()getDiscQueue() 都使用 OAuth 就绪的 RestTemplate 对 Netflix REST API 执行其各自的操作。URL 中的 output 参数告诉 Netflix API 我们希望接收 JSON 响应而不是 XML。在每种情况下,对 getForObject() 的调用都会返回一个反映 JSON 响应结构的 Map。然后从 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 绑定。但是,如果您已经有一些现有的库绑定到您希望使用的服务提供商,那么您没有理由不能使用它与提供商的 API 进行交互,同时使用 Spring Social 的服务提供商框架进行连接处理。

例如,虽然 Spring Social 带有与 Twitter 的 REST API 的 Java 绑定,但您可能更愿意使用其他绑定实现,例如 Twitter4J。Twitter4J 提供了与 Twitter 的服务 API 的全面 Java 绑定,但没有解决授权流程或连接管理。如果您想将 Twitter4J 的 API 与 Spring Social 的连接管理功能一起使用,您可以通过创建一个使用 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 示例存储库 中找到。

总结

即使 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 中的拉取请求。我们已经收到了社区的一些拉取请求,并且正在努力将它们合并到 Spring Social 中。非常感谢这些贡献!

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部