Spring Security OAuth2 带有用于代码交换的证明密钥 (PKCE) 的授权码流
Spring Security OAuth2 带有用于代码交换的证明密钥 (PKCE) 的授权码流
概述
OAuth2依据是否能持有客户端密钥,将客户端分为两种类型:公共客户端和保密客户端。
保密客户端在服务器上运行,在前面介绍OAuth2文章中Spring Boot创建的应用程序是保密客户端类型的示例。首先它们在服务器上运行,并且通常位于具有其他保护措施防火墙或网关的后面。
公共客户端的代码一般会以某种形式暴露给最终用户,要么是在浏览器中下载执行,要么是直接在用户的设备上运行。例如原生应用是直接在最终用户的设备(计算机或者移动设备)上运行的应用。这类应用在使用OAuth2协议时,我们无法保证为此应用颁发的客户端密钥能安全的存储,因为这些应用程序在运行之前会完全下载到设备上,反编译应用程序将完全显示客户端密钥。
同样存在此安全问题还有单页应用(SPA),浏览器本身是一个不安全的环境,一旦你加载JavaScript应用程序,浏览器将会下载整个源代码以便运行它,整个源代码,包括其中的任何 客户端密钥,都将可见。如果你构建一个拥有100000名用户的应用程序,那么很可能这些用户中的一部分将感染恶意软件或病毒,并泄漏客户端密钥。
你可能会想,“如果我通过将客户端密钥拆分为几个部分进行混淆呢?”这不可否认会为你争取点时间,但真正有决心的人仍可能会弄清楚。
为了规避这种安全风险,最好使用代码交换证明密钥(PKCE)。
Proof Key for Code Exchange
PKCE 有自己独立的规范。它使应用程序能够在公共客户端中使用授权码流程。
用户在客户端请求资源。
客户端创建并记录名为 code_verifier 的秘密信息,然后客户端根据 code_verifier 计算出 code_challenge,它的值可以是 code_verifier,也可以是 code_verifier 的 SHA-256 散列,但是应该优先考虑使用密码散列,因为它能防止验证器本身遭到截获。
客户端将 code_challenge 以及可选的 code_challenge_method(一个关键字,表 示原文或者 SHA-256 散列)与常规的授权请求参数一起发送给授权服务器。
授权服务器将用户重定向到登录页面。
用户使进行身份验证,并且可能会看到一个同意页面,其中列出了 授权服务器将授予客户端的权限。
授权服务器将 code_challenge 和 code_challenge_method(如果有 的话)记录下来。授权服务器会将这些信息与颁发的授权码关联起来,并携带code重定向回客户端。
客户端接收到授权码之后,携带之前生成的 code_verifier 执行令牌请求。
授权服务器根据code_verifier计算出 code_challenge,并检查是否与最初提交的code_challenge一致。
授权服务器向客户端发送令牌。
客户端向受保护资源发送令牌。
受保护资源向客户端返回资源。
使用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) 的授权码流相关推荐
- 使用Spring Security OAuth2实现单点登录(SSO)系统
一.单点登录SSO介绍 目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的 ...
- 还不了解Oauth2协议?这篇文章从入门到入土让你了解Oauth2以及Spring Security OAuth2 的使用
SpringSecurityOAuth2学习和实战 1.OAuth2概述 1.1 什么是OAuth2 OAuth(Open Authorization)是一个关于授权(authorization)的开 ...
- spring security Oauth2验证码等多方式登录
前言 基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spr ...
- Spring Security OAuth2 优雅的集成短信验证码登录以及第三方登录
基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring ...
- Spring Security OAuth2.0认证授权知识概括
Spring Security OAuth2.0认证授权知识概括 安全框架基本概念 基于Session的认证方式 Spring Security简介 SpringSecurity详解 分布式系统认证方 ...
- 004-云E办_学习Oathu2和Spring Security Oauth2
这里写目录标题 一.Oauth2简介 1.简介 2.分析Oauth2认证的例子,网站使用微信认证的过程: 3.Oauth2.0认证流程如下: 1.角色: 2.常用术语: 3.令牌类型 4.特点 二.授 ...
- Spring Security OAuth2 单点登录和登出
文章目录 1. 单点登录 1.1 使用内存保存客户端和用户信息 1.1.1 认证中心 auth-server 1.1.2 子系统 service-1 1.1.3 测试 1.2 使用数据库保存客户端和用 ...
- Spring Security OAuth2.0认证授权
文章目录 1.基本概念 1.1.什么是认证 1.2 什么是会话 1.3什么是授权 1.4授权的数据模型 1.4 RBAC 1.4.1 基于角色的访问控制 2.基于Session的认证方式 3.整合案例 ...
- 微服务安全Spring Security OAuth2实战
文章目录 一.OAuth2.0介绍 1.1 应用场景 1.2 基本概念 1.3 优缺点 二.OAuth2的设计思路 2.1 客户端授权模式 授权码模式 简化(隐式)模式 密码模式 客户端模式 2.2 ...
最新文章
- 离线版的SAP中F1帮助
- 【NLP】预训练时代下的文本生成|模型技巧
- sql语句的一些参考
- python使用笔记
- 转 Android的Activity屏幕切换动画(一)-左右滑动切换
- 你不是不擅长数学,你只是打开方式不对
- centos 6.5 yum
- 在域控制器上安装Exchange 2003的注意事项
- C#3.0亮点 —— lambda表达式
- 在mudbuilder上的胡扯1
- VS2012 安装番茄助手
- 浮动时间怎么计算_轻松搞定PMP考试的计算题(四)时间参数计算
- linux加密自己的smb目录,SmbFile连接加密共享文件夹
- SLAM--LSD_SLAM在高版本系统中运行(ubuntu20.04 ROS-noetic)
- DHCP 客户端移动位置后无法获取IP地址的解决办法和原因分析
- 1002. 写出这个数 (20)练习
- Atcoder题解与视频集
- 【实习日记】实习第N天 从零开始搭建一个tiktok puppet(一)
- sprintf()和itoa()的区别
- 拨乱反正:MyISAM中key_buffer_size的设置
热门文章
- Bootstrap-table formatter
- 【附源码】Java计算机毕业设计户籍管理系统(程序+LW+部署)
- 《达梦数据库运维实战》 发售了
- 【2022南京大学操作系统(蒋岩炎)】(一)操作系统概述 | 操作系统上的程序
- matlab projinv,有没有大神救救孩子?
- 使用openCV 的cv2.imread函数读取图片找不到路径
- 计算机课程设计答辩评语,课程设计指导教师评语
- 参考学习的各种跑马灯代码
- c语言贪吃蛇程序尾巴掉了,c语言贪吃蛇 要怎么重新开始 要怎么写
- 问题“找不到Microsoft Access Driver(*.mdb)ODBC驱动程序的安装例程”的解决方法