Spring Security and OpenID Connect – Spring安全和OpenID连接

最后修改: 2017年 3月 8日

中文/混合/英文(键盘快捷键:t)

Note that this article has been updated to the new Spring Security OAuth 2.0 stack. The tutorial using the legacy stack is still available, though.

请注意,本文已更新为新的 Spring Security OAuth 2.0 栈。不过,使用传统栈的教程仍然可用。

1. Overview

1.概述

In this tutorial, we’ll focus on setting up OpenID Connect (OIDC) with Spring Security.

在本教程中,我们将重点讨论如何用Spring Security设置OpenID Connect(OIDC)。

We’ll present different aspects of this specification, and then we’ll see the support that Spring Security offers to implement it on an OAuth 2.0 Client.

我们将介绍该规范的不同方面,然后我们将看到Spring Security为在OAuth 2.0客户端上实现该规范而提供的支持。

2. Quick OpenID Connect Introduction

2.快速的OpenID连接介绍

OpenID Connect is an identity layer built on top of the OAuth 2.0 protocol.

OpenID Connect是一个建立在OAuth 2.0协议之上的身份层。

So, it’s really important to know OAuth 2.0 before diving into OIDC, especially the Authorization Code flow.

因此,在深入研究OIDC之前,了解OAuth 2.0真的很重要,特别是授权代码流程。

The OIDC specification suite is extensive. It includes core features and several other optional capabilities, presented in different groups. Here are the main ones:

OIDC规范套件是广泛的。它包括核心功能和其他一些可选功能,以不同的组别呈现。以下是主要的功能。

  • Core – authentication and use of Claims to communicate End User information
  • Discovery – stipulate how a client can dynamically determine information about OpenID Providers
  • Dynamic Registration – dictate how a client can register with a provider
  • Session Management – define how to manage OIDC sessions

On top of this, the documents distinguish the OAuth 2.0 Authentication Servers that offer support for this spec, referring to them as OpenID Providers (OPs) and the OAuth 2.0 Clients that use OIDC as Relying Parties (RPs). We’ll be using this terminology in this article.

在此基础上,这些文件区分了为该规范提供支持的OAuth 2.0认证服务器,将其称为OpenID提供商(OP),将使用OIDC的OAuth 2.0客户端称为信赖方(RP)。我们将在本文中使用这一术语。

It’s also worth noting that a client can request the use of this extension by adding the openid scope in its Authorization Request.

还值得注意的是,客户可以通过在其授权请求中添加openid范围来请求使用这个扩展。

Finally, for this tutorial, it’s useful to know that the OPs emit End User information as a JWT called an ID Token.

最后,对于本教程来说,知道OPs以JWT的形式发射终端用户信息是非常有用的,它被称为ID Token。

Now we’re ready to dive deeper into the OIDC world.

现在我们准备深入到OIDC的世界。

3. Project Setup

3.项目设置

Before focusing on the actual development, we’ll have to register an OAuth 2.0 Client with our OpenID Provider.

在专注于实际开发之前,我们必须向我们的OpenID提供者注册一个OAuth 2.0客户端。

In this case, we’ll use Google as the OpenID Provider. We can follow these instructions to register our client application on their platform. Notice that the openid scope is present by default.

在这种情况下,我们将使用谷歌作为OpenID提供商。我们可以按照这些说明,在他们的平台上注册我们的客户应用程序。注意,openid范围默认是存在的。

The Redirect URI we set up in this process is an endpoint in our service: http://localhost:8081/login/oauth2/code/google.

我们在这个过程中设置的重定向URI是我们服务中的一个端点。http://localhost:8081/login/oauth2/code/google

We should obtain a Client ID and a Client Secret from this process.

我们应该从这个过程中获得一个客户ID和一个客户秘密。

3.1. Maven Configuration

3.1.Maven配置

We’ll start by adding these dependencies to our project pom file:

我们将首先把这些依赖项添加到我们的项目pom文件中。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

The starter artifact aggregates all Spring Security Client-related dependencies, including

