Spring Security in Action 第十三章 实现OAuth2的认证端

本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringSecurity专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
专栏汇总:专栏汇总


本章包括

  • 实现OAuth 2authorization server
  • 管理authorization server的客户端
  • 使用OAuth 2授权类型

在本章中,我们将讨论用Spring Security实现一个authorization server。正如你在第12章中学到的,authorization server是OAuth 2架构中的一个组件(图13.1)。authorization server的作用是验证用户并向客户端提供一个令牌。客户端使用这个令牌来代表用户访问资源服务器所暴露的资源。你还了解到,OAuth 2框架定义了多个获取令牌的流程。我们称这些流程为授予。你可以根据你的情况选择不同的授予方式之一。authorization server的行为会根据所选择的授予而有所不同。在本章中,你将学习如何用Spring Security为最常见的OAuth 2授权类型配置authorization server。

  • 授权码(Authorization code grant)授予类型
  • 密码授予(Password grant)类型
  • 客户凭证授予(Client credentials grant)类型

你还将学习如何配置authorization server来发行刷新令牌。客户端使用刷新令牌来获得新的访问令牌。如果一个访问令牌过期了,客户端必须获得一个新的。要做到这一点,客户有两个选择:使用用户凭证重新认证或使用刷新令牌。我们在12.3.4节中讨论了使用刷新令牌比用户凭证的优势。

image-20230129230249451

图13.1 authorization server是OAuth 2的角色之一。它识别资源所有者,并向客户提供一个访问令牌。客户端需要该访问令牌来代表用户访问资源。

Spring Security团队宣布正在开发一个新的authorization server:http://mng.bz/4Be5 https://spring.io/projects/spring-authorization-server。可以通过这个链接了解不同的Spring Security项目中所实现的功能:http://mng.bz/Qx01。

在本章,我我们实现一个自定义的authorization server可以帮助你更好地理解这个组件的工作原理。 当然,这也是目前实现authorization server的唯一方法。

我看到开发人员在他们的项目中应用这种方法。如果你不得不和一个这样实现authorization server的项目打交道,那么在你使用新的实现之前,你还是要理解它。

你可以使用第三方工具,如Keycloak或Okta,而不是实现一个自定义的authorization server(authorization server)。在第18章中,我们将在我们的实践案例中使用Keycloak。但根据我的经验,有时利益相关者不会接受使用这样的解决方案,你需要去实现自定义代码。让我们在本章下面的章节中学习如何做到这一点并更好地理解authorization server。

13.1 编写你自己的authorization server实现

没有authorization server就没有OAuth 2的流程。正如我前面所说,OAuth 2主要是为了获得一个访问令牌。而authorization server是OAuth 2架构中发放访问令牌的组成部分。所以你首先需要知道如何实现它。然后,在第14章和第15章中,你将学习资源服务器如何根据客户端从authorization server获得的访问令牌来授权请求。让我们开始构建一个authorization server。首先,你需要创建一个新的Spring Boot项目,并添加以下代码片断中的依赖项。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency

在项目中,你还需要为spring-cloud-dependencies添加dependencyManagement标签。接下来的代码片段显示了这一点。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR12</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

我们现在可以定义一个配置类,我称之为AuthServerConfig。除了经典的@Configuration注解,我们还需要给这个类加上@EnableAuthorizationServer注解。这样,我们就指示Spring Boot启用OAuth 2authorization server的特定配置。我们可以通过扩展AuthorizationServerConfigurerAdapter类和重写特定的方法来定制这种配置,我们将在本章讨论。下面列出了AuthServerConfig类。

代码清单13.1 AuthServerConfig类

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    
}

我们已经有了authorization server的最低配置。然而,为了使其可用,我们仍然需要实现用户管理,注册至少一个客户端,并决定支持哪些授权类型。

13.2 定义用户管理

在本节中,我们将讨论用户管理。authorization server是处理OAuth 2框架中的用户认证的组件。因此,它自然需要管理用户。幸运的是,用户管理的实现与你在第3章和第4章中学到的并没有改变。我们继续使用UserDetails、UserDetailsService和UserDetailsManager接口来管理凭证。而为了管理密码,我们继续使用PasswordEncoder接口。在这里,这些接口具有相同的角色,并且与你在第3章和第4章中学到的相同。

