通过 api 和 keycloak 理解OIDC认证

参考资料

  • 通过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 [email protected]: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

整体的认证流程如下:

通过 api 和 keycloak 理解OIDC认证_第1张图片

  1. 客户端呼叫登录,oidc provider检查登录状态
  2. 未登录则要求客户登录并发送authorization code到回调地址
  3. 已登陆则直接跳转到回调地址(携带authorization code
  4. authorization code转换为access_token
  5. 使用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>firstApptitle>
  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">logina>
  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>secondApptitle>
  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">logina>
  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的授权环节,发现没有登录等待输入用户名密码(默认放行任意用户名密码)

通过 api 和 keycloak 理解OIDC认证_第2张图片

确权页面,显示应用需要获取那些用户权限

通过 api 和 keycloak 理解OIDC认证_第3张图片

点击continue回到最初的入口,但是url已经发生变化。下面的MDbf7u7HPwQEGFCzWkR-YAc3rnJ78ZE2Nq-oOmE_3Qnauthorization 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信息

示意图如下

通过 api 和 keycloak 理解OIDC认证_第4张图片

使用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

通过 api 和 keycloak 理解OIDC认证_第5张图片

创建用户并绑定role

通过 api 和 keycloak 理解OIDC认证_第6张图片

仿照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,
    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配置文件

通过 api 和 keycloak 理解OIDC认证_第7张图片

添加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中授权

通过 api 和 keycloak 理解OIDC认证_第8张图片

你可能感兴趣的:(AWS,系统运维,web安全,云计算)