使用 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 社区的所有近期活动。

查看全部