Spring Security in Action 第十三章 实现OAuth2的认证端
本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringSecurity专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
专栏汇总:专栏汇总
文章目录
- 13.1 编写你自己的authorization server实现
- 13.2 定义用户管理
- 13.3 向authorization server注册clients
- 13.4 使用密码授予类型
- 13.5 使用授权码授予类型
- 13.6 使用客户端凭证授予类型
- 13.7 使用刷新令牌授予类型
本章包括
- 实现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节中讨论了使用刷新令牌比用户凭证的优势。
图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 AuthServerConfigextends 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的信息,在那里我们讨论资源服务器。
图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 {@Beanpublic UserDetailsService uds() {InMemoryUserDetailsManager uds = new InMemoryUserDetailsManager();UserDetails user = User.withUsername("john").password("12345").authorities("read").build();uds.createUser(user);return uds;}@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}//将AuthenticationManager实例作为Spring上下文中的一个Bean加入。@Override@Beanpublic 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 AuthServerConfigextends AuthorizationServerConfigurerAdapter {//从上下文中注入AuthenticationManager实例@Autowiredprivate AuthenticationManager authenticationManager;//重写configure()方法以设置AuthenticationManager。@Overridepublic 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)。
图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显示了这些类和接口,以及它们之间的关系。
图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 AuthServerConfigextends AuthorizationServerConfigurerAdapter {//Omitted code//重写configure()方法来设置ClientDetailsService实例。@Overridepublic 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 AuthServerConfigextends AuthorizationServerConfigurerAdapter {//Omitted code@Overridepublic 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提醒了你密码授予类型和授权服务器在这个流程中的位置。
图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中,你可以回忆起授权代码授予类型是如何工作的,以及授权服务器如何与这个流程中的其他组件进行交互。
图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 AuthServerConfigextends AuthorizationServerConfigurerAdapter {@Autowiredprivate AuthenticationManager authenticationManager;@Overridepublic void configure(ClientDetailsServiceConfigurer clients)throws Exception {clients.inMemory().withClient("client").secret("secret").authorizedGrantTypes("authorization_code").scopes("read").redirectUris("http://localhost:9090/home");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager);}}
你可以有多个客户,每个客户可能使用不同的权限。但也有可能为一个客户设置多个授权。授权服务器会根据客户的请求采取行动。看一下下面的代码,看看你如何为不同的客户配置不同的授权。
清单13.8 配置具有不同授予类型的客户端
@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {// Omitted code@Overridepublic 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");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager);}
}
为一个客户使用多种授权类型
正如你所学到的,可以为一个客户允许多种授予类型。但是,你必须谨慎对待这种方法,因为从安全的角度来看,它可能会暴露出你在架构中使用了错误的做法。授予类型是客户端(应用程序)获得访问令牌的流程,这样它就可以访问一个特殊的资源。当你在这样的系统中实现客户端时(就像我们在第12章中做的那样),你要根据你使用的授予类型来编写逻辑。
那么,在授权服务器端为同一个客户分配多个授权类型的原因是什么呢?我在一些系统中看到,我认为这是一种不好的做法,最好避免,那就是共享客户证书。共享客户凭证意味着不同的客户程序共享相同的客户凭证。
在共享客户端凭证时,多个客户端使用相同的凭证从授权服务器获得访问令牌。
在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 WebSecurityConfigextends WebSecurityConfigurerAdapter {// Omitted code@Overrideprotected 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 AuthServerConfigextends AuthorizationServerConfigurerAdapter {@Autowiredprivate AuthenticationManager authenticationManager;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("client").secret("secret").authorizedGrantTypes("authorization_code", "refresh_token").scopes("read").redirectUris("http://localhost:9090/home");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.authenticationManager(authenticationManager);}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) {security.checkTokenAccess("isAuthenticated()");}}
现在你可以启动应用程序,并在浏览器中访问链接,如下面的代码段所示。然后你会被重定向到图13.7所示的登录页面。
http://localhost:8080/oauth/authorize?response_type=code&client_id=client&scope=read
图13.7 授权服务器将你重定向到登录页面。在它验证了你的身份后,它把你重定向到所提供的重定向URI。
登录后,授权服务器会明确要求你授予或拒绝所请求的作用域。图13.8显示了这个表格。
图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提醒你客户端凭证授予类型是如何工作的,以及授权服务器如何与这个流程中的其他组件进行交互。
客户端凭证授予类型不涉及用户。一般来说,我们使用这种授予类型在两个后端解决方案之间进行认证。客户端只需要它的凭证来验证和获得访问令牌。
正如你所期望的那样,要使用客户凭证授予类型,必须用这个授予类型注册一个客户。在接下来的代码中,你可以找到客户端的配置,它使用了这种授予类型。
@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {// Omitted code@Overridepublic 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而不需要用户首先进行认证,从而造成了安全漏洞。
图13.10 开发者想为客户端提供调用/info端点的可能性,而不需要得到用户的许可。但由于这些使用了相同的范围,他们现在也允许客户端调用/transactions api,这是一个用户的资源。
13.7 使用刷新令牌授予类型
在这一节中,我们将讨论在用Spring Security开发的授权服务器中使用刷新令牌。你可能还记得第12章,当与另一种授予类型一起使用时,刷新令牌具有一些优势。你可以在授权码授予类型和密码授予类型中使用刷新令牌(图13.11)。
图13.11 当用户认证时,除了访问令牌,客户端还收到一个刷新令牌。客户端使用刷新令牌来获得新的访问令牌。
如果你想让你的授权服务器支持刷新令牌,你需要在客户端的授予列表中加入刷新令牌授予。
@Configuration
@EnableAuthorizationServer
public class AuthServerConfigextends AuthorizationServerConfigurerAdapter {// Omitted code@Overridepublic 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实现的授权服务器来说,客户端的注册决定了授予类型。同一个授权服务器可以为不同的客户提供不同的授予类型。这意味着你不需要在你的授权服务器中实现特定的东西来定义多种授予类型。
- 对于授权码授予类型,授权服务器必须向用户提供登录的可能性。这一要求是由于在授权代码流程中,用户(资源所有者)直接在授权服务器上授权给客户端访问的结果。
- 一个客户端注册可以请求多种授予类型。这意味着一个客户可以在不同情况下使用,例如,密码和授权码授予类型。
- 我们使用客户端证书授予类型来进行后端到后端的授权。 技术上有可能,但不常见的是,客户端请求客户端证书授予类型与另一个授予类型一起。
- 我们可以将刷新令牌与授权码授予类型和密码授予类型一起使用。通过将刷新令牌添加到客户注册中,我们指示授权服务器在访问令牌之外也发放一个刷新令牌。客户端使用刷新令牌来获得新的访问令牌,而不需要再次验证用户。
相关文章:
Spring Security in Action 第十三章 实现OAuth2的认证端
本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获&#…...
本文章提供中国国界、国界十段线原始数据以及加载方法
本文章提供中国国界九段线原始数据和加载方法 1、中国国界 完整数据 包括十段线 中国国界线(完整版 包括十段线) 2、原始数据 中国国界十段线topojson格式数据.rar 中国国界线topjson数据 中国国界十段线svg格式数据.rar 中国国界线svg数据 中国国界十段线shp格式数据…...
一文带你搞懂,Python语言运算符
Python语言支持很多种运算符,我们先用一个表格为大家列出这些运算符,然后选择一些马上就会用到的运算符为大家进行讲解。 说明:上面这个表格实际上是按照运算符的优先级从上到下列出了各种运算符。所谓优先级就是在一个运算的表达式中&#x…...
JAVA集合专题4 —— Map
目录Map接口实现类的特点Map接口的常见方法Map六大遍历方式Map练习1code编程练习2code编程练习3思路codeMap接口实现类的特点 Map与Collection并列存在,是Map集合体系的顶级接口Map的有些子实现存储数据是有序的(LinkedHashMap),有些子实现存储数据是无…...
二叉树进阶--二叉搜索树
目录 1.二叉搜索树 1.1 二叉搜索树概念 1.2 二叉搜索树操作 1.3 二叉搜索树的实现 1.4 二叉搜索树的应用 1.5 二叉搜索树的性能分析 2.二叉树进阶经典题: 1.二叉搜索树 1.1 二叉搜索树概念 二叉搜索树又称二叉排序树,它或者是一棵空树,…...
牛客网Python篇数据分析习题(三)
1.现有一个Nowcoder.csv文件,它记录了牛客网的部分用户数据,包含如下字段(字段与字段之间以逗号间隔): Nowcoder_ID:用户ID Level:等级 Achievement_value:成就值 Num_of_exercise&a…...
Java开发常见关键词集绵
一、关键词1: (1)RPC:远程过程调用(Remote Procedure Call)的缩写形式。远程调用的时候让人们觉得是本地调用。 (2)HTTP:超文本传输协议(Hyper Text Transfer…...
解决idea出现的java.lang.OutOfMemoryError: Java heap space的问题
文章目录1. 复现问题2. 分析问题3. 解决问题4. 补充解决java.lang.OutOfMemoryError: PermGen space问题1. 复现问题 今天使用idea开发时,突然报出如下错误: Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat org.…...
为什么子进程要继承处理器亲缘性?
请先考虑一个典型的程序为什么需要启动一个子进程。(当然资源管理器不算一个典型的程序) 这是因为手头的任务被分解为子任务,无论出于何种原因,这些子任务都被放入子流程中。例如,在实现多次遍历型编译器/链接器时,其中每次遍历都…...
【算法】高精度
作者:指针不指南吗 专栏:算法篇 🐾不能只会思路,必须落实到代码上🐾 文章目录前言一、高精度加法二、高精度减法三、高精度乘法四、高精度除法前言 高精度即很大很大的数,超过了 long long 的范围&…...
计算机网络-基本概念
目录 计算机网络-基本概念 互联网 Java的跨平台原理 编辑 C\C的跨平台原理 解释性语言的跨平台原理(python,js等) 客户端 vs 服务器 什么是协议? 网络互连模型 请求过程 计算机之间的通信基础 计算机之间的连接方式-网线直连(需要用交叉线,而…...
你评论,我赠书~【哈士奇赠书 - 13期】-〖Python程序设计-编程基础、Web开发及数据分析〗参与评论,即可有机获得
大家好,我是 哈士奇 ,一位工作了十年的"技术混子", 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 💬 人生格言:优于别人,并不高贵,真正的高贵应该是优于过去的自己。💬 ὎…...
【设计模式】我终于读懂了代理模式。。。
👦代理模式的基本介绍 1)代理模式:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。 2)被代理的对象可以是远程对象、创建…...
每天10个前端小知识 【Day 2】
👩 个人主页:不爱吃糖的程序媛 🙋♂️ 作者简介:前端领域新星创作者、CSDN内容合伙人,专注于前端各领域技术,成长的路上共同学习共同进步,一起加油呀! ✨系列专栏:前端…...
帮助中心在线制作工具推荐这4款,很不错哟!
根据用户咨询问题是否解决的情景,分为三个部分,首先帮助中心恰好有用户需要咨询的问题,用户可以通过点击相关问题即可解决自己的问题,其次,用户第一眼没有在帮助中心解决问题,有个搜索框,用户的…...
rabbitMQ相关文章汇总
RabbitMQ五种工作模式: https://blog.csdn.net/weixin_41882200/article/details/117128590?ops_request_misc%257B%2522request%255Fid%2522%253A%2522167625223516800182771874%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id1…...
【C++】异常
🌈欢迎来到C专栏~~异常 (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort目前状态:大三非科班啃C中🌍博客主页:张小姐的猫~江湖背景快上车🚘,握好方向盘跟我有一起打天下嘞!送给自己的一句鸡汤…...
@Validated注解不生效问题汇总
Validated注解不生效问题汇总 文章目录Validated注解不生效问题汇总背景:一:可能原因原因1:原因2:原因3:原因4:二:补充全局异常对validation的处理背景: 项目框架应用的是validatio…...
华科万维C++章节练习2_4
题目:编写程序,从键盘输入一个字符,然后在屏幕上输出该字符开头的连续3个字符以及对应ASCII码。 输出格式请参看: 请输入一个字符>>A 字符 ASCII码 A 65 B 66 C 67 请按任意键继续. . . 请直接…...
17万字数字化医院信息化建设大数据平台建设方案WORD
【版权声明】本资料来源网络,知识分享,仅供个人学习,请勿商用。【侵删致歉】如有侵权请联系小编,将在收到信息后第一时间删除!完整资料领取见文末,部分资料内容: 目录 第1章 医院信息化概述 1.…...
Android 11系统签名修改
Android OS 映像在两个地方使用加密签名:映像中的所有 .apk 文件都必须经过签名。Android 软件包管理器通过下列两种方式使用 .apk 签名:更换应用时,必须使用与旧应用相同的密钥对其签名,才能存取旧应用的数据。无论是通过覆盖 .a…...
亚马逊、沃尔玛卖家自养号退款经验和测评技术
今天给大家介绍下在做亚马逊、沃尔玛退款自养号中的经验,众所周知,自养号最重要的是养号的环境,包括系统的纯净度,下单的信用卡以及其他的一些细节。 环境系统市面上有很多,鱼龙混杂,比如什么lumi…...
Spring Security in Action 第十一章 SpringSecurity前后端分离实战
本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获&#…...
高级前端二面vue面试题(持续更新中)
action 与 mutation 的区别 mutation 是同步更新, $watch 严格模式下会报错 action 是异步操作,可以获取数据后调用 mutation 提交最终数据 MVVM的优缺点? 优点: 分离视图(View)和模型(Model)ÿ…...
七大设计原则之依赖倒置原则应用
目录1 依赖倒置原则2 依赖倒置应用1 依赖倒置原则 依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。…...
Dubbo面试题2023
1、为什么要用Dubbo 随着服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务 的架构体系(SOA),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、 …...
Swift(5)
目录 集合类型 数组 编辑 合集 合集操作 字典 Where 集合类型 Swift提供了三种主要的集合类型:组合,合集,字典。 数组是有序的值的集合。 合集是唯一值的无序集合。 字典是无序的键值对集合。 数组 Swift数组的类型的完整写法是…...
[Java 进阶面试题] CAS 和 Synchronized 优化过程
最有用的东西,是你手里的钱,有钱就有底气,还不快去挣钱~ 文章目录CAS 和 Synchronized 优化过程1. CAS1.1 CAS的原理1.2 CAS实现自增自减的原子性1.3 CAS实现自旋锁1.4 CAS针对ABA问题的优化2. synchronized2.1 synchronized加锁阶段分析2.2 synchronized优化CAS 和 Synchroniz…...
算法思想 - 贪心算法
本文主要介绍算法中贪心算法的思想: 保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。贪心思想相关题目分配饼干455. Assign Cookies (Easy)Input: [1,2], [1,2,3] Output: 2Explanation: You have 2 children and 3 cookies. The greed factors of 2 …...
解决需求变更难题的8大方案
需求变更8大原因为什么会出现需求变更,这是由于需求约束、规则有了新的变化、由于政策发生变化,客户、沟通方式、流程化、标准化的问题等导致。这里在在过去的项目经验中,提出了常见的8大需求变更的原因。政策发生变化:指由于国家…...
企业网站设计合同/浙江网站建设营销
目录 快速入门 1.基础标签 练习:公司简介案例 2.图片、音频、视频标签 3.超链接标签 4.列表标签 5.表格标签 练习:课程表 6.布局标签 7.表单标签 基本使用 表单项 快速入门 <html><head><title>html 快速入门</title&g…...
wordpress做的网站/邢台市seo服务
视频链接 一.:课程说明(1~1)...
怎样用dw做网站主页/目前病毒的最新情况
seq2seq模型目前还有很多缺点,本文所做实验表明: (1)生成的文本过短,3%的摘要不超过3个词 (2)随着生成序列的增加,生成性能急剧恶化 (3)重复生成某个词 &…...
网站的购物车怎么做/全国seo搜索排名优化公司
问题 在序列化枚举字段的时候,出现只序列化一个string的枚举名字。这对于普通java枚举类问题不大的,但是对于稍复杂一点java枚举类,就会出现字段丢失的情况。例如,如下枚举类: import com.fasterxml.jackson.annotati…...
容桂最新消息/吉林网络seo
1082. Read Number in Chinese (25) 时间限制400 ms内存限制65536 kB代码长度限制16000 B判题程序Standard作者CHEN, YueGiven an integer with no more than 9 digits, you are supposed to read it in the traditional Chinese way. Output "Fu" first if it is ne…...
做外贸网站选美国服务器的费用/电商的运营模式有几种
过滤器的区别 捕捉过滤器(CaptureFilters):用于决定将什么样的信息记录在捕捉结果中。需要在开始捕捉前设置。显示过滤器(DisplayFilters):在捕捉结果中进行详细查找。他们可以在得到捕捉结果后随意修改。那…...