起动器集合了所有与Spring Security Client相关的依赖关系,包括

  • the spring-security-oauth2-client dependency for OAuth 2.0 Login and Client functionality
  • the JOSE library for JWT support

As usual, we can find the latest version of this artifact using the Maven Central search engine.

像往常一样,我们可以使用Maven Central搜索引擎找到该工件的最新版本。

4. Basic Configuration Using Spring Boot

4.使用Spring Boot的基本配置

First, we’ll start by configuring our application to use the client registration we just created with Google.

首先,我们将开始配置我们的应用程序,以使用我们刚刚在谷歌创建的客户端注册。

Using Spring Boot makes this very easy since all we have to do is define two application properties:

使用Spring Boot让这一切变得非常容易,因为我们所要做的就是定义两个应用程序属性

spring:
  security:
    oauth2:
      client:
        registration: 
          google: 
            client-id: <client-id>
            client-secret: <secret>

Let’s launch our application and try to access an endpoint now. We’ll see that we get redirected to a Google Login page for our OAuth 2.0 Client.

现在让我们启动我们的应用程序并尝试访问一个端点。我们会看到,我们的OAuth 2.0客户端被重定向到一个谷歌登录页面。

It looks really simple, but there are quite a lot of things going on under the hood here. Next, we’ll explore how Spring Security pulls this off.

它看起来真的很简单,但在这里有相当多的事情是在引擎盖下进行的。接下来,我们将探讨Spring Security是如何做到这一点的。

Formerly, in our WebClient and OAuth 2 Support post, we analyzed the internals on how Spring Security handles OAuth 2.0 Authorization Servers and Clients.

之前,在我们的WebClient和OAuth 2支持帖子中,我们分析了Spring Security如何处理OAuth 2.0授权服务器和客户端的内部情况。

There we saw that we have to provide additional data, apart from the Client ID and the Client Secret, to configure a ClientRegistration instance successfully.

在那里我们看到,除了客户ID和客户秘密,我们必须提供额外的数据,以成功配置ClientRegistration 实例。

So, how is this working?

那么,这是如何工作的呢?

Google is a well-known provider, and therefore the framework offers some predefined properties to make things easier.

谷歌是一个著名的供应商,因此该框架提供了一些预定义的属性以使事情变得更容易。

We can have a look at those configurations in the CommonOAuth2Provider enum.

我们可以在CommonOAuth2Providerenum中查看这些配置。

For Google, the enumerated type defines properties such as

对于谷歌来说,枚举类型定义了如下属性

  • the default scopes that will be used
  • the Authorization endpoint
  • the Token endpoint
  • the UserInfo endpoint, which is also part of the OIDC Core specification

4.1. Accessing User Information

4.1.访问用户信息

Spring Security offers a useful representation of a user Principal registered with an OIDC Provider, the OidcUser entity.

Spring Security为在OIDC提供商处注册的用户主体提供了一个有用的表示,即OidcUser 实体。

Apart from the basic OAuth2AuthenticatedPrincipal methods, this entity offers some useful functionality:

除了基本的OAuth2AuthenticatedPrincipal方法外,这个实体还提供了一些有用的功能。

  • Retrieve the ID Token value and the Claims it contains
  • Obtain the Claims provided by the UserInfo endpoint
  • Generate an aggregate of the two sets

We can easily access this entity in a controller:

我们可以在控制器中轻松访问这个实体。

@GetMapping("/oidc-principal")
public OidcUser getOidcUserPrincipal(
  @AuthenticationPrincipal OidcUser principal) {
    return principal;
}

Or we can use the SecurityContextHolder in a bean:

或者我们可以在一个bean中使用SecurityContextHolder

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
    OidcUser principal = ((OidcUser) authentication.getPrincipal());
    
    // ...
}

If we inspect the principal, we’ll see a lot of useful information here, such as the user’s name, email, profile picture and locale.

如果我们检查本金,我们会在这里看到很多有用的信息,如用户的姓名、电子邮件、个人资料图片和地区。

