使用 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 Security 启动器依赖项添加到 Spring Boot 项目的构建中。

<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 支持的任何属性源。当然,您需要将上面 YAML 中显示的占位符文本替换为您应用程序自己的客户端 ID 和密钥。

有了 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,则特殊的请求拦截器将抛出 IllegalStateException,甚至不会尝试发出 API 请求。对于大多数需要所有请求都经过授权的 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() 方法从用户的 feed 中获取数据,所以我们需要将 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 支持的操作的一小部分。如果您只需要获取用户的 feed,为什么还要使用提供数百个其他操作的大型 API 绑定呢?同样,如果您只关心帖子响应的一个或两个属性,为什么还要处理一个对 Facebook 的 Graph API 提供的内容非常全面的 Post 对象呢?在许多这样的情况下,编写您自己的 API 绑定可能更容易,并且可以根据应用程序的需求进行定制。

此外,Spring Social 的所有 API 绑定都在后台使用 RestTemplate。如果您想使用非阻塞式反应式 API 绑定,那么您就运气不好了。将 API 绑定改造为基于 WebClient 并非易事,这实际上会使这些 API 绑定的维护工作量增加一倍。

但是,如果您自己开发了一个 API 绑定,则可以轻松地将 RestTemplate 替换为反应式 WebClient,如下所示的 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类的需求,以及本文中显示的Facebookbean配置中的大部分管道代码。敬请期待!

获取 Spring Newsletter

关注 Spring Newsletter

订阅

领先一步

VMware 提供培训和认证,助您快速提升。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部