Spring Security OAuth2 带有用于代码交换的证明密钥 (PKCE) 的授权码流

概述

OAuth2依据是否能持有客户端密钥,将客户端分为两种类型:公共客户端保密客户端

保密客户端在服务器上运行,在前面介绍OAuth2文章中Spring Boot创建的应用程序是保密客户端类型的示例。首先它们在服务器上运行,并且通常位于具有其他保护措施防火墙或网关的后面。

公共客户端的代码一般会以某种形式暴露给最终用户,要么是在浏览器中下载执行,要么是直接在用户的设备上运行。例如原生应用是直接在最终用户的设备(计算机或者移动设备)上运行的应用。这类应用在使用OAuth2协议时,我们无法保证为此应用颁发的客户端密钥能安全的存储,因为这些应用程序在运行之前会完全下载到设备上,反编译应用程序将完全显示客户端密钥。

同样存在此安全问题还有单页应用(SPA),浏览器本身是一个不安全的环境,一旦你加载JavaScript应用程序,浏览器将会下载整个源代码以便运行它,整个源代码,包括其中的任何 客户端密钥,都将可见。如果你构建一个拥有100000名用户的应用程序,那么很可能这些用户中的一部分将感染恶意软件或病毒,并泄漏客户端密钥。

你可能会想,“如果我通过将客户端密钥拆分为几个部分进行混淆呢?”这不可否认会为你争取点时间,但真正有决心的人仍可能会弄清楚。

为了规避这种安全风险,最好使用代码交换证明密钥(PKCE)。

Proof Key for Code Exchange

PKCE 有自己独立的规范。它使应用程序能够在公共客户端中使用授权码流程。

  1. 用户在客户端请求资源。

  2. 客户端创建并记录名为 code_verifier 的秘密信息,然后客户端根据 code_verifier 计算出 code_challenge,它的值可以是 code_verifier,也可以是 code_verifier 的 SHA-256 散列,但是应该优先考虑使用密码散列,因为它能防止验证器本身遭到截获。

  3. 客户端将 code_challenge 以及可选的 code_challenge_method(一个关键字,表 示原文或者 SHA-256 散列)与常规的授权请求参数一起发送给授权服务器。

  4. 授权服务器将用户重定向到登录页面。

  5. 用户使进行身份验证,并且可能会看到一个同意页面,其中列出了 授权服务器将授予客户端的权限。

  6. 授权服务器将 code_challenge 和 code_challenge_method(如果有 的话)记录下来。授权服务器会将这些信息与颁发的授权码关联起来,并携带code重定向回客户端。

  7. 客户端接收到授权码之后,携带之前生成的 code_verifier 执行令牌请求。

  8. 授权服务器根据code_verifier计算出 code_challenge,并检查是否与最初提交的code_challenge一致。

  9. 授权服务器向客户端发送令牌。

  10. 客户端向受保护资源发送令牌。

  11. 受保护资源向客户端返回资源。

使用Spring Authorization Server搭建授权服务器

本节我们将使用Spring Authorization Server搭建一个授权服务器,并注册一个客户端使之支持PKCE。

maven

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>2.6.7</version>
</dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>0.3.1</version>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.6.7</version>
</dependency>

配置

首先很简单,我们将创建application.yml文件,并指定授权服务器端口为8080:

server:port: 8080

之后我们将创建一个OAuth2ServerConfig配置类,并在此类中我们将创建OAuth2授权服务所需特定Bean:

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);return http.exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))).build();
}@Bean
public RegisteredClientRepository registeredClientRepository() {RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("relive-client").clientAuthenticationMethods(s -> {s.add(ClientAuthenticationMethod.NONE);//客户端认证模式为none}).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-client-pkce").scope("message.read").clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(true) //仅支持PKCE.build()).tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 生成JWT令牌.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).accessTokenTimeToLive(Duration.ofSeconds(30 * 60)).refreshTokenTimeToLive(Duration.ofSeconds(60 * 60)).reuseRefreshTokens(true).build()).build();return new InMemoryRegisteredClientRepository(registeredClient);
}@Bean
public ProviderSettings providerSettings() {return ProviderSettings.builder().issuer("http://127.0.0.1:8080").build();
}@Bean
public JWKSource<SecurityContext> jwkSource() {RSAKey rsaKey = Jwks.generateRsa();JWKSet jwkSet = new JWKSet(rsaKey);return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}static class Jwks {private Jwks() {}public static RSAKey generateRsa() {KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();return new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();}
}static class KeyGeneratorUtils {private KeyGeneratorUtils() {}static KeyPair generateRsaKey() {KeyPair keyPair;try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);keyPair = keyPairGenerator.generateKeyPair();} catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}
}

请注意在创建RegisteredClient注册客户端类中,1.我们没有定义client_secret;2.客户端认证模式指定为none;3.requireProofKey()设置为true,此客户端仅支持PKCE。