图13.2提醒你在Spring Security的认证过程中的主要组件。你应该观察到与我们之前描述的认证架构不同的是,我们在这张图中不再有SecurityContext了。发生这种变化是因为认证的结果并不存储在SecurityContext中。认证是通过TokenStore的令牌来管理的。你会在第14章中了解到更多关于TokenStore的信息,在那里我们讨论资源服务器。

image-20230130092859107

图13.2 认证过程。一个过滤器拦截用户请求,并将认证责任委托给 authentication manager。此外, authentication manager使用一个authentication provider来实现认证逻辑。为了找到用户,authentication provider使用UserDetailsService,为了验证密码,认证提供者使用PasswordEncoder。

让我们来看看如何在我们的authorization server中实现用户管理。我总是喜欢把配置类的责任分开。为此,我选择在我们的应用程序中定义第二个配置类,在那里我只写用户管理所需的配置。我把这个类命名为WebSecurityConfig,你可以在下面的代码中看到它的实现。

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
//扩展WebSecurityConfigurerAdapter以访问AuthenticationManager实例。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Bean
    public UserDetailsService uds() {
    
    
        InMemoryUserDetailsManager uds = new InMemoryUserDetailsManager();

        UserDetails user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();

        uds.createUser(user);

        return uds;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }

    //将AuthenticationManager实例作为Spring上下文中的一个Bean加入。
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

}

我们现在可以修改AuthServerConfig类,将Authentication- Manager注册到authorization server上。接下来的代码显示了你需要在AuthServerConfig类中进行的修改。

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    

    //从上下文中注入AuthenticationManager实例
    @Autowired
    private AuthenticationManager authenticationManager;

    //重写configure()方法以设置AuthenticationManager。
     @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    
    
        endpoints.authenticationManager(authenticationManager);
    }
}

有了这些配置,我们现在有了可以在我们的认证服务器上进行认证的用户。但是,OAuth 2的架构意味着用户要授予客户端权限。代表用户使用资源的是客户端。在第13.3节,你将学习如何为authorization server配置客户端。

13.3 向authorization server注册clients

在这一节中,你将学习如何让authorization server知道你的客户端。 为了调用authorization server,在OAuth 2架构中作为客户端的应用程序需要有自己的凭证。authorization server也会管理这些凭证,只允许来自已知客户端的请求(图13.3)。

image-20230130093939569

图13.3 authorization server存储用户和客户的凭证。它使用客户端的凭证,因此它只允许已知的应用程序被它授权。

你还记得我们在第十二章开发的客户端程序吗?我们用GitHub作为我们的认证服务器。GitHub需要知道这个客户端程序,所以我们做的第一件事就是在GitHub上注册这个程序。然后我们收到了一个客户ID和一个客户密码:客户凭证。我们配置了这些凭证,然后我们的应用就用它们与授权服务器(GitHub)进行认证。在这种情况下也是如此。我们的authorization server需要知道它的客户,因为它接受来自客户的请求。在这里,这个过程应该变得很熟悉。定义授权服务器的客户端的接口是ClientDetails。定义通过ID检索ClientDetails的对象的接口是ClientDetailsService。

这些名字听起来很熟悉吗?这些接口的工作方式与UserDetails和UserDetailsService接口类似,但这些接口代表的是客户端。你会发现,我们在第3章中讨论的许多东西对ClientDetails和ClientDetailsService的作用是相似的。例如,我们的InMemoryClientDetailsService是ClientDetailsService接口的一个实现,它在内存中管理ClientDetails。它的工作原理类似于InMemoryUserDetailsManager类的UserDetails。同样地,JdbcClientDetailsService与JdbcUserDetailsManager类似。图13.4显示了这些类和接口,以及它们之间的关系。

image-20230130094402507

图13.4 我们用来定义authorization server客户端管理的类和接口之间的依赖关系

我们可以把这些相似之处总结为几点,你可以很容易地记住:

  • ClientDetails是为客户提供的,正如UserDetails是为用户提供的。
  • ClientDetailsService对客户来说就像UserDetailsService对用户来说一样。
  • InMemoryClientDetailsService对客户来说,就像InMemoryUserDetailsManager对用户来说一样。
  • JdbcClientDetailsService对客户而言,就像JdbcUserDetails- Manager对用户而言一样。