Furthermore, it’s important to note that Spring adds authorities to the principal based on the scopes it received from the provider, prefixed with “SCOPE_“. For example, the openid scope becomes a SCOPE_openid granted authority.

此外,需要注意的是,Spring根据它从提供者那里收到的作用域向委托人添加权限,前缀为”SCOPE_“。例如,openid作用域成为SCOPE_openid 授予的权限。

These authorities can be used to restrict access to certain resources:

这些授权可以用来限制对某些资源的访问。

@EnableWebSecurity
public class MappedAuthorities {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
          .authorizeRequests(authorizeRequests -> authorizeRequests
            .mvcMatchers("/my-endpoint")
              .hasAuthority("SCOPE_openid")
            .anyRequest().authenticated()
          );
        return http.build();
    }
}

5. OIDC in Action

5.行动中的OIDC

So far, we’ve learned how we can easily implement an OIDC Login solution using Spring Security.

到目前为止,我们已经学会了如何使用Spring Security轻松实现OIDC登录解决方案。

We’ve seen the benefit it carries by delegating the user identification process to an OpenID Provider, which in turn supplies detailed useful information, even in a scalable manner.

我们已经看到了它所带来的好处,它将用户识别过程委托给OpenID提供者,而OpenID提供者则提供详细的有用信息,甚至以可扩展的方式。

But the truth is that we didn’t have to deal with any OIDC-specific aspect so far. This means that Spring is doing most of the work for us.

但事实是,到目前为止,我们并没有必要处理任何OIDC特有的方面。这意味着Spring正在为我们做大部分的工作。

So, let’s look at what’s going on behind the scenes to understand better how this specification is put into action and be able to get the most out of it.

因此,让我们看看幕后发生了什么,以便更好地了解这一规范是如何付诸实施的,并能够从中获得最大的收益。

5.1. The Login Process

5.1.登录过程

In order to see this clearly, let’s enable the RestTemplate logs to see the requests the service is performing:

为了清楚地看到这一点,让我们启用RestTemplate日志,以看到服务正在执行的请求。

logging:
  level:
    org.springframework.web.client.RestTemplate: DEBUG

If we call a secured endpoint now, we’ll see the service is carrying out the regular OAuth 2.0 Authorization Code Flow. That’s because, as we said, this specification is built on top of OAuth 2.0.

如果我们现在调用一个安全的端点,我们会看到该服务正在执行常规的OAuth 2.0授权代码流。这是因为,正如我们所说,这个规范是建立在OAuth 2.0之上的。

There are some differences.

有一些区别。

First, depending on the provider we’re using and the scopes we’ve configured, we might see that the service is making a call to the UserInfo endpoint we mentioned at the beginning.

首先,根据我们所使用的提供者和我们所配置的作用域,我们可能会看到服务正在调用我们在开始时提到的 UserInfo 端点。

Namely, if the Authorization Response retrieves at least one of profile, email, address or phone scope, the framework will call the UserInfo endpoint to obtain additional information.

也就是说,如果授权响应至少检索了profileemailaddressphonescope中的一个,框架将调用UserInfo端点以获得额外的信息。

Even though everything would indicate that Google should retrieve the profile and the email scope — since we’re using them in the Authorization Request — the OP retrieves their custom counterparts instead, https://www.googleapis.com/auth/userinfo.email and https://www.googleapis.com/auth/userinfo.profile, so Spring doesn’t call the endpoint.

尽管一切都表明Google应该检索profile email 范围–因为我们在授权请求中使用了它们–但OP检索了它们的自定义对应项,https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile,所以Spring没有调用这个端点。

This means that all the information we’re obtaining is part of the ID Token.

这意味着我们获得的所有信息都是ID Token的一部分。

We can adapt to this behavior by creating and providing our own OidcUserService instance:

我们可以通过创建和提供我们自己的OidcUserService实例来适应这种行为。

