PKCE 全称是 Proof Key for Code Exchange(代码交换证明密钥), 在2015年发布, 它是 OAuth 2.0 核心的一个扩展协议, 所以可以和现有的授权模式结合使用,比如 Authorization Code + PKCE, 这也是最佳实践,PKCE 最初是为移动设备应用和本地应用创建的, 主要是为了减少公共客户端的授权码拦截攻击。
PKCE其实主要是通过在授权的过程中增加了code_challenge和code_verifier两个元素来对整个流程进行验证,防止code被第三方截取的情况。 实际上它的原理是客户端提供一个自创建的证明给授权服务器, 授权服务器通过它来验证客户端,把访问令牌(access_token) 颁发给真实的客户端而不是伪造的。
参考
参考2
https://www.rfc-editor.org/rfc/rfc6749

(A)客户端请求资源所有者授权。 可以直接向资源所有者发出授权请求 (如所示),或者最好通过授权间接地 服务器作为中介。
(B)客户端收到授权授予,这是a 表示资源所有者授权的凭据, 使用本例中定义的四种授权类型之一表示 说明或使用延期授权类型方法使用的方法决定了授权授予类型
客户机请求授权和支持的类型 授权服务器。
©客户端通过认证请求访问令牌 授权服务器,并给出授权授权。
(D)授权服务器对客户端进行认证和验证 授权授予,如果有效,将发出访问令牌。

PKCE主要是通过在授权的过程中增加了code_challenge和code_verifier两个元素来对整个流程进行验证,防止code被第三方截取的情况。具体流程如下:

也就是在原来请求的token中添加了code_challenge和code_verifier参数
http://localhost:9000/oauth2/authorize?response_type=code&client_id=felord&scope=message.read message.write&state=46ge_TeI-dHuAnyv67nVmCcAmFgCVSZAqjTi9Om-1aA=&redirect_uri=http://127.0.0.1:8082/test/bar
http://localhost:9000/oauth2/authorize?response_type=code&client_id=felord&scope=message.read%20message.write&state=NAqBLbmooEhMPGELwleACOHvybODP_hctqV-PuNjuxo%3D&redirect_uri=http://127.0.0.1:8082/test/bar&code_challenge=mQ4xjFrCXg-1P4iURSXcSCmGVc-dloG0b0sdGpICrN0&code_challenge_method=S256
pkce 分支
OAuth2客户端要生成code_verifier,并使用 code_challenge_method 计算 code_challenge,并封装到授权请求参数,需要改造OAuth2AuthorizationRequestResolver接口。
@Bean
SecurityFilterChain customSecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = accessTokenResponseClient();
//TODO OAuth2客户端测要生成code_verifier,并使用 code_challenge_method 计算 code_challenge,并封装到授权请求参数,需要改造OAuth2AuthorizationRequestResolver接口。
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
authorizationRequestResolver.setAuthorizationRequestCustomizer(builder -> builder.attributes(attributes -> {
if (!attributes.containsKey(PkceParameterNames.CODE_VERIFIER)) {
String codeVerifier = this.secureKeyGenerator.generateKey();
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
builder.additionalParameters(additionalParameters -> {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
} catch (NoSuchAlgorithmException ex) {
// plain 方式 这种方式几乎作废了
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
}
});
}
}));
http.authorizeRequests((requests) -> requests
.antMatchers("/test/bar", "/oauth2/jwks")
.hasAnyAuthority("ROLE_ANONYMOUS", "SCOPE_userinfo")
.anyRequest().authenticated())
//然后把改造好的OAuth2AuthorizationRequestResolver配置到HttpSecurity: .oauth2Login().authorizationEndpoint().authorizationRequestResolver(authorizationRequestResolver)
.and()
// 获取token端点配置 比如根据code 获取 token
.tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient);
http.oauth2Client()
.authorizationCodeGrant().authorizationRequestResolver(authorizationRequestResolver)
.accessTokenResponseClient(accessTokenResponseClient);
return http.build();
}
客户端配置要添加 .requireProofKey(true)
private RegisteredClient createJwtRegisteredClient(final String id) {
return RegisteredClient.withId(id)
// 客户端ID和密码
.clientId("testid")
// 名称 可不定义
.clientName("test")
// jwt 断言必备
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
.clientSettings(ClientSettings.builder()
.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
// 开启PKCE
.requireProofKey(true)
// private key jwt
.jwkSetUrl("http://localhost:8082/oauth2/jwks")
.build())
.build();
}