Keycloak是一款主流的IAM(Identity and Access Management 的缩写,即“身份识别与访问管理”)开源实现,它具有单点登录、强大的认证管理、基于策略的集中式授权和审计、动态授权、企业可管理性等功能。
结合我们项目内的过往使用经验,本篇文章,将介绍:
1、一键式Keycloak安装;
2、配置Grafana,使接入Keycloak进行单点登录;
3、分析Grafana SSO登录过程,从而帮助用户更理解OAuth2的交互过程;
下图就是最终的过程图示:
1、一键式Keycloak安装
基于项目需要,我们在使用Keycloak时,需要外接企业微信的认证方式,鉴于Keycloak已有的Social Login并不支持企业微信,我们对此作出了扩展, https://github.com/qugeppl/keycloak-social-wecom ,并基于官方的12.0.4镜像做了无感扩展,可直接以容器方式启动:
代码语言:javascript复制docker run -it --name keycloak-wecom -p 80:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin qugeppl/keycloak-social-wecom:12.0.4.1
2、配置Keycloak
2.1 Grafana的OAuth接入
这里我们重点以Grafana的通用OAuth2接入能力https://grafana.com/docs/grafana/latest/auth/generic-oauth/
可以看到,对Grafana来说,天生支持了OAuth协议的sso过程,只需要添加一些配置(其他非必填,我们先只关注基础的部分)
代码语言:javascript复制[auth.generic_oauth]
enabled = true
client_id = YOUR_APP_CLIENT_ID
client_secret = YOUR_APP_CLIENT_SECRET
scopes =
empty_scopes = false
auth_url =
token_url =
api_url =
root_url =
Grafana或者其他任意需要SSO登录的组件,其实都是Keycloak(或者其他IAM)里的一个Client,所以需要去Keycloak创建出这个client,并且拿到对应的信息;
2.2 Keycloak配置
- 访问
上述步骤安装后的Keycloak ip,例如可以是http://localhost, 会自动打开如下页面,从管理员界面进行登录,账号密码如步骤1中的环境变量所设置 admin/admin
2. 创建新域Test (Master是顶级域,一般不建议使用)
3.在Test域创建Client,命名为grafana
补充其他必须信息,保存
拿到client secret
2.3 填充Grafana配置信息
几个url信息从哪里得到?参照Keycloak官方文档的指引, https://www.keycloak.org/docs/latest/server_admin/index.html
发现有endpoint现成提供
代码语言:javascript复制<root>/auth/realms/{realm-name}/.well-known/openid-configuration
like:
http://localhost/auth/realms/Test/.well-known/openid-configuration
访问得到的结果在这里啦
代码语言:javascript复制{
"issuer":"http://local/auth/realms/Test",
"authorization_endpoint":"http://localhost/auth/realms/Test/protocol/openid-connect/auth",
"token_endpoint":"http://localhost/auth/realms/Test/protocol/openid-connect/token",
"introspection_endpoint":"http://localhost/auth/realms/Test/protocol/openid-connect/token/introspect",
"userinfo_endpoint":"http://localhost/auth/realms/Test/protocol/openid-connect/userinfo",
"end_session_endpoint":"http://localhost/auth/realms/Test/protocol/openid-connect/logout",
"jwks_uri":"http://localhost/auth/realms/Test/protocol/openid-connect/certs",
"check_session_iframe":"http://localhost/auth/realms/Test/protocol/openid-connect/login-status-iframe.html",
"grant_types_supported":[
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials"
],
...省略了其他无关信息
}
所以,这里基本的配置都有了
代码语言:javascript复制[auth.generic_oauth]
enabled = true
name = Test
allow_sign_up = true
client_id = grafana
client_secret = ****** #这个在keycloak->client id=grafana->credential找到
scopes = openid email #这里要注意,openid这个是一定要有的,email的话,表示登录成功后,可以授权拿到用户的email信息
auth_url = http://localhost/auth/realms/Test/protocol/openid-connect/auth #第一步登录
token_url = http://localhost/auth/realms/Test/protocol/openid-connect/token #第二步获取token
api_url = http://localhost/auth/realms/Test/protocol/openid-connect/userinfo #第三部获取user信息
root_url = http://grafanaip:port #这里是grafana的地址一定要注意,grafana 认证成功并且存入grafana session后,会将用户redirect这个地址,就是grafana首页
role_attribute_path = role #这里是读取用户Atrribute里的某个字段来解析用户登录到Grafana的角色
上面的role_attribute_path 可以更近一步在官方文档中找到说明:
https://grafana.com/docs/grafana/latest/auth/generic-oauth/#role-mapping
对应的keycloak里grafana client需要配置在userinfo时候,返回用户的role属性
用户的属性信息里role是哪里来的呢,其实可以配置给用户登录的时使用的IDP Mapper,如下图的意思是,用户登录后,自动给用户匹配被硬编码的“Admin”字符串
当然既然都是管理员,那可不可以不这么麻烦,直接在grafana配置不就好了么,于是参照grafana文档及测试,得到了如下配置,节省了keycloak的用户角色配置
代码语言:javascript复制role_attribute_path ='True'&&'Admin'
3. Grafana SSO登录过程分析
按照上述的步骤,你的grafana配置好后,已经能够使用keycloak进行登录了(需要在Keycloak创建用户):
当然你需要在Keycloak的Test域下创建一个用户,或者直接使用企业微信扫码登录(这里需要企业应用的一些信息)
一般情况下,第三方软件以oauth2接入keycloak时,都需要提供三个url
代码语言:javascript复制auth_url = http://localhost/auth/realms/PulseLine/protocol/openid-connect/auth #第一步登录
token_url = http://localhost/auth/realms/PulseLine/protocol/openid-connect/token #第二步获取token
api_url = http://localhost/auth/realms/PulseLine/protocol/openid-connect/userinfo #第三部获取user信息
(第三方软件外部用)auth_url:浏览器访问时,第三方软件(Grafana)靠此url使用户浏览器转向Keycloak的认证登录界面,用户在Keycloak登录成功后,keycloak会生成一个针对该用户的code(这个code只能使用一次,用于换取token),并返回给浏览器,并指定下一跳的url.(这个url是用户指定的第三方软件中能够处理code的url,即callback)
(第三方软件内部用)token_url:浏览器按照上一步第三方软件转向地址,携带code进行访问,第三方软件内部收到请求后使用token_url以及code,和keycloak通信交换出这个用户的access_token。
(第三方软件内部用)api_url:第三方软件内部使用api_url,以及上一步的access_token与keycloak通信,获取这个用户的详细信息后,内部注册用户并存储session,最后将浏览器重定向到第三方软件首页(因为已经存储了第三方软件的session,所以直接保持登录态)
第一个请求authenticate,请求keycloak登录授权
代码语言:javascript复制http://localhost/auth/realms/Test/login-actions/authenticate?session_code=TqvBA9opgD0HDGZIgKIo3bRHa6k9kCmB_pBu1ISuOGE&execution=3b27c26c-883e-47f1-b999-c6f003f7f4ec&client_id=grafana&tab_id=FymTJ3TfBXA
#此时用户输入账号密码,点击登录,认证成功后
#该请求的responseHeader看出,keycloak登录成功,客户端可以转向grafana了,并给予了keycloak的code
http://localhost:3000/login/generic_oauth?state=dFgc4PlQAA_7f8b6D6g-N4lWn8fWI-EnEVpcy1sJ-o8=&session_state=c78927fc-37bd-4d00-8cc8-c23c8f5d1989&code=9d4a337c-83f2-4940-a079-4185020c883a.c78927fc-37bd-4d00-8cc8-c23c8f5d1989.27cb44d1-7d6b-46a9-966b-cdaa49a7f9f5
第二个请求则为第一个请求中keycloak让客户端登录keycloak登录成功后转向
代码语言:javascript复制http://localhost:3000/login/generic_oauth?state=dFgc4PlQAA_7f8b6D6g-N4lWn8fWI-EnEVpcy1sJ-o8=&session_state=c78927fc-37bd-4d00-8cc8-c23c8f5d1989&code=9d4a337c-83f2-4940-a079-4185020c883a.c78927fc-37bd-4d00-8cc8-c23c8f5d1989.27cb44d1-7d6b-46a9-966b-cdaa49a7f9f5
#这里grafana会拿到code,并和keycloak通信,用code换回accesstoken
#有了accesstoken后,遂向keycloak,发起api_url的请求,获取用户身份
#此时存入自己管理的用户session
#然后返回
#response header里,设置浏览器的grafana cookie,说明grafana已经将keycloak的code验证登录后,生成了该客户端的grafana_session
Set-Cookie: oauth_state=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax
Set-Cookie: grafana_session=d08672d2f9a27fa5d760d8a44bb8fc73; Path=/; Max-Age=2595600; HttpOnly; SameSite=Lax
Set-Cookie: redirect_to=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax
Location: /
第三个请求为第二个请求的客户端强制转向,即Grafana里配置的root_url
代码语言:javascript复制http://localhost:3000/
#请求头
Cookie: grafana_session=d08672d2f9a27fa5d760d8a44bb8fc73