@Configuration
public class OAuth2LoginSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add("https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add("https://www.googleapis.com/auth/userinfo.profile");

        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);

        http.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest()
            .authenticated())
            .oauth2Login(oauthLogin -> oauthLogin.userInfoEndpoint()
                .oidcUserService(googleUserService));
        return http.build();
    }
}

The second difference we’ll observe is a call to the JWK Set URI. As we explained in our JWS and JWK post, this is used to verify the JWT-formatted ID Token signature.

我们将观察到的第二个区别是对JWK Set URI的调用。正如我们在我们的JWS和JWK帖子中所解释的那样,这被用来验证JWT格式的ID令牌签名。

Next, we’ll analyze the ID Token in detail.

接下来,我们将详细分析ID Token的情况。

5.2. The ID Token

5.2.ID令牌

Naturally, the OIDC spec covers and adapts to a lot of different scenarios. In this case, we’re using the Authorization Code flow, and the protocol indicates that both the Access Token and the ID Token will be retrieved as part of the Token Endpoint response.

当然,OIDC规范涵盖并适应了许多不同的场景。在这种情况下,我们使用的是授权代码流,而协议表明访问令牌和ID令牌将作为令牌端点响应的一部分被检索。

As we said before, the OidcUser entity contains the Claims contained in the ID Token, and the actual JWT-formatted token, which can be inspected using jwt.io.

正如我们之前所说,OidcUser实体包含ID令牌中包含的Claims,以及实际的JWT格式的令牌,可以使用jwt.io进行检查。

On top of this, Spring offers many handy getters to obtain the standard Claims defined by the specification in a clean manner.

在此基础上,Spring提供了许多方便的getters,以简洁的方式获得规范所定义的标准Claims。

We can see the ID Token includes some mandatory Claims:

我们可以看到ID Token包括一些强制性的索赔。

  • The issuer identifier formatted as a URL (e.g., “https://accounts.google.com“)
  • A subject id, which is a reference of the End User contained by the issuer
  • The expiration time for the token
  • Time at which the token was issued
  • The audience, which will contain the OAuth 2.0 Client ID we’ve configured

It also contains many OIDC Standard Claims such as the ones we mentioned before (name, locale, picture, email).

它还包含许多OIDC标准声明,如我们之前提到的那些(姓名地域照片电子邮件)。

As these are standard, we can expect many providers to retrieve at least some of these fields and therefore facilitate the development of simpler solutions.

由于这些是标准的,我们可以预期许多供应商至少会检索其中的一些字段,从而促进更简单的解决方案的开发。

5.3. Claims and Scopes

5.3.要求和范围

As we can imagine, the Claims that are retrieved by the OP correspond with the scopes we (or Spring Security) configured.

我们可以想象,被OP检索到的Claims与我们(或Spring Security)配置的作用域相对应。

OIDC defines some scopes that can be used to request the Claims defined by OIDC:

OIDC定义了一些范围,可以用来请求OIDC定义的索赔。

  • profile, which can be used to request default profile Claims (e.g., name, preferred_usernamepicture, etc.)
  • email, to access to the email and email_verified Claims
  • address
  • phone, to request the phone_number and phone_number_verified Claims

Even though Spring doesn’t support it yet, the spec allows requesting single Claims by specifying them in the Authorization Request.

尽管Spring还不支持它,但该规范允许通过在授权请求中指定单一的Claims来请求它们。

6. Spring Support for OIDC Discovery

6.Spring对OIDC发现的支持

As we explained in the introduction, OIDC includes many different features apart from its core purpose.

正如我们在介绍中所解释的,OIDC除了其核心目的外,还包括许多不同的功能。

The capabilities we’re going to analyze in this section and the following are optional in OIDC. So, it’s important to understand that there might be OPs that don’t support them.

我们在本节和下面要分析的能力在OIDC中是可选的。因此,必须了解可能有一些OP不支持它们。

The specification defines a Discovery mechanism for an RP to discover the OP and obtain information needed to interact with it.

该规范定义了一种发现机制,供RP发现OP并获得与之互动所需的信息。

In a nutshell, OPs provide a JSON document of standard metadata. The information must be served by a well-known endpoint of the issuer location, /.well-known/openid-configuration.

简而言之,OP提供一个标准元数据的JSON文档。这些信息必须由发行人位置的知名端点提供,/.known/openid-configuration

Spring benefits from this by allowing us to configure a ClientRegistration with just one simple property, the issuer location.

Spring从中受益,它允许我们只用一个简单的属性来配置ClientRegistration,即发行者的位置。

But let’s jump right into an example to see this clearly.

但是,让我们直接跳到一个例子中来清楚地看到这一点。

We’ll define a custom ClientRegistration instance:

我们将定义一个自定义的ClientRegistrationinstance。

spring:
  security:
    oauth2:
      client:
        registration: 
          custom-google: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          custom-google:
            issuer-uri: https://accounts.google.com

Now we can restart our application and check the logs to confirm the application is calling the openid-configuration endpoint in the startup process.

现在我们可以重新启动我们的应用程序,并检查日志以确认应用程序在启动过程中调用openid-configuration端点。

We can even browse this endpoint to have a look at the information provided by Google:

我们甚至可以浏览这个端点,看一看谷歌提供的信息。

https://accounts.google.com/.well-known/openid-configuration

https://accounts.google.com/.well-known/openid-configuration

We can see, for example, the Authorization, the Token and the UserInfo endpoints that the service has to use, and the supported scopes.

例如,我们可以看到服务必须使用的授权、令牌和UserInfo端点,以及支持的范围。

It’s especially relevant to note here that if the Discovery endpoint is not available when the service launches, our app won’t be able to complete the startup process successfully.

这里特别需要注意的是,如果在服务启动时发现端点不可用,我们的应用程序将无法成功完成启动过程。

7. OpenID Connect Session Management

7.OpenID连接会话管理

This specification complements the Core functionality by defining the following:

本规范通过定义以下内容对核心功能进行补充。

  • Different ways to monitor the End User’s login status at the OP on an ongoing basis so that the RP can log out an End User who has logged out of the OpenID Provider
  • The possibility of registering RP logout URIs with the OP as part of the Client registration, in order to be notified when the End User logs out of the OP
  • A mechanism to notify the OP that the End User has logged out of the site and might want to log out of the OP as well

Naturally, not all OPs support all of these items, and some of these solutions can be implemented only in a front-end implementation via the User-Agent.

当然,不是所有的OP都支持所有这些项目,其中一些解决方案只能通过用户代理在前端实现。

In this tutorial, we’ll focus on the capabilities offered by Spring for the last item of the list, RP-initiated Logout.

在本教程中,我们将重点讨论Spring为列表中最后一项提供的功能,即RP发起的注销。

At this point, if we log in to our application, we can normally access every endpoint.

在这一点上,如果我们登录到我们的应用程序,我们通常可以访问每一个端点。

If we log out (calling the /logout endpoint) and we make a request to a secured resource afterward, we’ll see that we can get the response without having to log in again.

如果我们注销(调用/logout端点),并在之后向安全资源发出请求,我们会看到我们可以得到响应,而无需再次登录。

However, this is actually not true. If we inspect the Network tab in the browser debug console, we’ll see that when we hit the secured endpoint the second time, we get redirected to the OP Authorization Endpoint. And since we’re still logged in there, the flow is completed transparently, ending up in the secured endpoint almost instantly.

然而,这实际上是不正确的。如果我们检查浏览器调试控制台中的网络标签,我们会看到当我们第二次点击安全端点时,我们会被重定向到OP授权端点。因为我们仍然在那里登录,所以流程是透明地完成的,几乎瞬间就结束在安全端点。

Of course, this might not be the desired behavior in some cases. Let’s see how we can implement this OIDC mechanism to deal with this.

当然,在某些情况下,这可能不是我们想要的行为。让我们看看我们如何实现这个OIDC机制来处理这个问题。

7.1. The OpenID Provider Configuration

7.1.OpenID提供商配置

In this case, we’ll be configuring and using an Okta instance as our OpenID Provider. We won’t go into details on how to create the instance, but we can follow the steps of this guide, keeping in mind that Spring Security’s default callback endpoint will be /login/oauth2/code/okta.

在这种情况下,我们将配置并使用一个Okta实例作为我们的OpenID提供者。我们不会详述如何创建该实例,但我们可以遵循本指南的步骤,同时牢记 Spring Security 的默认回调端点将是 /login/oauth2/code/okta

In our application, we can define the client registration data with properties:

在我们的应用程序中,我们可以用属性定义客户注册数据。

spring:
  security:
    oauth2:
      client:
        registration: 
          okta: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          okta:
            issuer-uri: https://dev-123.okta.com

OIDC indicates that the OP logout endpoint can be specified in the Discovery document, as the end_session_endpoint element.

OIDC指出,可以在发现文件中指定OP注销端点,作为end_session_endpoint元素。

7.2. The LogoutSuccessHandler Configuration

7.2.LogoutSuccessHandler配置

Next, we’ll have to configure the HttpSecurity logout logic by providing a customized LogoutSuccessHandler instance:

接下来,我们必须通过提供一个定制的LogoutSuccessHandler实例来配置HttpSecurity注销逻辑。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
      .authorizeRequests(authorizeRequests -> authorizeRequests
        .mvcMatchers("/home").permitAll()
        .anyRequest().authenticated())
      .oauth2Login(oauthLogin -> oauthLogin.permitAll())
      .logout(logout -> logout
        .logoutSuccessHandler(oidcLogoutSuccessHandler()));
    return http.build();
}