其余配置我这里就不一一说明,可以参考之前文章。

接下来,我们创建一个Spring Security的配置类,指定Form表单认证和设置用户名密码:

@Configuration
public class SecurityConfig {@BeanSecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests(authorizeRequests ->authorizeRequests.anyRequest().authenticated()).formLogin(withDefaults());return http.build();}@BeanUserDetailsService users() {UserDetails user = User.withDefaultPasswordEncoder().username("admin").password("password").roles("USER").build();return new InMemoryUserDetailsManager(user);}@BeanPasswordEncoder passwordEncoder() {return PasswordEncoderFactories.createDelegatingPasswordEncoder();}
}

至此我们就已经配置好了一个简单的授权服务器。

OAuth2客户端

本节中我们使用Spring Security创建一个客户端,此客户端通过PKCE授权码流向授权服务器请求授权,并将获取的access_token发送到资源服务。

maven

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.6.7</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>2.6.7</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId><version>2.6.7</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-webflux</artifactId><version>5.3.9</version>
</dependency>
<dependency><groupId>io.projectreactor.netty</groupId><artifactId>reactor-netty</artifactId><version>1.0.9</version>
</dependency>

配置

首先我们将在application.yml中配置客户端信息,并指定服务端口号为8070:

server:port: 8070servlet:session:cookie:name: CLIENT-SESSIONspring:security:oauth2:client:registration:messaging-client-pkce:provider: client-providerclient-id: relive-clientclient-secret: relive-clientauthorization-grant-type: authorization_codeclient-authentication-method: noneredirect-uri: "http://127.0.0.1:8070/login/oauth2/code/{registrationId}"scope: message.readclient-name: messaging-client-pkceprovider:client-provider:authorization-uri: http://127.0.0.1:8080/oauth2/authorizetoken-uri: http://127.0.0.1:8080/oauth2/token

接下来,我们创建Spring Security配置类,启用OAuth2客户端。

@Configuration
public class SecurityConfig {@BeanSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests(authorizeRequests ->//便于测试,将权限开放authorizeRequests.anyRequest().permitAll()).oauth2Client(withDefaults());return http.build();}@BeanWebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);return WebClient.builder().filter(oauth2Client).build();}@BeanOAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,OAuth2AuthorizedClientRepository authorizedClientRepository) {OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder().authorizationCode().refreshToken().build();DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);return authorizedClientManager;}
}

上述配置类中我们通过oauth2Client(withDefaults())启用OAuth2客户端。并创建一个WebClient实例用于向资源服务器执行HTTP请求。OAuth2AuthorizedClientManager这是协调OAuth2授权码请求的高级控制器类,不过授权码流程并不是由它控制,可以查看它所管理的Provider实现类AuthorizationCodeOAuth2AuthorizedClientProvider中并没有涉及相关授权码流程代码逻辑,对于Spring Security授权码模式涉及核心接口流程我会放在之后的文章统一介绍。回到OAuth2AuthorizedClientManager类中,我们可以看到同时还指定了refreshToken(),它实现了刷新token逻辑,将在请求资源服务过程中access_token过期后将刷新token,前提是refresh_token没有过期,否则你将重新执行OAuth2授权码流程。

接下来,我们创建一个Controller类,使用WebClient请求资源服务:

@RestController
public class PkceClientController {@Autowiredprivate WebClient webClient;@GetMapping(value = "/client/test")public List getArticles(@RegisteredOAuth2AuthorizedClient("messaging-client-pkce") OAuth2AuthorizedClient authorizedClient) {return this.webClient.get().uri("http://127.0.0.1:8090/resource/article").attributes(oauth2AuthorizedClient(authorizedClient)).retrieve().bodyToMono(List.class).block();}
}

资源服务器

本节中,我们将使用Spring Security搭建一个资源服务器。

maven

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.6.7</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>2.6.7</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId><version>2.6.7</version>
</dependency>

配置

通过application.yml配置资源服务器服务端口8070,并指定授权服务器jwk uri,用于获取公钥信息验证token:

server:port: 8090spring:security:oauth2:resourceserver:jwt:jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks

接下来配置Spring Security配置类,指定受保护端点访问权限:

@Configuration
public class SecurityConfig {@Beanpublic SecurityFilterChain defaultSecurityFilter(HttpSecurity http) throws Exception {http.requestMatchers().antMatchers("/resource/article").and().authorizeHttpRequests((authorize) -> authorize.antMatchers("/resource/article").hasAuthority("SCOPE_message.read").mvcMatchers()).oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);return http.build();}
}

上述配置类中指定/resource/article必须拥有message.read权限才能访问,并配置资源服务使用JWT身份验证。

之后我们将创建Controller类,作为受保护端点:

@RestController
public class ArticleRestController {@GetMapping("/resource/article")public List<String> article() {return Arrays.asList("article1", "article2", "article3");}
}

访问资源列表

启动所有服务后,在浏览器中输入 http://127.0.0.1:8070/client/test ,通过授权服务器认证后,您将在页面中看到以下输出信息:

["article1","article2","article3"]

结论

在Spring Security目前版本中保密客户端的 PKCE 已经成为默认行为。在保密客户端授权码模式中同样可以使用PKCE。

与往常一样,本文中使用的源代码可在 GitHub 上获得。

Spring Security OAuth2 带有用于代码交换的证明密钥 (PKCE) 的授权码流相关推荐

  1. 使用Spring Security OAuth2实现单点登录(SSO)系统

    一.单点登录SSO介绍   目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的 ...

  2. 还不了解Oauth2协议?这篇文章从入门到入土让你了解Oauth2以及Spring Security OAuth2 的使用

    SpringSecurityOAuth2学习和实战 1.OAuth2概述 1.1 什么是OAuth2 OAuth(Open Authorization)是一个关于授权(authorization)的开 ...

  3. spring security Oauth2验证码等多方式登录

    前言 基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spr ...

  4. Spring Security OAuth2 优雅的集成短信验证码登录以及第三方登录

    基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring ...

  5. Spring Security OAuth2.0认证授权知识概括

    Spring Security OAuth2.0认证授权知识概括 安全框架基本概念 基于Session的认证方式 Spring Security简介 SpringSecurity详解 分布式系统认证方 ...

  6. 004-云E办_学习Oathu2和Spring Security Oauth2

    这里写目录标题 一.Oauth2简介 1.简介 2.分析Oauth2认证的例子,网站使用微信认证的过程: 3.Oauth2.0认证流程如下: 1.角色: 2.常用术语: 3.令牌类型 4.特点 二.授 ...

  7. Spring Security OAuth2 单点登录和登出

    文章目录 1. 单点登录 1.1 使用内存保存客户端和用户信息 1.1.1 认证中心 auth-server 1.1.2 子系统 service-1 1.1.3 测试 1.2 使用数据库保存客户端和用 ...

  8. Spring Security OAuth2.0认证授权

    文章目录 1.基本概念 1.1.什么是认证 1.2 什么是会话 1.3什么是授权 1.4授权的数据模型 1.4 RBAC 1.4.1 基于角色的访问控制 2.基于Session的认证方式 3.整合案例 ...

  9. 微服务安全Spring Security OAuth2实战

    文章目录 一.OAuth2.0介绍 1.1 应用场景 1.2 基本概念 1.3 优缺点 二.OAuth2的设计思路 2.1 客户端授权模式 授权码模式 简化(隐式)模式 密码模式 客户端模式 2.2 ...

最新文章

  1. 离线版的SAP中F1帮助
  2. 【NLP】预训练时代下的文本生成|模型技巧
  3. sql语句的一些参考
  4. python使用笔记
  5. 转 Android的Activity屏幕切换动画(一)-左右滑动切换
  6. 你不是不擅长数学,你只是打开方式不对
  7. centos 6.5 yum
  8. 在域控制器上安装Exchange 2003的注意事项
  9. C#3.0亮点 —— lambda表达式
  10. 在mudbuilder上的胡扯1
  11. VS2012 安装番茄助手
  12. 浮动时间怎么计算_轻松搞定PMP考试的计算题(四)时间参数计算
  13. linux加密自己的smb目录,SmbFile连接加密共享文件夹
  14. SLAM--LSD_SLAM在高版本系统中运行(ubuntu20.04 ROS-noetic)
  15. DHCP 客户端移动位置后无法获取IP地址的解决办法和原因分析
  16. 1002. 写出这个数 (20)练习
  17. Atcoder题解与视频集
  18. 【实习日记】实习第N天 从零开始搭建一个tiktok puppet(一)
  19. sprintf()和itoa()的区别
  20. 拨乱反正:MyISAM中key_buffer_size的设置

热门文章

  1. Bootstrap-table formatter
  2. 【附源码】Java计算机毕业设计户籍管理系统(程序+LW+部署)
  3. 《达梦数据库运维实战》 发售了
  4. 【2022南京大学操作系统(蒋岩炎)】(一)操作系统概述 | 操作系统上的程序
  5. matlab projinv,有没有大神救救孩子?
  6. 使用openCV 的cv2.imread函数读取图片找不到路径
  7. 计算机课程设计答辩评语,课程设计指导教师评语
  8. 参考学习的各种跑马灯代码
  9. c语言贪吃蛇程序尾巴掉了,c语言贪吃蛇 要怎么重新开始 要怎么写
  10. 问题“找不到Microsoft Access Driver(*.mdb)ODBC驱动程序的安装例程”的解决方法