代码清单13.5向你展示了如何使用InMemoryClientDetailsService定义一个客户端配置并进行设置。我在清单中使用的BaseClientDetails类是Spring Security提供的ClientDetails接口的一个实现。在代码清单13.6中,你可以找到一种更简短的方法来编写同样的配置。

代码清单13.5 使用InMemoryClientDetailsService来配置一个客户端

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    

   //Omitted code

    //重写configure()方法来设置ClientDetailsService实例。
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        //使用ClientDetailsService的实现创建一个实例。
        InMemoryClientDetailsService service = new InMemoryClientDetailsService();

        //创建一个ClientDetails的实例,并设置所需的关于客户的细节。
        BaseClientDetails cd = new BaseClientDetails();
        cd.setClientId("client");
        cd.setClientSecret("secret");
        cd.setScope(List.of("read"));
        cd.setAuthorizedGrantTypes(List.of("password"));

        //在InMemoryClientDetailsService中添加ClientDetails实例。
        service.setClientDetailsStore(Map.of("client", cd));

        //配置ClientDetailsService,供我们的authorization server使用。
        clients.withClientDetails(service);
    }


   
}

清单13.6提出了一个更短的方法来编写相同的配置。这使我们能够避免重复,写出更干净的代码。

清单13.6 在内存中配置ClientDetails

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    

    //Omitted code

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        //使用ClientDetailsService实现来管理存储在内存中的ClientDetails。
        clients.inMemory()
            //构建并添加一个ClientDetails的实例。
                .withClient("client")
                .secret("secret")
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("read");
    }


}

为了少写代码,我更喜欢使用较短的版本,而不是代码清单13.5中更详细的版本。但是,如果你写的实现是将客户的详细信息存储在一个数据库中,这主要是现实世界中的情况,那么最好是使用代码清单13.5中的实现。

注意 正如我们对UserDetailsService所做的那样,在这个例子中,我们使用一个在内存中管理细节的实现。这种方法只适用于例子和研究目的。在现实世界中,你会使用一个持久化这些细节的实现,通常是在一个数据库中。

13.4 使用密码授予类型

在这一节中,我们使用了带有OAuth 2密码授予的authorization server。好吧,我们主要是测试它是否达到预期,因为通过我们在第13.2和13.3节所做的实现,我们已经有一个使用密码授予类型的authorization server。我告诉过你,这很容易!图13.5提醒了你密码授予类型和授权服务器在这个流程中的位置。

image-20230130101009678

图13.5 密码授予类型。授权服务器收到用户的凭证并对用户进行认证。如果凭证是正确的,授权服务器会发出一个访问令牌,客户可以用它来调用属于被认证用户的资源。

细节作为查询参数。正如你在第12章所知道的,我们需要在这个请求中发送的参数是:

  • grant_type的值为password
  • 用户名和密码,这是用户凭证
  • scope,也就是授予的权力

在下一个代码片断中,你看到了cURL命令。

#如果网址携带了"&“拼接的多个参数,如果不做处理,”&“后面的参数无法取到。
#这个时候需要对”&“进行转义,包括两个步骤:
#1.使用英文模式下输入的单引号将参数包含。
#2.使用^符号对”&"符号进行转义。
curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=password^&username=john^&password=12345^&scope=read

运行这个命令,你会得到这样的响应

{
    
    "access_token":"de1c975a-5268-49df-a910-20e2b970da59","token_type":"bearer","refresh_token":"53c4e171-204b-42da-9e56-5b185a9ba915","expires_in":43199,"scope":"read"}

观察响应中的访问令牌。在Spring Security的默认配置下,令牌是一个简单的UUID。客户端现在可以使用这个令牌来调用资源服务器所暴露的资源。在第13.2节中,你学到了如何实现资源服务器,同时也学到了更多关于自定义令牌的知识。

13.5 使用授权码授予类型

在这一节中,我们将讨论如何为授权码授予类型配置授权服务器。你在第12章开发的客户端应用程序中使用了这种授予类型,你知道它是最常用的OAuth 2授予类型之一。 了解如何配置你的授权服务器以适应这种授予类型是很有必要的,因为你很可能会在现实世界的系统中发现这种需求。因此,在这一节中,我们写一些代码来证明如何让它与Spring Security一起工作。从图13.6中,你可以回忆起授权代码授予类型是如何工作的,以及授权服务器如何与这个流程中的其他组件进行交互。