Now let’s see how we can create a LogoutSuccessHandler for this purpose using a special class provided by Spring Security, the OidcClientInitiatedLogoutSuccessHandler:

现在让我们看看如何使用Spring Security提供的一个特殊类–OidcClientInitiatedLogoutSuccessHandler来创建一个LogoutSuccessHandler

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

private LogoutSuccessHandler oidcLogoutSuccessHandler() {
    OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
      new OidcClientInitiatedLogoutSuccessHandler(
        this.clientRegistrationRepository);

    oidcLogoutSuccessHandler.setPostLogoutRedirectUri(
      URI.create("http://localhost:8081/home"));

    return oidcLogoutSuccessHandler;
}

Consequently, we’ll need to set up this URI as a valid logout Redirect URI in the OP Client configuration panel.

因此,我们需要在OP客户端配置面板中把这个URI设置为一个有效的注销重定向URI。

Clearly, the OP logout configuration is contained in the client registration setup since all we’re using to configure the handler is the ClientRegistrationRepository bean present in the context.

显然,OP的注销配置包含在客户端注册设置中,因为我们用来配置处理程序的是上下文中的ClientRegistrationRepositorybean。

So, what will happen now?

那么,现在会发生什么?

After we log in to our application, we can send a request to the /logout endpoint provided by Spring Security.

在我们登录到我们的应用程序后,我们可以向Spring Security提供的/logout端点发送一个请求。

If we check the Network logs in the browser debug console, we’ll see we got redirected to an OP logout endpoint before finally accessing the Redirect URI we configured.

如果我们检查浏览器调试控制台中的网络日志,我们会看到在最终访问我们配置的重定向URI之前,我们被重定向到一个OP注销端点。

Next time we access an endpoint in our application that requires authentication, we’ll mandatorily need to log in again in our OP platform to get permissions.

下次我们在应用中访问一个需要认证的端点时,我们将强制性地需要在我们的OP平台中再次登录以获得权限。

8. Conclusion

8.结论

To summarize, in this article, we learned a lot about the solutions offered by OpenID Connect and how we can implement some of them using Spring Security.

总结一下,在这篇文章中,我们学到了很多关于OpenID Connect提供的解决方案,以及我们如何使用Spring Security实现其中的一些解决方案。

As always, all the complete examples can be found over on GitHub.

一如既往,所有完整的例子都可以在GitHub上找到