在 "Spring Security OAuth 2 教程 - 1:熟悉 OAuth 2 概念" 中,我们学习了如何设置 Keycloak、创建 Realm、启用 Standard flow 的客户端和用户。在本文中,我们将了解如何通过 "授权码模式"(Authorization Code Flow)对用户进行身份认证。
首先,让我们澄清一下 "授权码授权方式"(Authorization Code Grant Type )与 "授权码模式"(Authorization Code Flow)之间的混淆。
正如我之前提到的,OAuth 2.0 规范仅关注授权(Authorization),而 OpenID Connect 规范是在 OAuth 2.0 之上添加的一层,用于处理身份认证(Authentication)。
"授权码授权方式"(Authorization Code Grant Type )是 OAuth 2.0 的术语,而 "授权码模式"(Authorization Code Flow )是 OpenID Connect 的术语。它们的工作方式相同,区别在于 scope。通过本文后面的示例,我们将更清楚地了解 "授权码模式" 的差异。
快速回顾一下,我们在上一教程中,创建的客户端(Client)和用户(User)的详细信息如下。
- Client id:messages-webapp
- Client secret :qVcg0foCUNyYbgF0Sg52zeIhLYyOwXpQ
- Username:siva
- Password:siva1234
OAuth 2.0 架构 {#oauth-20-架构}
以下是基于 OAuth 2.0 的系统的架构图:
- 资源所有者(Resource Owner) ,也就是终端用户,希望使用客户端(Client )应用访问其存储在资源服务器(Resource Server)上的数据。
- 资源服务器(Resource Server )数据受保护,需要访问令牌(access_token)才能访问数据。
- 客户端(Client )应用将管理用户、签发访问令牌(access_token )和认证用户的责任转交给了授权服务器(Authorization Server)。
- 当你(Resource Owner )试图在客户端(Client )应用上访问受保护的资源时,你将被重定向到授权服务器(Authorization Server),在那里你需要通过提供用户凭证来认证自己的身份。
- 如果认证成功,授权服务器(Authorization Server )就会向客户端(Client )签发访问令牌(access_token)。
- 然后,客户端(Client )就可以使用访问令牌(access_token)访问受保护的用户的数据了。
这是终端用户如何使用基于 OAuth 2.0 的安全机制访问受保护资源的流程。
现在,如果将这些不同的组件映射到当前的 Keycloak 设置中,它将如下所示:
- Authorization Server:Keycloak
- Client:messages-webapp(尚未创建)
- Resource Server: messages-service(尚未创建)
- Resource Owner:user(siva)
正如我在上一教程中提到的,获取 access_token
的方法有多种,如授权码、客户端凭证、隐式、资源所有者密码。
在这一部分中,我们将重点介绍如何使用 "授权码模式" 获取 access_token
。
在深入了解之前,首先看一下如何获取一些重要的 URL,这些 URL 将在接下来的过程中使用。
登录 Keycloak 管理控制台,选择 sivalabs
Realm,点击 "Realm settings"。你可以看到 "Endpoints" 部分和 "OpenID Endpoint Configuration" 链接。如果点击该链接,就会看到下面的 JSON 响应:
{
"issuer": "http://localhost:9191/realms/sivalabs",
"authorization_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/auth",
"token_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/token",
"introspection_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/userinfo",
"end_session_endpoint": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/logout",
"frontchannel_logout_session_supported": true,
"frontchannel_logout_supported": true,
"jwks_uri": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/certs",
"check_session_iframe": "http://localhost:9191/realms/sivalabs/protocol/openid-connect/login-status-iframe.html",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:openid:params:grant-type:ciba"
],
...,
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
...
...
}
我们将在 "授权码模式" 中使用 authorization_endpoint
、token_endpoint
。
授权码模式的认证 {#授权码模式的认证}
在 "授权码模式" 中,我们首先通过前端(浏览器网址)的重定向 URL 获取一个授权码(authorization_code
),然后使用该授权码以及 client_id
和 client_secret
信息,通过后端(服务器上的代码)获取 access_token
。
获取授权码 {#获取授权码}
首先,通过 authorization_endpoint
获取授权码,如下:
-
在浏览器窗口中打开以下 URL:
http://localhost:9191/realms/sivalabs/protocol/openid-connect/auth? response_type=code &client_id=messages-webapp &scope=profile &state=randomstring &redirect_uri=http://localhost:8080/callback
-
然后,你将被重定向到 Keycloak 的登录页面。
-
使用用户凭证
siva/siva1234
登录。 -
然后,你将被重定向到包含查询参数
code
的 Redirect URI。http://localhost:8080/callback? state=randomstring &session_state=07aecbd0-dc7e-487e-a1e5-ee36b3fdd5d7 &code=6e4fb072-df61-4d13-998d-469abf60492b.07aecbd0-dc7e-487e-a1e5-ee36b3fdd5d7.27a26554-da99-417c-a775-bf6559c98f02
authorization_code
是6e4fb072-df61-4d13-998d-469abf60492b.07aecbd0-dc7e-487e-a1e5-ee36b3fdd5d7.27a26554-da99-417c-a775-bf6559c98f02
。
注意 :在请求 URL 中指定的
redirect_uri
应与 "Login settings" 中配置的客户端的 "Valid redirect URI" 之一相匹配。
获取 AccessToken {#获取-accesstoken}
有了 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-webapp' \
--data-urlencode 'client_secret=qVcg0foCUNyYbgF0Sg52zeIhLYyOwXpQ' \
--data-urlencode 'code=6e4fb072-df61-4d13-998d-469abf60492b.07aecbd0-dc7e-487e-a1e5-ee36b3fdd5d7.27a26554-da99-417c-a775-bf6559c98f02' \
--data-urlencode 'redirect_uri=http://localhost:8080/callback' \
--data-urlencode 'scope=profile'
这将返回类似下面的 JSON 响应:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeVVPTDg4LVBGM3BYQzFpN3BIeGdFZTJwaWZJY3RyTXJiNklHOElmRTlVIn0.eyJleHAiOjE2OTU1NDAxODEsImlhdCI6MTY5NTUzOTg4MSwiYXV0aF90aW1lIjoxNjk1NTM5ODQ3LCJqdGkiOiIyYjRmNjNlMC0xNzg2LTRmYzctOTIxNy01ZDEzMjg5MDhhN2QiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkxOTEvcmVhbG1zL3NpdmFsYWJzIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImNhMWEyZjM0LTE2MTQtNDVkZC04NmMxLTVlYWZmZjA4NWQ4YSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im1lc3NhZ2VzLXdlYmFwcCIsInNlc3Npb25fc3RhdGUiOiIwN2FlY2JkMC1kYzdlLTQ4N2UtYTFlNS1lZTM2YjNmZGQ1ZDciLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1zaXZhbGFicyIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIwN2FlY2JkMC1kYzdlLTQ4N2UtYTFlNS1lZTM2YjNmZGQ1ZDciLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6IlNpdmEgS2F0YW1yZWRkeSIsInByZWZlcnJlZF91c2VybmFtZSI6InNpdmEiLCJnaXZlbl9uYW1lIjoiU2l2YSIsImZhbWlseV9uYW1lIjoiS2F0YW1yZWRkeSIsImVtYWlsIjoic2l2YUBnbWFpbC5jb20ifQ.W23gLsCkaWMi85BvvgzHhAdAEKMBPP6pzLhHnkKamzfG0Sw6XILPlXagrLSZBgzkaNSgt3ttZ6JLizh_oKuKmZBucAWtsBCo6ZIiPZDUDrEk95sDybhIEJZH-b1LSWMVLa2NXLE_FkEtTE44zCi2B2MxFQzfCuf7RJ5WnnW8XLAD6qbZVF64HT2wYNRiZiuq9MPwKPU-pOAGjGa53ko37EOgc0RjVE14D1--RFlGfLmGc4aGrl6OSElf1X8ya4wyDT8vP9pbWK4Fe-XLI9zOlYdY506TdOtAoe28Pu4GPhL5PrBQ1-2pY30_fN6BVgFk0qyff76yFyNNMFNNm6aA5w",
"expires_in": 299,
"refresh_expires_in": 1765,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N2E1NTU3Ni01MThlLTQ1MDItOWQyNi1jNzVmYjZhNGRhZWEifQ.eyJleHAiOjE2OTU1NDE2NDcsImlhdCI6MTY5NTUzOTg4MiwianRpIjoiOTZkYzFhYzYtMWI1ZC00MjA2LWFiM2EtOWQ4MDg2Y2ZmMjc1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MTkxL3JlYWxtcy9zaXZhbGFicyIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTE5MS9yZWFsbXMvc2l2YWxhYnMiLCJzdWIiOiJjYTFhMmYzNC0xNjE0LTQ1ZGQtODZjMS01ZWFmZmYwODVkOGEiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibWVzc2FnZXMtd2ViYXBwIiwic2Vzc2lvbl9zdGF0ZSI6IjA3YWVjYmQwLWRjN2UtNDg3ZS1hMWU1LWVlMzZiM2ZkZDVkNyIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjA3YWVjYmQwLWRjN2UtNDg3ZS1hMWU1LWVlMzZiM2ZkZDVkNyJ9.G5shFoEF1ptXUHM0FommjZ40fF1r61rd0fHU33R1CAc",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "07aecbd0-dc7e-487e-a1e5-ee36b3fdd5d7",
"scope": "email profile"
}
注意 :authorization_code
的有效期很短。因此,一旦获得 authorization_code
,就应迅速调用 Token 端点。否则,你将得到如下响应:
{
"error": "invalid_grant",
"error_description": "Code not valid"
}
最后,我们得到了 access_token
,用于访问资源服务器上受保护的资源。
使用 Postman {#使用-postman}
如你所见,获取 access_token
需要两个步骤,而在调用 token_endpoint
时,authorization_code
可能已经失效。
我们可以使用 Postman 将其简化如下:
- 在 Postman 中打开新请求选项卡
- 转到 Authorization 选项卡,Type 选择 OAuth 2.0
- 在 Configure New Token 部分:
- Grant Type:Authorization Code
- Callback URL :
http://localhost:8080/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-webapp
- Client Secret: qVcg0foCUNyYbgF0Sg52zeIhLYyOwXpQ
- Scope:profile
- State:randomstring
- Client Authentication:Send as Basic Auth header
- 点击 Get New Access Token 按钮
- Postman 会弹出 Keycloak 登录页面
- 使用用户凭证
siva/siva1234
登录 - 现在你应该可以看到带有 Token Details 的响应了
So easy!
获取 ID Token {#获取-id-token}
如果你有注意,在上面的请求中,我们指定 scope
为 "profile",并在响应中得到了 access_token
和 refresh_token
。这就是 OAuth 2.0 授权码授权方式。
现在,如果将 scope
指定为 "openid profile" ,那么你还将在响应中获得 id_token
,其中包含更多有关认证用户(即资源所有者)的信息。这就是基于 OpenID Connect 的授权码模式。
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeVVPTDg4LVBGM3BYQzFpN3BIeGdFZTJwaWZJY3RyTXJiNklHOElmRTlVIn0.eyJleHAiOjE2OTU1NDIyMTMsImlhdCI6MTY5NTU0MTkxMywiYXV0aF90aW1lIjoxNjk1NTQxMTg0LCJqdGkiOiIyZDM1NWFjNC0zOTRmLTQ3ZjktOTFiMC05Yjc3MjU0NDBjZDQiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkxOTEvcmVhbG1zL3NpdmFsYWJzIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImNhMWEyZjM0LTE2MTQtNDVkZC04NmMxLTVlYWZmZjA4NWQ4YSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im1lc3NhZ2VzLXdlYmFwcCIsInNlc3Npb25fc3RhdGUiOiJmNDJmMWU5NC0zMmFiLTRjYzktYTRiOS1lMGRmNTIwNWY1NTQiLCJhY3IiOiIwIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1zaXZhbGFicyIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwic2lkIjoiZjQyZjFlOTQtMzJhYi00Y2M5LWE0YjktZTBkZjUyMDVmNTU0IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJTaXZhIEthdGFtcmVkZHkiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzaXZhIiwiZ2l2ZW5fbmFtZSI6IlNpdmEiLCJmYW1pbHlfbmFtZSI6IkthdGFtcmVkZHkiLCJlbWFpbCI6InNpdmFAZ21haWwuY29tIn0.IQ52KDoyaGztq1hYqMjZici6eWxPOnLa3RSz7E23Cp1607wVwrCtWwYLAfwewzedrdkjYO9w58qEww-LvsWuzOr85i85JcHL7gQ7kYXS4kRvsKNUs-41wnJUEI6V4-9BduQdSarE43VMUNVS9ZUlNWCsAa7yVKOLdlLEbtM7bWWyGekojye6AQrB9enpKnRGcZaQWRhUZ6k07-d91QeyT2P7VRMit4gjaZmnaSlUKUvKzSYdmRhpB8vcNfggFTeNPgedYQOOwA-vbMWW0LQVElgtvi6eSUTksyFMwaP9iGdI0HyTW7IFi9ddFn3hq43GNahfrckoDeKXtkKQEPOgng",
"expires_in": 299,
"refresh_expires_in": 1765,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N2E1NTU3Ni01MThlLTQ1MDItOWQyNi1jNzVmYjZhNGRhZWEifQ.eyJleHAiOjE2OTU1NDM3MTIsImlhdCI6MTY5NTU0MTkxMywianRpIjoiYWQyNWQwN2UtZWY0OS00M2YxLWI1ZWUtOThlYzU2ZmZjYWY0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MTkxL3JlYWxtcy9zaXZhbGFicyIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTE5MS9yZWFsbXMvc2l2YWxhYnMiLCJzdWIiOiJjYTFhMmYzNC0xNjE0LTQ1ZGQtODZjMS01ZWFmZmYwODVkOGEiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibWVzc2FnZXMtd2ViYXBwIiwic2Vzc2lvbl9zdGF0ZSI6ImY0MmYxZTk0LTMyYWItNGNjOS1hNGI5LWUwZGY1MjA1ZjU1NCIsInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJmNDJmMWU5NC0zMmFiLTRjYzktYTRiOS1lMGRmNTIwNWY1NTQifQ.xClg0y5senUjiPIa_AzqDlSQ6M5isUBthzgYprfbNN8",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeVVPTDg4LVBGM3BYQzFpN3BIeGdFZTJwaWZJY3RyTXJiNklHOElmRTlVIn0.eyJleHAiOjE2OTU1NDIyMTMsImlhdCI6MTY5NTU0MTkxMywiYXV0aF90aW1lIjoxNjk1NTQxMTg0LCJqdGkiOiJjYzkyOGVkOC1kMGEzLTQ0YjUtOTEyMS1mOTYwOTc0YzM3ODciLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkxOTEvcmVhbG1zL3NpdmFsYWJzIiwiYXVkIjoibWVzc2FnZXMtd2ViYXBwIiwic3ViIjoiY2ExYTJmMzQtMTYxNC00NWRkLTg2YzEtNWVhZmZmMDg1ZDhhIiwidHlwIjoiSUQiLCJhenAiOiJtZXNzYWdlcy13ZWJhcHAiLCJzZXNzaW9uX3N0YXRlIjoiZjQyZjFlOTQtMzJhYi00Y2M5LWE0YjktZTBkZjUyMDVmNTU0IiwiYXRfaGFzaCI6IkF0R1B6RXdnR3lfejB5TDhWcXc0QmciLCJhY3IiOiIwIiwic2lkIjoiZjQyZjFlOTQtMzJhYi00Y2M5LWE0YjktZTBkZjUyMDVmNTU0IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJTaXZhIEthdGFtcmVkZHkiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzaXZhIiwiZ2l2ZW5fbmFtZSI6IlNpdmEiLCJmYW1pbHlfbmFtZSI6IkthdGFtcmVkZHkiLCJlbWFpbCI6InNpdmFAZ21haWwuY29tIn0.f6kIyFDu9kaMBufQWgtta-ZnEDGUNWDa9YVmrsuZDVCWy4roV-dd4Bj41Ncg1bYquHmCTgjk4u2FT7tYxOu4aj5aZS9xGvBDn3zUMOehxY4VxLa-3YSZ-DLsEburxGWFZmrDXbGGP59o7fgLYxsDEsXxuq7LVwzOPYgtNsnleihdADSPNJP9wQ-4_ozWUx1rXEkcJo93S9w6BIiGeKBZBXHMCD8wjxGJPLJh078UumqgypZiEBsmlJRMDQE23k8s2K1Txaru7zDbw1mAu43-lki-sTJgqXnOG8kg3tF5lFK0fLTlG9cB5i8oXZ2zJ3mUbTDx6gZCjfA85Oc_q_vUdw",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "f42f1e94-32ab-4cc9-a4b9-e0df5205f554",
"scope": "openid email profile"
}
总结 {#总结}
在本文中,我们学习了如何通过 "授权码模式" 获取 access_token
和 id_token
。我们还学习了如何使用 Postman 轻松执行该流程。
在下一教程中,我们将了解 "OAuth 2.0 客户端凭证模式" 的工作原理。
参考:https://www.sivalabs.in/spring-security-oauth2-tutorial-authorization-code-flow/