image-20230130122302677

图13.6 在授权代码授予类型中,客户端将用户重定向到授权服务器进行认证。用户直接与授权服务器进行交互,一旦通过认证,授权服务器就会向客户端返回一个重定向URI。当它回拨给客户端时,它也提供一个授权码。客户端使用授权码来获得一个访问令牌。

正如你在第13.3节中所学到的,这完全是关于你如何注册客户的问题。所以,你需要做的就是在客户端注册中设置另一种授予类型,如清单13.7所示。对于授权码授予类型,你还需要提供重定向URI。这是授权服务器在完成认证后将用户重定向到的URI。在调用重定向URI时,授权服务器也会提供访问代码。

代码清单13.7 设置授权码授予类型

package com.laurentiuspilca.ssia.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(
            ClientDetailsServiceConfigurer clients)
            throws Exception {
    
    
        clients.inMemory()
                .withClient("client")
                .secret("secret")
                .authorizedGrantTypes("authorization_code")
                .scopes("read")
                .redirectUris("http://localhost:9090/home");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    
    
        endpoints.authenticationManager(authenticationManager);
    }

}

你可以有多个客户,每个客户可能使用不同的权限。但也有可能为一个客户设置多个授权。授权服务器会根据客户的请求采取行动。看一下下面的代码,看看你如何为不同的客户配置不同的授权。

清单13.8 配置具有不同授予类型的客户端

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    
    // Omitted code
    @Override
    public void configure(
            ClientDetailsServiceConfigurer clients)
            throws Exception {
    
    
        clients.inMemory()
                .withClient("client1")
                .secret("secret1")
            //ID为client1的客户只能使用authority_code授权。
                .authorizedGrantTypes("authorization_code")
                .scopes("read")
                .redirectUris("http://localhost:9090/home")
                .and()
                .withClient("client2")
                .secret("secret2")
            //ID为client2的客户可以使用授权码、密码和刷新令牌中的任何一种。
                .authorizedGrantTypes("authorization_code", "password", "refresh_token")
                .scopes("read")
                .redirectUris("http://localhost:9090/home");
    }
    @Override
    public void configure(
            AuthorizationServerEndpointsConfigurer endpoints) {
    
    
        endpoints.authenticationManager(authenticationManager);
    }
}

为一个客户使用多种授权类型

正如你所学到的,可以为一个客户允许多种授予类型。但是,你必须谨慎对待这种方法,因为从安全的角度来看,它可能会暴露出你在架构中使用了错误的做法。授予类型是客户端(应用程序)获得访问令牌的流程,这样它就可以访问一个特殊的资源。当你在这样的系统中实现客户端时(就像我们在第12章中做的那样),你要根据你使用的授予类型来编写逻辑。

那么,在授权服务器端为同一个客户分配多个授权类型的原因是什么呢?我在一些系统中看到,我认为这是一种不好的做法,最好避免,那就是共享客户证书。共享客户凭证意味着不同的客户程序共享相同的客户凭证。

image-20230130123208075

在共享客户端凭证时,多个客户端使用相同的凭证从授权服务器获得访问令牌。

在OAuth 2的流程中,客户端,即使它是一个应用程序,也作为一个独立的组件,有自己的凭证,用来识别自己。因为你不分享用户凭证,所以你也不应该分享客户端凭证。即使所有定义客户的应用程序都是同一个系统的一部分,也不能阻止你在授权服务器层面上将这些客户注册为独立的客户。在授权服务器上单独注册客户端会带来以下好处。

  • 它提供了从每个应用程序单独审计事件的可能性。 当你记录事件时,你知道哪个客户端产生了这些事件。
  • 它允许更强的隔离性。如果一对凭证丢失,只有一个客户端受到影响。
  • 允许范围的分离。你可以给一个以特定方式获得令牌的客户分配不同的范围(授予的权限)。

范围分离是最基本的,如果管理不当,会导致奇怪的情况。让我们假设你定义了一个客户端,就像下一个代码片断中介绍的那样。

clients.inMemory()
        .withClient("client")
        .secret("secret")
        .authorizedGrantTypes(
        "authorization_code",
        "client_credentials")
        .scopes("read")

