Spring Security 5 – OAuth2 Login – Spring Security 5 – OAuth2登录

最后修改: 2018年 1月 14日

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

1. Overview

1.概述

Spring Security 5 introduces a new OAuth2LoginConfigurer class that we can use for configuring an external Authorization Server.

Spring Security 5引入了一个新的OAuth2LoginConfigurer类,我们可以用它来配置一个外部授权服务器。

In this tutorial, we’ll explore some of the various configuration options available for the oauth2Login() element.

在本教程中,我们将探讨一些可用于oauth2Login()元素的各种配置选项。

2. Maven Dependencies

2.Maven的依赖性

In a Spring Boot project, we just need to add the starter spring-boot-starter-oauth2-client:

在Spring Boot项目中,我们只需要添加启动器spring-boot-starter-oauth2-client

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

In a non-Boot project, in addition to the standard Spring and Spring Security dependencies, we’ll also need to explicitly add the spring-security-oauth2-client and spring-security-oauth2-jose dependencies:

在非Boot项目中,除了标准的Spring和Spring Security依赖项之外,我们还需要明确添加spring-security-oauth2-clientspring-security-oauth2-jose依赖项。

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>

3. Clients Setup

3.客户设置

In a Spring Boot project, all we need to do is add a few standard properties for each client we want to configure.

在Spring Boot项目中,我们所要做的就是为我们想要配置的每个客户端添加一些标准属性。

Let’s set up our project for login with clients registered with Google and Facebook as authentication providers.

让我们来设置我们的项目,让客户在谷歌和Facebook注册为认证提供者后进行登录。

3.1. Obtaining Client Credentials

3.1.获取客户凭证

To obtain client credentials for Google OAuth2 authentication, head on over to the Google API Console, “Credentials” section.

要获得Google OAuth2认证的客户证书,请前往Google API Console,”证书 “部分。

Here we’ll create credentials of type “OAuth2 Client ID” for our web application. This results in Google setting up a client id and secret for us.

在这里,我们将为我们的网络应用程序创建 “OAuth2客户端ID “类型的凭证。这导致谷歌为我们设置了一个客户端ID和秘密。

We also have to configure an authorized redirect URI in the Google Console, which is the path that users will be redirected to after they successfully log in with Google.

我们还必须在谷歌控制台配置一个授权重定向URI,这是用户在成功登录谷歌后将被重定向到的路径。

By default, Spring Boot configures this redirect URI as /login/oauth2/code/{registrationId}.

默认情况下,Spring Boot将此重定向URI配置为/login/oauth2/code/{registrationId}

So, for Google we’ll add this URI:

因此,对于谷歌,我们将添加这个URI。

http://localhost:8081/login/oauth2/code/google

To obtain the client credentials for authentication with Facebook, we need to register an application on the Facebook for Developers website and set up the corresponding URI as a “Valid OAuth redirect URI”:

为了获得用于与Facebook认证的客户端凭证,我们需要在Facebook for Developers网站上注册一个应用程序,并将相应的URI设置为 “有效OAuth重定向URI”。

http://localhost:8081/login/oauth2/code/facebook

3.2. Security Configuration

3.2.安全配置

Next, we need to add the client credentials to the application.properties file.

接下来,我们需要在application.properties文件中添加客户凭证。

The Spring Security properties are prefixed with spring.security.oauth2.client.registration followed by the client name and then the name of the client property:

Spring Security属性的前缀是spring.security.oauth2.client.registration,后面是客户名称,然后是客户属性的名称

spring.security.oauth2.client.registration.google.client-id=<your client id>
spring.security.oauth2.client.registration.google.client-secret=<your client secret>

spring.security.oauth2.client.registration.facebook.client-id=<your client id> 
spring.security.oauth2.client.registration.facebook.client-secret=<your client secret>

Adding these properties for at least one client will enable the Oauth2ClientAutoConfiguration class, which sets up all the necessary beans.

为至少一个客户端添加这些属性将启用Oauth2ClientAutoConfiguration,该类设置了所有必要的bean。

