参考资料
-
通过Keycloak API理解OAuth2与OpenID Connect
-
什么是keycloak如何在nodejs中使用它
-
如何通过 OIDC 协议实现单点登录?
-
https://jwt.io/#encoded-jwt
OIDC认证的简单demo
单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一,在多个应用系统中,用户只需要登录一就可访问所有相互信任的应用系统。而OIDC (OpenID Connect)是一个基于 OAuth 2.0 的轻量级认证 + 授权协议,是 OAuth 2.0 的超集。OIDC Provider即身份提供商,常见的微信扫码登陆场景,微信就是OIDC Provider。我们使用**node-oidc-provider**(最新版本仅支持18LTSnodejs)创建一个自己的oidc provider。
git clone git@github.com:panva/node-oidc-provider.git
cd node-oidc-provider
npm install
将我们的应用程序集成到oidc provider的方式是向其申请一个client,这里直接在配置文件中添加client
# ./example/support/configuration.js
{
client_id: '1',
client_secret: '1',
grant_types: ['refresh_token', 'authorization_code'],
redirect_uris: ['http://localhost:8080/app1.html', 'http://localhost:8080/app2.html'],
}
启动oidc provider
cd ./example
node express.js
整体的认证流程如下:
- 客户端呼叫登录,
oidc provider
检查登录状态 - 未登录则要求客户登录并发送
authorization code
到回调地址 - 已登陆则直接跳转到回调地址(携带
authorization code
) - 将
authorization code
转换为access_token
- 使用
access_token
获取用户信息
创建应用程序
$ mkdir app
$ vim app/app1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>firstApp</title>
</head>
<body>
<a href="http://localhost:3000/auth?client_id=1&redirect_uri=http://localhost:8080/app1.html&scope=openid profile&response_type=code&state=455356436">login</a>
</body>
</html>
$ vim app/app2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>secondApp</title>
</head>
<body>
<a href="http://localhost:3000/auth?client_id=1&redirect_uri=http://localhost:8080/app2.html&scope=openid profile&response_type=code&state=455356436">login</a>
</body>
</html>
启动服务器托管客户端应用
$ cd app
$ http-server .
一直报错Authorization Server policy requires PKCE to be used for this request
,最后发现是默认开启了pkce
https://github.com/panva/node-oidc-provider/blob/v7.x/docs/README.md#pkcerequired
略坑,不知道这个功能是什么用,关闭之后正常
function pkceRequired(ctx, client) { return false; }
访问app1的登录链接,跳转到oidc provider的授权环节,发现没有登录等待输入用户名密码(默认放行任意用户名密码)
确权页面,显示应用需要获取那些用户权限
点击continue回到最初的入口,但是url已经发生变化。下面的MDbf7u7HPwQEGFCzWkR-YAc3rnJ78ZE2Nq-oOmE_3Qn
即authorization code
http://localhost:8080/app1.html?code=MDbf7u7HPwQEGFCzWkR-YAc3rnJ78ZE2Nq-oOmE_3Qn&state=455356436
通过authorization code
获取access_token
,之后应用程序即通过access_token
访问oidc provider上的资源
curl --location --request POST 'http://localhost:3000/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=1' \
--data-urlencode 'client_secret=1' \
--data-urlencode 'redirect_uri=http://localhost:8080/app1.html' \
--data-urlencode 'code=MDbf7u7HPwQEGFCzWkR-YAc3rnJ78ZE2Nq-oOmE_3Qn' \
--data-urlencode 'grant_type=authorization_code'
{
"access_token": "bhDGXk9yR0dJj-o_2X-YmTZqg4Lotn_UBTOmgGudPg4",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQyODE3S3gwdmJpNmlfS2MifQ.eyJzdWIiOiJxd2VyIiwiYXRfaGFzaCI6IjBVbDB3b1N6dFN3azV1REdMa0xkSmciLCJhdWQiOiIxIiwiZXhwIjoxNjcyMDc3MjgyLCJpYXQiOjE2NzIwNzM2ODIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJ9.ZrxZMKxauh1qysC_jC58mz3JAPbBpzQUabreGydYBpgG9WQ65Ca_Ch85kwFpWWhLCMBl9T_cKtiRRMBtdkaWsElKlYtZ4z6p0LlmVmVOBlY2TkBh4xxg0rcLBPcyuN0ATGrMOwwhMnlCV0RRzY3lcfh5Dbd1oj_OOWlqqqXxOT1F4M9F_kCeRzLQBwCGScwDEzVLZsTEapOrZFGXpH16Jnf8C1nqs2s7WJbepE9RZwOxpjlNjTTfCi755gBWLIU5-pHBFeqNxa_CdAfQ1OYBPX9MGGdAYIwL5TPqvR3F000kgCKx7YaSLnpaczjlROQzmst4-FlbUhEip_Hd3uBFhw",
"scope": "openid profile",
"token_type": "Bearer"
}
通过access_token
获取用户数据
curl --location --request POST 'http://localhost:3000/me' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'access_token=bhDGXk9yR0dJj-o_2X-YmTZqg4Lotn_UBTOmgGudPg4'
{
"sub": "qwer",
"birthdate": "1987-10-16",
"family_name": "Doe",
"gender": "male",
"given_name": "John",
"locale": "en-US",
"middle_name": "Middle",
"name": "John Doe",
"nickname": "Johny",
"picture": "http://lorempixel.com/400/200/",
"preferred_username": "johnny",
"profile": "https://johnswebsite.com",
"updated_at": 1454704946,
"website": "http://example.com",
"zoneinfo": "Europe/Berlin"
}
OIDC核心概念和逻辑
我们通过上面的应用完成了简单的OIDC认证过程,下面的部分完整的对以上逻辑进行梳理
OIDC认证涉及的主要概念
- Resource Owner - 用户
- Resource Server - 服务器资源
- Client - 用户前端程序
- Authorization Server - 对用户认证并发送access_token
OIDC涉及的核心数据,这些关键数据可以和上面的demo相互印证
- User Credential - 用户凭据(用户名和密码)
- Client ID - client的唯一标识
- Client Secret - Authroziation Server验证Client身份的标识
- Authorization Code - 授权码,通过 User Credential + Client ID换取Authorization Code。授权码不能泄漏,且一次性有效。
- Access Token - 访问令牌,拥有令牌者可以访问受保护的资源。通过 Authorization Code + Client ID + Client Secret 换取。Access Token在有效期内有效。
- Refresh Token - 刷新令牌,重新获取(刷新)Access Token和Refresh Token。Refresh Token在有效期内有效。
- ID Token - 包括会话认证的JWT,包括用户标识,identity provider,client信息
示意图如下
使用Keycloak完成oidc认证
创建keycloak服务的过程之前的文章已经讲过,略去不表
常用的Keycloak endpoints如下:
- authorization_endpoint - 获取Authorization code
- token_endpoint - 获取Access Token
- introspection_endpointt - Token内省,可验证Token和获取Token的元信息。
- userinfo_endpoint - 获取用户信息
- end_session_endpoint - 用户注销
$ curl http://127.0.0.1:8090/realms/myoidc/.well-known/openid-configuration
{
"issuer": "http://127.0.0.1:8090/realms/myoidc",
"authorization_endpoint": "http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/auth",
"token_endpoint": "http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/token",
"introspection_endpoint": "http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/userinfo",
"end_session_endpoint": "http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/logout",
"jwks_uri": "http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/certs",
}
向keycloak注册client
创建用户并绑定role
仿照demo中操作访问keycloak认证页面,输入用户名和密码跳转到回调链接(回调链接可以随便写)
curl http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/auth?client_id=testapp&redirect_uri=http://127.0.0.1:8099/&response_type=code&scope=openid
跳转到回调连接后url发生变化,获取到授权码(code后的内容)
http://127.0.0.1:8099/?session_state=dbf528e5-beea-4815-848e-7e7651216aac&code=ca78ca6c-b79c-4f09-a360-9ffd7574da6d.dbf528e5-beea-4815-848e-7e7651216aac.da5886f1-238b-4e79-8e12-0e33e601fdea
通过curl命令,使用授权码获取access_token
curl --location --request POST 'http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'code=ca78ca6c-b79c-4f09-a360-9ffd7574da6d.dbf528e5-beea-4815-848e-7e7651216aac.da5886f1-238b-4e79-8e12-0e33e601fdea' \
--data-urlencode 'client_id=testapp' \
--data-urlencode 'client_secret=V1FTVZMIUAVx52n7VM5ndrCLqwj0pGTF' \
--data-urlencode 'redirect_uri=http://127.0.0.1:8099/' \
--data-urlencode 'grant_type=authorization_code'
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJNEQyVGp6WUZvSWRGeTlIRGE1aFk2a2tjclRmZUNodmhLODZuN2JXTWJRIn0.eyJleHAiOjE2NzIwNzY5MjYsImlhdCI6MTY3MjA3NjYyNiwiYXV0aF90aW1lIjoxNjcyMDc1ODY5LCJqdGkiOiI2ZDI0ZGQ0My1iZGJlLTQwNWUtYmNiNC0xOWM4NTU1NGUxNWQiLCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwOTAvcmVhbG1zL215b2lkYyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIzZDQ2NGM4NC1jZDEzLTQzY2ItODFiNi0yYTZlNmViOTY0ZTUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0ZXN0YXBwIiwic2Vzc2lvbl9zdGF0ZSI6ImRiZjUyOGU1LWJlZWEtNDgxNS04NDhlLTdlNzY1MTIxNmFhYyIsImFjciI6IjAiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsidHVzZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1teW9pZGMiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwic2lkIjoiZGJmNTI4ZTUtYmVlYS00ODE1LTg0OGUtN2U3NjUxMjE2YWFjIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0dXNlciIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiJ9.SBHEfhRg2tHCYhE1HzJcQ03B2MNyKiHmxdAr9VHl7wEalY2kYdy_CIZotcpOLQJ7oj13dliIspX1sHY0XAyqHYOYek9a1G-tmhElrkzkST0DttqwaWaFzgHdpOfF_TMPRAjqeDe24g_6T7g7749QmR8ChhtN4c77xNhOSpSIVOIIRqEu6BD9y-9ccxENCs4rG8Ww5f7WQuVHF5I5cQe8qoYf15ne3fr8W2IvtXSeec09c5DT1RlUZ04RITX6GL81PI74qkQSokdFQLkwhdQ0Toxu2_odwZgvjVbYOObl2j1cpvKCSsUUNnaxuAa0C29p4iX7xk-PdQjBitQzeOdVGg",
"expires_in": 299,
"refresh_expires_in": 1799,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxZjI0YjA5OS1kOThkLTQxMzAtODM5ZC1kNTE0YWU1NjZiYWQifQ.eyJleHAiOjE2NzIwNzg0MjYsImlhdCI6MTY3MjA3NjYyNiwianRpIjoiNTRhMWEwZDUtZmQ0OC00MTI5LThhYzUtOWQ4NDY5ZGRjMTU3IiwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDkwL3JlYWxtcy9teW9pZGMiLCJhdWQiOiJodHRwOi8vMTI3LjAuMC4xOjgwOTAvcmVhbG1zL215b2lkYyIsInN1YiI6IjNkNDY0Yzg0LWNkMTMtNDNjYi04MWI2LTJhNmU2ZWI5NjRlNSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJ0ZXN0YXBwIiwic2Vzc2lvbl9zdGF0ZSI6ImRiZjUyOGU1LWJlZWEtNDgxNS04NDhlLTdlNzY1MTIxNmFhYyIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiJkYmY1MjhlNS1iZWVhLTQ4MTUtODQ4ZS03ZTc2NTEyMTZhYWMifQ.bY5ijVxi9TO8tLp6Gk3ZpvZzaTcHex6hj7Oh-OtqUBg",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJNEQyVGp6WUZvSWRGeTlIRGE1aFk2a2tjclRmZUNodmhLODZuN2JXTWJRIn0.eyJleHAiOjE2NzIwNzY5MjYsImlhdCI6MTY3MjA3NjYyNiwiYXV0aF90aW1lIjoxNjcyMDc1ODY5LCJqdGkiOiJjMmFkYTM4NC1iNTdmLTRjNWQtYTUyZS1kZmQ4NWMyM2JjYjUiLCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwOTAvcmVhbG1zL215b2lkYyIsImF1ZCI6InRlc3RhcHAiLCJzdWIiOiIzZDQ2NGM4NC1jZDEzLTQzY2ItODFiNi0yYTZlNmViOTY0ZTUiLCJ0eXAiOiJJRCIsImF6cCI6InRlc3RhcHAiLCJzZXNzaW9uX3N0YXRlIjoiZGJmNTI4ZTUtYmVlYS00ODE1LTg0OGUtN2U3NjUxMjE2YWFjIiwiYXRfaGFzaCI6ImF6V3NqR3FDMEFwajRRbGx6XzMtM1EiLCJhY3IiOiIwIiwic2lkIjoiZGJmNTI4ZTUtYmVlYS00ODE1LTg0OGUtN2U3NjUxMjE2YWFjIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0dXNlciIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiJ9.KeOK3Fa3gZHtOYe-j1YcG9fZ5DV09WGnm-118lWfrHqjAc3ZWKvguQqfGVQHjXum9obPQvu0-luUYvoFF4LghEbteFie9R83QvgW4palPOVFig2zTbSrP2eeXTk39hOiUG8RYbmYRygWSTA5poht3r77fuPiC5HohyqnRBLKmzRSqZQC6P_7bjbO1LOq8JB7NpCVKpJwZtSRkka5d3CHkGuLZiQuRh_Wo7B66bXJgDJ4b_okVaqR_M4m31WLpeEF9mkAyVQa5lVt1-keNj6y05cjiRuZewOXAomuDysJpyZEhZFREkz5ByuFHGn40D6mWWOriRq5QhorQTb81E1l4A",
"not-before-policy": 0,
"session_state": "dbf528e5-beea-4815-848e-7e7651216aac",
"scope": "openid profile email"
}
还可以根据用户名和密码,同样能够获取access_token
curl --location --request POST 'http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=testapp' \
--data-urlencode 'client_secret=V1FTVZMIUAVx52n7VM5ndrCLqwj0pGTF' \
--data-urlencode 'username=tuser' \
--data-urlencode 'password=passwd' \
--data-urlencode 'grant_type=password'
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJNEQyVGp6WUZvSWRGeTlIRGE1aFk2a2tjclRmZUNodmhLODZuN2JXTWJRIn0.eyJleHAiOjE2NzIwNzcwODUsImlhdCI6MTY3MjA3Njc4NSwianRpIjoiY2YxNDczMjgtYjlhZC00MTgzLWFhNTMtYWJhMGE2MDRlNDllIiwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDkwL3JlYWxtcy9teW9pZGMiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiM2Q0NjRjODQtY2QxMy00M2NiLTgxYjYtMmE2ZTZlYjk2NGU1IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdGFwcCIsInNlc3Npb25fc3RhdGUiOiJhMzY2ZGQ0MS02MGIzLTQ2YTYtOWI3Ni1lZTk0OWFkMmYwZWQiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInR1c2VyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtbXlvaWRjIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiYTM2NmRkNDEtNjBiMy00NmE2LTliNzYtZWU5NDlhZDJmMGVkIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0dXNlciIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiJ9.Tquqv50danvZWx3ZVln4PF7SPuO0cd1oiUn8bSY_UIWo7CeuiUkiUq0jdlUrJ2Y79hSHpGwJ5HBddhUOvlLJc-XkmED8iqvRPTROyLg7FzO_Y-QRVp29xjSf1S45x28q4-xiJN8zdAqnFCmBe5gjXhsDG7zcZGd9Gf1eqstkSzUM1CeYLLW0pjO6lLtwFsLArmB7G0LtB4xA2RHpfZZeSmeroHU6Ijbex2MF6oXhdtZSW072ZwJNSbAODB2VSWskMP5Be15grx40mY80e9ujYliWTbtGTm7qHueYOn99xh8-cLK04JcoZSyH-EtIe2pj-mi7ljpdSxiPGUCphYT-5A",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxZjI0YjA5OS1kOThkLTQxMzAtODM5ZC1kNTE0YWU1NjZiYWQifQ.eyJleHAiOjE2NzIwNzg1ODUsImlhdCI6MTY3MjA3Njc4NSwianRpIjoiOTVkMmVmZWItMGFhNi00ODk2LWFjZjEtMzU5ZGMxNjlkNjk2IiwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDkwL3JlYWxtcy9teW9pZGMiLCJhdWQiOiJodHRwOi8vMTI3LjAuMC4xOjgwOTAvcmVhbG1zL215b2lkYyIsInN1YiI6IjNkNDY0Yzg0LWNkMTMtNDNjYi04MWI2LTJhNmU2ZWI5NjRlNSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJ0ZXN0YXBwIiwic2Vzc2lvbl9zdGF0ZSI6ImEzNjZkZDQxLTYwYjMtNDZhNi05Yjc2LWVlOTQ5YWQyZjBlZCIsInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6ImEzNjZkZDQxLTYwYjMtNDZhNi05Yjc2LWVlOTQ5YWQyZjBlZCJ9.u72Fw2Us0YldlRfLXRRdy3PehtqKrrV6ciIy29gwy60",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "a366dd41-60b3-46a6-9b76-ee949ad2f0ed",
"scope": "profile email"
}
根据access_token
获取用户信息
curl --location --request GET 'http://127.0.0.1:8090/realms/myoidc/protocol/openid-connect/userinfo' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Bearer xxxxxxxxxxxxInR5cCIgOiAiSldUIiwia2lkIiA6ICJJNEQyVGp6WUZvSWRGeTlIRGE1aFk2a2tjclRmZUNodmhLODZuN2JXTWJRIn0.eyJleHAiOjE2NzIwNzczOTksImlhdCI6MTY3MjA3NzA5OSwiYXV0aF90aW1lIjoxNjcyMDc1ODY5LCJqdGkiOiJmNzQ5NDU5Ny05NTg4LTQ0MDYtYTAzMC0yNzIxNzY3ZDY2YTciLCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwOTAvcmVhbG1zL215b2lkYyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIzZDQ2NGM4NC1jZDEzLTQzY2ItODFiNi0yYTZlNmViOTY0ZTUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0ZXN0YXBwIiwic2Vzc2lvbl9zdGF0ZSI6ImRiZjUyOGU1LWJlZWEtNDgxNS04NDhlLTdlNzY1MTIxNmFhYyIsImFjciI6IjAiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsidHVzZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1teW9pZGMiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwic2lkIjoiZGJmNTI4ZTUtYmVlYS00ODE1LTg0OGUtN2U3NjUxMjE2YWFjIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0dXNlciIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiJ9.TpHQBKxhVk1kPslB5uji8nJj3XeYMme3ocQkP3jhu37WoJOYGpAhHeT8PB4pEgPIju4PX_fBvmwU-cAWxmLeCR5S6W1p1LROTjx714b2F3NITLIufyEJXemkamabXbIkRZRsqjKGxPfXuwh6KPDfh3Lx7knbtRpNmXU0bbyx2ufb3T-qqRVoszJxYs1JeKPTw2ao1rpNs11vdQaHR-PkRkHR39PVBEICkIaTGKN2-eTOpbBa4FXFL_fBFIcIHfiAaclCqihZBjOkQpDgOIYTZ76yxl_oDYIB8GEQHpjcFeuf-IbsUcbKNaqDcprqE8EZTIAQt1ynV0Cisf9ZUgdIfg'
{
"sub": "3d464c84-cd13-43cb-81b6-2a6e6eb964e5",
"email_verified": false,
"preferred_username": "tuser",
"given_name": "",
"family_name": ""
}
由此可见使用keycloak进行oidc认证的思路和demo中是一致的,甚至连请求方式都一样
nodejs应用集成keycloak
最后我们尝试将nodejs应用与keycloak进行集成
创建主函数,使用中间件集成keycloak
const path = require('path');
const express = require('express');
const session = require('express-session');
const favicon = require('serve-favicon');
const Keycloak = require('keycloak-connect');
const app = express();
const memoryStore = new session.MemoryStore();
app.set('view engine', 'ejs');
app.set('views', require('path').join(__dirname, '/view'));
app.use(express.static('static'));
app.use(favicon(path.join(__dirname, 'static', 'images', 'favicon.ico')));
app.use(session({
secret: 'KWhjV<T=-*VW<;cC5Y6U-{F.ppK+])Ub',
resave: false,
saveUninitialized: true,
store: memoryStore,
}));
const keycloak = new Keycloak({
store: memoryStore,
});
app.use(keycloak.middleware({
logout: '/logout',
admin: '/',
}));
app.get('/', (req, res) => res.redirect('/home'));
const parseToken = raw => {
if (!raw || typeof raw !== 'string') return null;
try {
raw = JSON.parse(raw);
const token = raw.id_token ? raw.id_token : raw.access_token;
const content = token.split('.')[1];
return JSON.parse(Buffer.from(content, 'base64').toString('utf-8'));
} catch (e) {
console.error('Error while parsing token: ', e);
}
};
app.get('/home', keycloak.protect(), (req, res, next) => {
const details = parseToken(req.session['keycloak-token']);
const embedded_params = {};
if (details) {
embedded_params.name = details.name;
embedded_params.email = details.email;
embedded_params.username = details.preferred_username;
}
res.render('home', {
user: embedded_params,
});
});
app.get('/login', keycloak.protect(), (req, res) => {
return res.redirect('home');
});
app.get('/asset01', keycloak.enforcer(['asset-01:read'], {
resource_server_id: 'my-application'
}), (req, res) => {
return res.status(200).end('success');
});
app.get('/asset01/update', keycloak.enforcer(['asset-01:write'], {
resource_server_id: 'my-application'
}), (req, res) => {
return res.status(200).end('success');
});
app.use((req, res, next) => {
return res.status(404).end('Not Found');
});
app.use((err, req, res, next) => {
return res.status(req.errorCode ? req.errorCode : 500).end(req.error ? req.error.toString() : 'Internal Server Error');
});
const server = app.listen(3000, '127.0.0.1', () => {
const host = server.address().address;
const port = server.address().port;
console.log('Application running at http://%s:%s', host, port);
});
导出keycloak配置文件
添加keycloak.json
{
"realm": "myoidc",
"auth-server-url": "http://127.0.0.1:8090/",
"ssl-required": "external",
"resource": "testapp",
"credentials": {
"secret": "V1FTVZMIUAVx52n7VM5ndrCLqwj0pGTF"
},
"confidential-port": 0
}
启动server
node index.js
访问127.0.0.1:3000然后自动跳转到keycloak登录界面,输入keycloak的用户名密码登录之后,由于没有权限会直接Access denied
,需要在keycloak中授权