这个客户端被配置为授权代码和客户端凭证授予类型。 使用其中任何一种,客户端都会获得一个访问令牌,这为它提供了读取权限。这里奇怪的是,客户端可以通过认证用户或只使用自己的凭证来获得相同的令牌。这没有道理,甚至可以说这是一个安全漏洞。即使这对你来说听起来很奇怪,我在一个被要求审计的系统中也看到了这种做法。为什么那个系统的代码是这样设计的?最有可能的是,开发人员并不了解授予类型的目的,而是使用了他们在网上找到的一些代码。请确保你避免此类错误。要小心。为了指定授予类型,你使用字符串,而不是枚举值,这种设计可能导致错误。是的,你可以写一个像这个代码片断中提出的配置。

clients.inMemory()
        .withClient("client")
        .secret("secret")
        .authorizedGrantTypes("password", "hocus_pocus")
        .scopes("read")

只要你不尝试使用 "hocus_pocus "授予类型,该应用程序将实际工作。

让我们使用代码清单13.9中的配置来启动应用程序。当我们想接受授权码授予类型时,服务器还需要提供一个客户端重定向用户登录的页面。我们使用你在第五章学到的formlogin配置来实现这个页面。你需要重写configure()方法,如下所示。

@Configuration
public class WebSecurityConfig
        extends WebSecurityConfigurerAdapter {
    
    
    // Omitted code
    @Override
    protected void configure(HttpSecurity http)
            throws Exception {
    
    
        http.formLogin();
    }
}

AuthServerConfig类代码如下

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        clients.inMemory()
                .withClient("client")
                .secret("secret")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("read")
                .redirectUris("http://localhost:9090/home");
    }




    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    
    
        endpoints.authenticationManager(authenticationManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
    
    
        security.checkTokenAccess("isAuthenticated()");
    }

}

现在你可以启动应用程序,并在浏览器中访问链接,如下面的代码段所示。然后你会被重定向到图13.7所示的登录页面。

http://localhost:8080/oauth/authorize?response_type=code&client_id=client&scope=read

image-20230130124104038

图13.7 授权服务器将你重定向到登录页面。在它验证了你的身份后,它把你重定向到所提供的重定向URI。

登录后,授权服务器会明确要求你授予或拒绝所请求的作用域。图13.8显示了这个表格。

image-20230130124152010

图13.8 认证后,授权服务器要求你确认你要授权的范围。

一旦你授予了这些作用域,授权服务器就会把你重定向到重定向URI并提供一个访问令牌。在下一个代码片段中,你会发现授权服务器将我重定向到的URL。观察一下客户端通过请求中的查询参数得到的访问代码:

http://localhost:9090/home?code=QSEJW9

其中code是授权码

你的应用程序现在可以使用授权码来获得一个调用/oauth/token端点的令牌。

curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=authorization_code^&scope=read^&code=QSEJW9

响应如下

{
    
    "access_token":"72e70268-80ea-428f-838e-5ef881ef3eac","token_type":"bearer","refresh_token":"51d2ff63-0b2d-4de5-ac98-e7c15ffc6aec","expires_in":43199,"scope":"read"}

请注意,一个授权码只能使用一次。如果你试图再次使用相同的代码调用/oauth/ token端点,你会收到像下一个代码片断中显示的错误。你只能通过要求用户再次登录来获得另一个有效的授权码。

{
    
    "error":"invalid_grant","error_description":"Invalid authorization code: QSEJW9"}

13.6 使用客户端凭证授予类型

在这一节中,我们将讨论实现客户凭证授予类型。你可能还记得在第12章中,我们将这种授予类型用于后端到后端的认证。在这种情况下,它不是强制性的,但有时我们会看到这种授予类型是我们在第8章讨论的API密钥认证方法的替代品。当我们保护一个与特定用户无关的、客户需要访问的api时,我们也可能使用客户凭证授予类型。比方说,你想实现一个返回服务器状态的api。客户端调用这个api来检查连接情况,并最终向用户显示连接状态或错误信息。因为这个api只代表客户端和资源服务器之间的交易,而不涉及任何用户特定的资源,所以客户端应该能够调用它而不需要用户进行认证。对于这种情况,我们使用客户端凭证授予类型。图13.9提醒你客户端凭证授予类型是如何工作的,以及授权服务器如何与这个流程中的其他组件进行交互。

image-20230130134002437

客户端凭证授予类型不涉及用户。一般来说,我们使用这种授予类型在两个后端解决方案之间进行认证。客户端只需要它的凭证来验证和获得访问令牌。