The automatic web security configuration is equivalent to defining a simple oauth2Login() element:

自动网络安全配置相当于定义了一个简单的oauth2Login()元素

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
         .anyRequest().authenticated()
         .and()
         .oauth2Login();
        return http.build();
    }
}

Here we can see the oauth2Login() element is used in a similar manner to already known httpBasic() and formLogin() elements.

在这里我们可以看到oauth2Login()元素的使用方式与已经知道的httpBasic()formLogin()元素类似。

Now, when we try to access a protected URL, the application will display an auto-generated login page with two clients:

现在,当我们试图访问一个受保护的URL时,应用程序将显示一个自动生成的有两个客户端的登录页面

oauth login default

3.3. Other Clients

3.3.其他客户

Note that the Spring Security project also contains default configurations for GitHub and Okta in addition to Google and Facebook. These default configurations provide all the necessary information for authentication, which is what allows us to only enter the client credentials.

注意,除了谷歌和Facebook之外,Spring Security项目还包含GitHub和Okta的默认配置。这些默认配置提供了所有必要的认证信息,这就是允许我们只输入客户端凭证的原因。

If we want to use a different authentication provider not configured in Spring Security, we’ll need to define the full configuration, with information such as authorization URI and token URI. Here’s a look at the default configurations in Spring Security to get an idea of the properties needed.

如果我们想使用未在 Spring Security 中配置的不同身份验证提供商,我们需要定义完整的配置,包括授权 URI 和令牌 URI 等信息。下面看一下Spring Security中的默认配置,以了解所需的属性。

4. Setup in a Non-Boot Project

4.在一个非启动项目中的设置

4.1. Creating a ClientRegistrationRepository Bean

4.1.创建一个ClientRegistrationRepository Bean

If we’re not working with a Spring Boot application, we’ll need to define a ClientRegistrationRepository bean that contains an internal representation of the client information owned by the authorization server:

如果我们不是在使用Spring Boot应用程序,我们就需要定义一个ClientRegistrationRepository Bean,它包含授权服务器所拥有的客户信息的内部表示。

@Configuration
@EnableWebSecurity
@PropertySource("classpath:application.properties")
public class SecurityConfig {
    private static List<String> clients = Arrays.asList("google", "facebook");

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        List<ClientRegistration> registrations = clients.stream()
          .map(c -> getRegistration(c))
          .filter(registration -> registration != null)
          .collect(Collectors.toList());
        
        return new InMemoryClientRegistrationRepository(registrations);
    }
}

Here we’re creating an InMemoryClientRegistrationRepository with a list of ClientRegistration objects.

这里我们要创建一个InMemoryClientRegistrationRepositoryClientRegistration对象列表。

4.2. Building ClientRegistration Objects

4.2.建立ClientRegistration对象

Let’s see the getRegistration() method that builds these objects:

让我们看看建立这些对象的getRegistration()方法。

private static String CLIENT_PROPERTY_KEY 
  = "spring.security.oauth2.client.registration.";

@Autowired
private Environment env;

private ClientRegistration getRegistration(String client) {
    String clientId = env.getProperty(
      CLIENT_PROPERTY_KEY + client + ".client-id");

    if (clientId == null) {
        return null;
    }

    String clientSecret = env.getProperty(
      CLIENT_PROPERTY_KEY + client + ".client-secret");
 
    if (client.equals("google")) {
        return CommonOAuth2Provider.GOOGLE.getBuilder(client)
          .clientId(clientId).clientSecret(clientSecret).build();
    }
    if (client.equals("facebook")) {
        return CommonOAuth2Provider.FACEBOOK.getBuilder(client)
          .clientId(clientId).clientSecret(clientSecret).build();
    }
    return null;
}

Here we’re reading the client credentials from a similar application.properties file. Then we use the CommonOauth2Provider enum already defined in Spring Security for the rest of the client properties for Google and Facebook clients.

