Spring Security OAuth 2 教程 - 4:PKCE 授权码模式
在 “Spring Security OAuth 2 教程 - 3:客户端凭证模式” 中,我们学习了如何通过客户端凭证模式获取访问令牌(access_token)。在本文中,我们将了解如何使用 PKCE 授权码模式。
PKCE 授权码模式
PKCE 授权码模式是 OpenId Connect Flow,主要用于保护本地、移动应用和单页应用 (SPA) 的安全。PKCE 是代码交换证明密钥(Proof Key for Code Exchange)的首字母缩写。
注意
PKCE 授权码模式也可用于保护服务器上运行的 Web 应用的安全。在这种情况下,PKCE 起到了额外保护层的作用。
创建 “public” 客户端
创建一个名为 messages-spa
的新客户端。
- General Settings:
- Client type:OpenID Connect
- Client ID:messages-spa
- Capability config:
- Client authentication:Off
- Authorization:Off
- Authentication flow:选中 Standard flow,取消选中其余复选框
- Login settings:
- Root URL:
http://localhost:3000
- Home URL:
http://localhost:3000
- Valid redirect URIs:
http://localhost:3000/callback
- Valid post logout redirect URIs:
http://localhost:3000
- Web origins:
http://localhost:3000
- Root URL:
使用上述配置创建客户端后,你将进入新创建的客户端 “Settings” 页面。单击 “Advanced” 选项卡,转到 “Advanced Settings” 部分,将 “Proof Key for Code Exchange Code Challenge Method” 值更新为 S256
。
PKCE 授权码模式获取 Access Token
在使用 PKCE 授权码模式时,首先通过前端(浏览器网址)上的重定向 URL 获取 authorization_code
。
首先,我们需要生成一个 code verifier,它是一个使用 A-Z、a-z、0-9 和标点符号 -._~(连字符、句号、下划线和斜线)的密码随机字符串,长度在 43 到 128 个字符之间。
生成代 code verifier 后,生成 code challenge,这是 code verifier SHA256 哈希值的 BASE64-URL 编码字符串。
你可以通过 https://www.oauth.com/playground/authorization-code-with-pkce.html 生成 code verifier 和 code challenge。
- code verifier: M7Q3C-V-_BafRd251gvQLhHw5lYRUYuAlbtT7BCF8cnDiHSV
- code challenge: gD2zOSHT__2UcU_BkXqqMld3b7TQ764LaPUOXMSDCMw
现在,在浏览器窗口中打开以下 URL:
http://localhost:9191/realms/sivalabs/protocol/openid-connect/auth?
response_type=code
&client_id=messages-spa
&redirect_uri=http://localhost:3000/callback
&state=randomstring
&response_mode=query
&scope=openid
&code_challenge=gD2zOSHT__2UcU_BkXqqMld3b7TQ764LaPUOXMSDCMw
&code_challenge_method=S256
- 然后,你将被重定向到 Keycloak 的登录页面。
- 使用用户凭证
siva/siva1234
登录。 - 然后,你将被重定向到包含查询参数
code
的重定向 URI。
http://localhost:3000/callback?
state=randomstring
&session_state=d0da5382-4408-4548-af53-fe4fb948c18c
&code=052aa7cf-a868-4b95-aa59-6a75eaff26d8.d0da5382-4408-4548-af53-fe4fb948c18c.63de5a66-e986-46ca-9b0f-3944529f3ad9
authorization_code 是 052aa7cf-a868-4b95-aa59-6a75eaff26d8.d0da5382-4408-4548-af53-fe4fb948c18c.63de5a66-e986-46ca-9b0f-3944529f3ad9。
有了 authorization_code
,我们就可以通过 token_endpoint
获取 access_token
了,如下所示:
curl --location 'http://localhost:9191/realms/sivalabs/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=messages-spa' \
--data-urlencode 'redirect_uri=http://localhost:3000/callback' \
--data-urlencode 'code_verifier=M7Q3C-V-_BafRd251gvQLhHw5lYRUYuAlbtT7BCF8cnDiHSV' \
--data-urlencode 'code=052aa7cf-a868-4b95-aa59-6a75eaff26d8.d0da5382-4408-4548-af53-fe4fb948c18c.63de5a66-e986-46ca-9b0f-3944529f3ad9'
在这里,我们发送的原始 code_verifier 应与 authorization_code
请求中发送的 code_verifier 相匹配。
返回的 JSON 响应应如下所示:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeVVPTDg4LVBGM3BYQzFpN3BIeGdFZTJwaWZJY3RyTXJiNklHOElmRTlVIn0.eyJleHAiOjE2OTU1NzAyNjcsImlhdCI6MTY5NTU2OTk2NywiYXV0aF90aW1lIjoxNjk1NTY5OTQxLCJqdGkiOiJkNzNjOTk4MS1jYzhmLTQ2NzItODcwYy03YzI2OTQ3ZDY3ZTkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkxOTEvcmVhbG1zL3NpdmFsYWJzIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImNhMWEyZjM0LTE2MTQtNDVkZC04NmMxLTVlYWZmZjA4NWQ4YSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im1lc3NhZ2VzLXNwYSIsInNlc3Npb25fc3RhdGUiOiJmZDU1YTdmZi0xNTI2LTQ4MjYtODM1OS00MTFkZTU0ZmJhMWYiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1zaXZhbGFicyIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwic2lkIjoiZmQ1NWE3ZmYtMTUyNi00ODI2LTgzNTktNDExZGU1NGZiYTFmIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJTaXZhIEthdGFtcmVkZHkiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzaXZhIiwiZ2l2ZW5fbmFtZSI6IlNpdmEiLCJmYW1pbHlfbmFtZSI6IkthdGFtcmVkZHkiLCJlbWFpbCI6InNpdmFAZ21haWwuY29tIn0.f4Ic6ggLBteou71NHzBWaCdcxKaaXnCSM38Glg3Xs1ruQz7uMtMJKewJOpZmCwrvodg0xCF0TZEl-wKWz3CnWEFFE92nIGGp3BIL42Coc4f8_aB_M0YvH3hUQznswYgZLcqvpN3k3e4yA70NfU9LWbNJudBkaLBYCDgPv62_t5620TPyg4cYxjcf2HHoCG4pU3Ms5DX-Zu6tc-aa3RT0uXp7CNzgQF3yqP0kmyu9SY8hkhLV05hdtsc5Szj0mH0e8T55IajM8Z_eYCi20VFaBehohM_Zr6FsP_S69numxhHLBy06JgQd9zUV8DpMPhOC0R4oTla4RpmPZo0B5ApFmw",
"expires_in": 300,
"refresh_expires_in": 1774,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N2E1NTU3Ni01MThlLTQ1MDItOWQyNi1jNzVmYjZhNGRhZWEifQ.eyJleHAiOjE2OTU1NzE3NDEsImlhdCI6MTY5NTU2OTk2NywianRpIjoiNDYwMzlmNGQtMjBjZi00OWUxLTk2ZWEtNjI4NmI0MjdiOWQ2IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MTkxL3JlYWxtcy9zaXZhbGFicyIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTE5MS9yZWFsbXMvc2l2YWxhYnMiLCJzdWIiOiJjYTFhMmYzNC0xNjE0LTQ1ZGQtODZjMS01ZWFmZmYwODVkOGEiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibWVzc2FnZXMtc3BhIiwic2Vzc2lvbl9zdGF0ZSI6ImZkNTVhN2ZmLTE1MjYtNDgyNi04MzU5LTQxMWRlNTRmYmExZiIsInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJmZDU1YTdmZi0xNTI2LTQ4MjYtODM1OS00MTFkZTU0ZmJhMWYifQ.QGgw00X_biM0kMLFDEG14UhjCaDBSuk1K8euqTxQpWk",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeVVPTDg4LVBGM3BYQzFpN3BIeGdFZTJwaWZJY3RyTXJiNklHOElmRTlVIn0.eyJleHAiOjE2OTU1NzAyNjcsImlhdCI6MTY5NTU2OTk2NywiYXV0aF90aW1lIjoxNjk1NTY5OTQxLCJqdGkiOiJjYjViMTQ0Ni0zOGQxLTQwN2ItYjIwYy03ZjQ0Mjg1MmZhYmYiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkxOTEvcmVhbG1zL3NpdmFsYWJzIiwiYXVkIjoibWVzc2FnZXMtc3BhIiwic3ViIjoiY2ExYTJmMzQtMTYxNC00NWRkLTg2YzEtNWVhZmZmMDg1ZDhhIiwidHlwIjoiSUQiLCJhenAiOiJtZXNzYWdlcy1zcGEiLCJzZXNzaW9uX3N0YXRlIjoiZmQ1NWE3ZmYtMTUyNi00ODI2LTgzNTktNDExZGU1NGZiYTFmIiwiYXRfaGFzaCI6ImRETGgycU1RdU5FT3F4MkRSYVR3cGciLCJhY3IiOiIxIiwic2lkIjoiZmQ1NWE3ZmYtMTUyNi00ODI2LTgzNTktNDExZGU1NGZiYTFmIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJTaXZhIEthdGFtcmVkZHkiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzaXZhIiwiZ2l2ZW5fbmFtZSI6IlNpdmEiLCJmYW1pbHlfbmFtZSI6IkthdGFtcmVkZHkiLCJlbWFpbCI6InNpdmFAZ21haWwuY29tIn0.BTuFqcyrjB99R9kBfeLbVRSL5EMtgwS-BSgue77eGmFZxoBMV6UXM2qjWCoF9Wdozy2xmOQcqehvNGpnvR34YHfdrCxymuIZ7jIp7LvNW8sf5ZzsqnCq2OLCKPH4pyWFPqAGfvBYXqzhammNjPITKTKHH11nIcGnDGt7bhfDf-iDj3saDf0CZNkyOmh1Udi3Bkl3O4OBERWbg9xpsNGVks0SDONkDtLSL0IO1ODfgJRq1ZASRdn2i67EMDh5kpLnpKHPj7a1rMZyIUTdhbF4_sODWj3D6STFZxS_FjhcXdjYHLvtLdQBIOmHDjTqEXpwNp5OrVBtEvjgMj4ERNyoMw",
"not-before-policy": 0,
"session_state": "fd55a7ff-1526-4826-8359-411de54fba1f",
"scope": "openid email profile"
}
有了 access_token
,资源服务器就可以调用另一个资源服务器的受保护的 API 端点。
PKCE 如何保证安全?
当我第一次看到 PKCE 模式时,我想知道 PKCE 究竟是如何保证安全的?任何人都可以创建 code verifier 和 code challenge,而且不需要知道 client_secret。
你可以参阅 “PKCE 如何保证安全?” 这个问题的答案,以及“PKCE 究竟在保护什么?”
使用 Postman
我们可以使用 Postman,通过 “PKCE 授权码模式” 取 access_token,如下所示:
- 在 Postman 中打开新请求选项卡
- 转到 Authorization 选项卡,Type 选择 OAuth 2.0。
- 在 Configure New Token 部分:
- Grant Type:Authorization Code (With PKCE)
- Callback URL:
http://localhost:3000/callback
- Auth URL:
http://localhost:9191/realms/sivalabs/protocol/openid-connect/auth
- Access Token URL:
http://localhost:9191/realms/sivalabs/protocol/openid-connect/token
- Client ID:messages-spa
- Client Secret:""(留空)
- Code Challenge Method: S256
- Code Verifier: M7Q3C-V-_BafRd251gvQLhHw5lYRUYuAlbtT7BCF8cnDiHSV
- Scope: profile
- State: randomstring
- Client Authentication: Send as Basic Auth header
- 点击 Get New Access Token 按钮
- Postman 会在弹出窗口中显示 Keycloak 的登录页面
- 使用凭证 siva/siva1234 登录
- 现在你应该可以看到带有 Token 详细信息的响应了
如果将 scope 指定为 openid profile
,那么也会得到 id_token
。
总结
在本文中,我们学习了如何通过 PKCE 授权码模式获取 access_token
,以及如何使用 Postman 完成同样的操作。
下一章节,我们将了解 “OAuth 2.0 隐式模式和资源所有者密码模式” 的工作原理。
参考:https://www.sivalabs.in/spring-security-oauth2-tutorial-authorization-code-flow-with-pkce/