正如你所期望的那样,要使用客户凭证授予类型,必须用这个授予类型注册一个客户。在接下来的代码中,你可以找到客户端的配置,它使用了这种授予类型。

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    
    // Omitted code
    @Override
    public void configure(
            ClientDetailsServiceConfigurer clients)
            throws Exception {
    
    
        clients.inMemory()
                .withClient("client")
                .secret("secret")
                .authorizedGrantTypes("client_credentials")
                .scopes("info");
    }
}

你现在可以启动应用程序,并调用/oauth/token api来获取访问令牌。下一个代码片断向你展示了如何获得这个:

curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=client_credentials^&scope=info

响应为:

{
    
    "access_token":"e0e7f45f-48a9-48c0-97cc-04e24329334e","token_type":"bearer","expires_in":43199,"scope":"info"}

对客户凭证授予类型要小心。这种授予类型只要求客户端使用其凭证。需要确保你不能像下图那样授予权限,否则,你可能允许客户端访问用户的资源而不需要用户的许可。图13.10展示了这样一个设计,开发者通过允许客户端调用用户的资源api而不需要用户首先进行认证,从而造成了安全漏洞。

image-20230130134943570

图13.10 开发者想为客户端提供调用/info端点的可能性,而不需要得到用户的许可。但由于这些使用了相同的范围,他们现在也允许客户端调用/transactions api,这是一个用户的资源。

13.7 使用刷新令牌授予类型

在这一节中,我们将讨论在用Spring Security开发的授权服务器中使用刷新令牌。你可能还记得第12章,当与另一种授予类型一起使用时,刷新令牌具有一些优势。你可以在授权码授予类型和密码授予类型中使用刷新令牌(图13.11)。

image-20230130135744751

图13.11 当用户认证时,除了访问令牌,客户端还收到一个刷新令牌。客户端使用刷新令牌来获得新的访问令牌。

如果你想让你的授权服务器支持刷新令牌,你需要在客户端的授予列表中加入刷新令牌授予。

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
        extends AuthorizationServerConfigurerAdapter {
    
    
    // Omitted code
    @Override
    public void configure(
            ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        clients.inMemory()
                .withClient("client")
                .secret("secret")
            	//在客户端的授权授予类型列表中添加 refresh_token。
                .authorizedGrantTypes(
                        "password",
                        "refresh_token")
                .scopes("read");
    }
}

现在试试你在第13.4节中使用的同样的cURL命令。你会看到响应是类似的,但现在包括一个刷新令牌。

curl -v -XPOST -u client:secret http://localhost:8080/oauth/token?grant_type=password^&username=john^&password=12345^&scope=read

响应为

{
    
    "access_token":"8c3b248d-5cc1-42e7-a56b-5aabeb01c7b8","token_type":"bearer","refresh_token":"d88665e3-f10e-4045-85ed-97c1ffc1785d","expires_in":43199,"scope":"read"}

总结

  • ClientRegistration接口定义了Spring Security中的OAuth 2客户端注册。ClientRegistrationRepository接口描述了负责管理客户端注册的对象。这两个接口允许你自定义你的授权服务器如何管理客户端注册。
  • 对于用Spring Security实现的授权服务器来说,客户端的注册决定了授予类型。同一个授权服务器可以为不同的客户提供不同的授予类型。这意味着你不需要在你的授权服务器中实现特定的东西来定义多种授予类型。
  • 对于授权码授予类型,授权服务器必须向用户提供登录的可能性。这一要求是由于在授权代码流程中,用户(资源所有者)直接在授权服务器上授权给客户端访问的结果。
  • 一个客户端注册可以请求多种授予类型。这意味着一个客户可以在不同情况下使用,例如,密码和授权码授予类型。
  • 我们使用客户端证书授予类型来进行后端到后端的授权。 技术上有可能,但不常见的是,客户端请求客户端证书授予类型与另一个授予类型一起。
  • 我们可以将刷新令牌与授权码授予类型和密码授予类型一起使用。通过将刷新令牌添加到客户注册中,我们指示授权服务器在访问令牌之外也发放一个刷新令牌。客户端使用刷新令牌来获得新的访问令牌,而不需要再次验证用户。

猜你喜欢

转载自blog.csdn.net/Learning_xzj/article/details/128998667