在这里,我们从类似的application.properties文件中读取客户端凭证。然后我们使用已经在Spring Security中定义的CommonOauth2Provider枚举,为Google和Facebook客户端提供其余的客户端属性。

Each ClientRegistration instance corresponds to a client.

每个ClientRegistration实例对应于一个客户。

4.3. Registering the ClientRegistrationRepository

4.3.注册ClientRegistrationRepository

Finally, we have to create an OAuth2AuthorizedClientService bean based on the ClientRegistrationRepository bean and register both with the oauth2Login() element:

最后,我们必须在ClientRegistrationRepository Bean的基础上创建一个OAuth2AuthorizedClientService Bean,并在oauth2Login()元素中注册两者。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated()
      .and()
      .oauth2Login()
      .clientRegistrationRepository(clientRegistrationRepository())
      .authorizedClientService(authorizedClientService());
    return http.build();
}

@Bean
public OAuth2AuthorizedClientService authorizedClientService() {
 
    return new InMemoryOAuth2AuthorizedClientService(
      clientRegistrationRepository());
}

As we can see, we can use the clientRegistrationRepository() method of oauth2Login() to register a custom registration repository.

正如我们所看到的,我们可以使用oauth2Login()clientRegistrationRepository()方法来注册一个自定义注册库。

We’ll also have to define a custom login page, as it won’t be automatically generated anymore. We’ll see more information on this in the next section.

我们还必须定义一个自定义的登录页面,因为它不会再自动生成。我们将在下一节中看到更多关于这方面的信息。

Let’s continue with further customization of our login process.

让我们继续对我们的登录过程进行进一步的定制。

5. Customizing oauth2Login()

5.定制oauth2Login()

There are several elements that the OAuth 2 process uses and that we can customize with oauth2Login() methods.

有几个元素是OAuth 2流程使用的,我们可以用oauth2Login()方法来定制。

Note that all these elements have default configurations in Spring Boot and explicit configuration isn’t required.

注意,所有这些元素在Spring Boot中都有默认配置,不需要明确配置。

Let’s see how we can customize these in our configuration.

让我们看看如何在我们的配置中定制这些。

5.1. Custom Login Page

5.1.自定义登录页面

Even though Spring Boot generates a default login page for us, we’ll usually want to define our own customized page.

尽管Spring Boot为我们生成了一个默认的登录页面,但我们通常希望定义自己的定制页面。

Let’s start with configuring a new login URL for the oauth2Login() element by using the loginPage() method:

让我们首先通过使用 oauth2Login()方法oauth2Login()元素配置一个新的登录URL。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .antMatchers("/oauth_login")
      .permitAll()
      .anyRequest()
      .authenticated()
      .and()
      .oauth2Login()
      .loginPage("/oauth_login");
    return http.build();
}

Here we’ve set up our login URL to be /oauth_login.

在这里,我们把登录的URL设置为/oauth_login

Next, let’s define a LoginController with a method that maps to this URL:

接下来,让我们定义一个LoginController,它有一个方法可以映射到这个URL。

@Controller
public class LoginController {

    private static String authorizationRequestBaseUri
      = "oauth2/authorization";
    Map<String, String> oauth2AuthenticationUrls
      = new HashMap<>();

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @GetMapping("/oauth_login")
    public String getLoginPage(Model model) {
        // ...

        return "oauth_login";
    }
}

This method has to send a map of the clients available and their authorization endpoints to the view, which we’ll obtain from the ClientRegistrationRepository bean:

该方法必须向视图发送一张可用的客户端及其授权端点的地图,我们将从ClientRegistrationRepositoryBean中获得。

public String getLoginPage(Model model) {
    Iterable<ClientRegistration> clientRegistrations = null;
    ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository)
      .as(Iterable.class);
    if (type != ResolvableType.NONE && 
      ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
        clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
    }

    clientRegistrations.forEach(registration -> 
      oauth2AuthenticationUrls.put(registration.getClientName(), 
      authorizationRequestBaseUri + "/" + registration.getRegistrationId()));
    model.addAttribute("urls", oauth2AuthenticationUrls);

    return "oauth_login";
}

