开讲之前需要先补充点预备知识:客户端的类型 https://oauth.net/2/client-types
OAuth2 定义了两种类型的客户端:机密客户端(confidential clients)和公共客户端(public clients)。
client_secret 的应用程序。例如我们的后台服务,将 client_secret 保存在配置中,和授权服务器交互都是后台操作,最终给到前端的只是 token,这样client_secret是安全的。client_secret 的应用程序。例如运行在浏览器的纯前端的应用程序,无论如何,将client_secret暴露在浏览器,都是不安全的。ok,铺垫完了,开讲。
由于公共客户端没法安全的保存 client_secret,所以在实际应用中,公共客户端 连 client_secret 都没必要存了,所以SpringAuthorizationServer定义一个 none 方式来表示这种情况。那这意味着公共客户端就不用认证了吗?答案是否定的!OAuth2 引入了另一个验证的机制 PKCE(Proof Key for Code Exchange)。
PKCE 是授权码流程的扩展,用于防止 CSRF 和授权码(code)注入攻击。 所以 PKCE 一般都伴随着授权码模式使用,可称之为 增强版授权码流程,又称 Authorization Code with PKCE Flow。
原来的授权码流程 如下:
\1. 客户端发起授权请求 -> 2. 用户授权 -> 3. 客户端拿到code -> 4. 客户端通过code获取token
授权码 + PKCE 流程 对原来流程做了如下增强:
一图胜千言:

整个示例吧,再说可能都要晕了。
code_challenge client生成的密文code_challenge_method:摘要算法,固定值 S256code_verifier:明文client_id:客户端id同样的,基于 快速搭建一个授权服务器 文章中的示例,修改 SecurityConfiguration 中 registeredClientRepository() 方法,如下:
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client1")
// 公共客户端,不需要密钥
// .clientSecret("01234567890123456789012345678912")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.clientSettings(ClientSettings.builder()
// 公共客户端(NONE方式认证)必须开启 PKCE 流程
.requireProofKey(true)
// 授权码模式需要用户手动授权!false表示默认通过
.requireAuthorizationConsent(true)
.build())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://cn.bing.com")
.scope("read")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
xx123,真正在项目用的时候得随机生成才安全!)public class ClientPkceTest {
public static void main(String[] args) throws Exception {
// 明文
String code_verifier = "xx123";
// 摘要算法
String code_challenge_method = "SHA-256";
// 密文
String code_challenge = calc(code_verifier, code_challenge_method);
System.out.println(code_challenge); // 3P6kopgvD5SwlNXyxCW-1DnPGJSNGGYn3H0vos0Xu4o
}
/**
* 明文 + 摘要算法,生成 密文
* @see CodeVerifierAuthenticator#codeVerifierValid(java.lang.String, java.lang.String, java.lang.String)
*/
private static String calc(String code_verifier, String code_challenge_method) throws Exception {
byte[] bytes = code_verifier.getBytes(StandardCharsets.US_ASCII);
MessageDigest md = MessageDigest.getInstance(code_challenge_method);
byte[] digest = md.digest(bytes);
String code_challenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
return code_challenge;
}
}
code_challenge、code_challenge_method 参数)http://localhost:9000/oauth2/authorize?response_type=code&client_id=client1&redirect_uri=https%3A%2F%2Fcn.bing.com&scope=read&code_challenge=3P6kopgvD5SwlNXyxCW-1DnPGJSNGGYn3H0vos0Xu4o&code_challenge_method=S256
code_verifier 参数)
这里涉及到有两处代码,一个授权请求是需要记录 密文和摘要算法,一个是获取token请求,计算明文和原来密文做验证。
虽然经过层层包装,但最终的效果就是 code_challenge、code_challenge_method 参数会被记录起来,并和 授权码(code)关联(后续通过code才能取回密文和摘要算法做验证)。暂时不用深究其他细节。

获取token请求,才是真正做校验的地方。会涉及如下关键类:
校验 code_verifier 参数是否存在,若不存在则报错。
核心逻辑就是:取出授权请求保存下来的code_challenge(密文)、code_challenge_method(摘要算法),和 code_verifier(明文)做校验。其入口代码如下:

委托给 CodeVerifierAuthenticator 处理

核心验证逻辑就是用相同的摘要算法计算明文,生成一个密文和 原来的密文对比。

以上,便是 pkce 整个流程。其他细节,读者可自行扩展了解啦啦啦~
end