Finally, we need to define our oauth_login.html page:

最后,我们需要定义我们的oauth_login.html页面。

<h3>Login with:</h3>
<p th:each="url : ${urls}">
    <a th:text="${url.key}" th:href="${url.value}">Client</a>
</p>

This is a simple HTML page that displays links to authenticate with each client.

这是一个简单的HTML页面,显示与每个客户进行认证的链接。

After adding some styling to it, we can change the look of the login page:

在给它添加一些样式后,我们可以改变登录页面的外观。

login

5.2. Custom Authentication Success and Failure Behavior

5.2.自定义认证成功和失败的行为

We can control the post-authentication behavior with different methods:

我们可以用不同的方法控制认证后的行为。

  • defaultSuccessUrl() and failureUrl() to redirect the user to a given URL
  • successHandler() and failureHandler() to run custom logic following the authentication process

Let’s see how we can set custom URLs to redirect the user to:

让我们看看我们如何设置自定义的URL来重定向给用户。

.oauth2Login()
  .defaultSuccessUrl("/loginSuccess")
  .failureUrl("/loginFailure");

If the user visited a secured page before authenticating, they will be redirected to that page after logging in. Otherwise, they will be redirected to /loginSuccess.

如果用户在认证前访问了一个安全的页面,他们在登录后将被重定向到该页面。否则,他们将被重定向到/loginSuccess

If we want the user to always be sent to the /loginSuccess URL regardless if they were on a secured page before or not, we can use the method defaultSuccessUrl(“/loginSuccess”, true).

如果我们希望用户总是被发送到/loginSuccess URL,无论他们之前是否在安全页面上,我们可以使用方法defaultSuccessUrl(“/loginSuccess”, true)

To use a custom handler, we would have to create a class that implements the AuthenticationSuccessHandler or AuthenticationFailureHandler interfaces, override the inherited methods and then set the beans using the successHandler() and failureHandler() methods.

要使用自定义处理程序,我们必须创建一个实现AuthenticationSuccessHandlerAuthenticationFailureHandler接口的类,覆盖继承的方法,然后使用successHandler()failureHandler()方法设置Bean。

5.3. Custom Authorization Endpoint

5.3.自定义授权端点

The authorization endpoint is the endpoint that Spring Security uses to trigger an authorization request to the external server.

授权端点是Spring Security用来触发对外部服务器的授权请求的端点。

First, let’s set new properties for the authorization endpoint:

首先,让我们为授权端点设置新的属性

.oauth2Login() 
  .authorizationEndpoint()
  .baseUri("/oauth2/authorize-client")
  .authorizationRequestRepository(authorizationRequestRepository());

Here we’ve modified the baseUri to /oauth2/authorize-client instead of the default /oauth2/authorization.

这里我们把baseUri修改为/oauth2/authorize-client,而不是默认的/oauth2/authorization

We’re also explicitly setting an authorizationRequestRepository() bean that we have to define:

我们还明确设置了一个authorizationRequestRepository()Bean,我们必须定义这个Bean。

@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> 
  authorizationRequestRepository() {
 
    return new HttpSessionOAuth2AuthorizationRequestRepository();
}

We’ve used the Spring-provided implementation for our bean, but we could also provide a custom one.

我们为我们的Bean使用了Spring提供的实现,但我们也可以提供一个自定义的实现。

5.4. Custom Token Endpoint

5.4.自定义令牌端点

The token endpoint processes access tokens.

令牌端点处理访问令牌。

Let’s explicitly configure the tokenEndpoint() with the default response client implementation:

让我们明确配置tokenEndpoint()与默认的响应客户端实现。

.oauth2Login()
  .tokenEndpoint()
  .accessTokenResponseClient(accessTokenResponseClient());

And here’s the response client bean:

这里是响应的客户Bean。

@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> 
  accessTokenResponseClient() {
 
    return new NimbusAuthorizationCodeTokenResponseClient();
}

This configuration is the same as the default one, and it uses the Spring implementation, which is based on exchanging an authorization code with the provider.

这个配置与默认配置相同,它使用了Spring的实现,它是基于与提供者交换授权代码。

Of course, we could also substitute a custom response client.

当然,我们也可以用一个自定义的响应客户端代替。

5.5. Custom Redirection Endpoint

5.5.自定义重定向端点

This is the endpoint to redirect to after authentication with the external provider.

这是在与外部供应商认证后重定向到的端点。

Let’s see how we can change the baseUri for the redirection endpoint:

让我们来看看如何改变重定向端点的baseUri

.oauth2Login()
  .redirectionEndpoint()
  .baseUri("/oauth2/redirect")

The default URI is login/oauth2/code.

默认URI是login/oauth2/code

Note that if we change it, we also have to update the redirectUriTemplate property of each ClientRegistration and add the new URI as an authorized redirect URI for each client.

注意,如果我们改变它,我们也必须更新每个ClientRegistrationredirectUriTemplate属性,并添加新的URI作为每个客户的授权重定向URI。

5.6. Custom User Information Endpoint

5.6.自定义用户信息端点

The user info endpoint is the location we can leverage to obtain user information.

用户信息端点是我们可以利用来获取用户信息的位置。

We can customize this endpoint using the userInfoEndpoint() method. For this, we can use methods such as userService() and customUserType() to modify the way user information is retrieved.

我们可以使用userInfoEndpoint()方法自定义该端点。为此,我们可以使用userService()customUserType()等方法来修改用户信息的检索方式。

6. Accessing User Information

6.访问用户信息

A common task we may want to achieve is finding information about the logged-in user. For this, we can make a request to the user information endpoint.

我们可能想要实现的一个常见任务是找到关于登录用户的信息。为此,我们可以向用户信息端点发出请求。

First, we’ll have to get the client corresponding to the current user token:

首先,我们要获得与当前用户令牌对应的客户端。

@Autowired
private OAuth2AuthorizedClientService authorizedClientService;

@GetMapping("/loginSuccess")
public String getLoginInfo(Model model, OAuth2AuthenticationToken authentication) {
    OAuth2AuthorizedClient client = authorizedClientService
      .loadAuthorizedClient(
        authentication.getAuthorizedClientRegistrationId(), 
          authentication.getName());
    //...
    return "loginSuccess";
}

Next, we’ll send a request to the client’s user info endpoint and retrieve the userAttributes Map:

接下来,我们将向客户端的用户信息端点发送一个请求,并检索userAttributes Map

String userInfoEndpointUri = client.getClientRegistration()
  .getProviderDetails().getUserInfoEndpoint().getUri();

if (!StringUtils.isEmpty(userInfoEndpointUri)) {
    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + client.getAccessToken()
      .getTokenValue());
    HttpEntity entity = new HttpEntity("", headers);
    ResponseEntity <map>response = restTemplate
      .exchange(userInfoEndpointUri, HttpMethod.GET, entity, Map.class);
    Map userAttributes = response.getBody();
    model.addAttribute("name", userAttributes.get("name"));
}

By adding the name property as a Model attribute, we can display it in the loginSuccess view as a welcome message to the user:

通过将name属性添加为Model属性,我们可以在loginSuccess视图中显示它作为对用户的欢迎信息。

welcome

Besides the name, the userAttributes Map also contains properties such as email, family_name, picture and locale.

除了姓名用户属性地图还包含电子邮件家庭名称照片地域等属性。

7. Conclusion

7.结论

In this article, we saw how to use the oauth2Login() element in Spring Security to authenticate with different providers such as Google and Facebook.

在这篇文章中,我们看到了如何使用Spring Security中的oauth2Login()元素来验证不同的供应商,如Google和Facebook。

We also went through some common scenarios of customizing this process.

我们还经历了一些定制这一过程的常见场景。

The full source code of the examples can be found over on GitHub.

这些例子的完整源代码可以在GitHub上找到