每日一博 | 如何通过 OIDC 协议实现单点登录?
什么是单点登录
我们通过一个例子来说明,假设有一所大学,内部有两个系统,一个是邮箱系统,一个是课表查询系统。现在想实现这样的效果:在邮箱系统中登录一遍,然后此时进入课表系统的网站,无需再次登录,课表网站系统直接跳转到个人课表页面,反之亦然。比较专业的定义如下:
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
为什么要实现单点登录
单点登录的意义在于能够在不同的系统中统一账号、统一登录。用户不必在每个系统中都进行注册、登录,只需要使用一个统一的账号,登录一次,就可以访问所有系统。
通过 OIDC 协议实现单点登录
创建自己的用户目录
用户目录这个词很贴切,你的系统的总用户表就像一本书一样,书的封皮上写着“所有用户”四个字。打开第一页,就是目录,里面列满了用户的名字,翻到对应的页码就能看到这个人的邮箱,手机号,生日信息等等。无论你开发多少个应用,要确保你有一份这些应用所有用户信息的 truth source。所有的注册、认证、注销都要到你的用户目录中进行增加、查询、删除操作。你要做的就是创建一个中央数据表,专门用于存储用户信息,不论这个用户是来自 A 应用、B 应用还是 C 应用。
什么是 OIDC 协议
OIDC 的全称是 OpenID Connect,是一个基于 OAuth 2.0 的轻量级认证 + 授权协议,是 OAuth 2.0 的超集。它规定了其他应用,例如你开发的应用 A(XX 邮件系统),应用 B(XX 聊天系统),应用 C(XX 文档系统),如何到你的中央数据表中取出用户数据,约定了交互方式、安全规范等,确保了你的用户能够在访问所有应用时,只需登录一遍,而不是反反复复地输入密码,而且遵循这些规范,你的用户认证环节会很安全。
架设自己的 OIDC Provider
什么是 OIDC Provider 呢?我来举一个例子:你经常见到一些网站的登录页面上有「使用 Github 登录」、「使用 Google 登录」这样的按钮。要想集成这样的功能,你要先去 Github 那里注册一个 OAuth App,填写一些资料,然后 Github 分配给你一对 id 和 key。 此时 Github 扮演的角色就是 OIDC Provider,你要做的就是把 Github 的这种角色的行为,搬到你自己的服务器来。
在 Github 上面搜索 OIDC Provider 会有很多结果:
JS:https://github.com/panva/node-oidc-provider
Golang:https://github.com/dexidp/dex
Python:https://github.com/juanifioren/django-oidc-provider
...
不再一一列举,你需要选择适合你的编程语言的 OIDC Provider 包,然后让它在你的服务器上运行起来。本文使用 JS 语言的 node-oidc-provider。
示例代码 Github
可以在 Github 找到本文示例代码:
https://github.com/Authing/implement-oidc-sso-demo.git
创建文件夹
我们首先创建一个文件夹,用于存放代码:
$ mkdir demo $ cd demo
克隆仓库
然后我们将 https://github.com/panva/node-oidc-provider.git 仓库 clone 到本地
$ git clone https://github.com/panva/node-oidc-provider.git
安装依赖
$ cd node-oidc-provider $ npm install
在 OIDC Provider 申请一个 Client
上一步讲到,Github 会分配给你一对 id 和 key,这一步其实就是你在 Github 申请了一个 Client。那么如何向我们自己的服务器上的 OIDC Provider 申请一对这样的 id 和 key 呢?
以 node-oidc-provider 举例,最快的获得一个 Client 的方法就是将 OIDC Client 所需的元数据直接写入 node-oidc-provider 的配置文件里面。
Wait wait wait,跨度有些大,这两者之间有什么关系?首先我们看,在 Github 上填写应用信息,然后提交,会发送一个 HTTP 请求到 Github 服务器。Github 服务器会生成一对 id 和 key,还会把它们与你的应用信息存储到 Github 自己的数据库里。所以,我们将 OIDC Client 所需的元数据直接写入到配置文件,可以理解成,我们在自己的数据库里手动插入了一条数据,为自己指定了一对 id 和 key 还有其他的一些 OIDC Client 信息。
修改配置文件
进入 node-oidc-provider 项目下的 example 文件夹:
$ cd ./example
编辑 ./support/configuration.js
,更改第 16 行的 clients 配置,我们为自己指定了一个 client_id 和一个 client_secret,其中的 grant_types 为授权模式,authorization_code 即授权码模式,redirect_uris 数组是允许的业务回调地址,需要填写 Web App 应用的地址,OIDC Provider 会将临时授权码发送到这个地址,以便后续换取 token。
module.exports = { clients: [ { client_id: '1', client_secret: '1', grant_types: ['refresh_token', 'authorization_code'], redirect_uris: ['http://localhost:8080/app1.html', 'http://localhost:8080/app2.html'], }, ], ... }
启动 node-oidc-provider
在 node-oidc-provider/example 文件夹下,运行以下命令来启动我们的 OP:
$ node express.js
到现在,我们的准备工作已经完成了,在讲如何在 Web App 中进行单点登录之前,我们先了解一下 OIDC 授权码模式。刚刚提到的许多术语:授权码模式、业务回调地址、临时授权码,可能这些概念你会感到陌生,下文会详细介绍。
OIDC 授权码模式
以下是 OIDC 授权码模式的交互模式,你的应用和 OP 之间要通过这样的交互方式来获取用户信息。
我们的 OIDC Provider 对外暴露一些接口
授权接口
每次调用这个接口,就像是对 OIDC Provider 喊话:我要登录,如第一步所示。
然后 OIDC Provider 会检查当前用户在 OIDC Provider 的登录状态,如果是未登录状态,OIDC Provider 会弹出一个登录框,与终端用户确认身份,登录成功后会将一个临时授权码(一个随机字符串)发到你的应用(业务回调地址);如果是已登录状态,OIDC Provider 会将浏览器直接重定向到你的应用(业务回调地址),并携带临时授权码(一个随机字符串)。如第二、三步所示。
token 接口
每次调用这个接口,就像是对 OIDC Provider 说:这是我的授权码,给我换一个 access_token。如第四、五步所示。
用户信息接口
每次调用这个接口,就像是对 OIDC Provider 说:这是我的 access_token,给我换一下用户信息。到此用户信息获取完毕。
为什么这么麻烦?直接返回用户信息不行吗?
因为安全,关于 OIDC 协议的安全性,又可以展开很大的篇幅,现在简单解释一下:code 的有效期一般只有十分钟,而且一次使用过后作废。OIDC 协议授权码模式中,只有 code 的传输经过了用户的浏览器,一旦泄露,攻击者很难抢在应用服务器拿这个 code 换 token 之前,先去 OP 使用这个 code 换掉 token。而如果 access_token 的传输经过浏览器,一般 access_token 的有效期都是一个小时左右,攻击者可以利用 access_token 获取用户的信息,而应用服务器和 OP 也很难察觉到,更不必说去手动撤退了。如果直接传输用户信息,那安全性就更低了。一句话:避免让攻击者偷走用户信息。
编写第一个应用
我们创建一个 app1.html 文件来编写第一个应用 demo,在 demo/app 目录下创建:
$ touch app1.html
并写入以下内容:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>第一个应用</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">登录</a> </body> </html>
编写第二个应用
我们创建一个 app2.html 文件来编写第二个应用 demo,注意 redirect_uri 的变化,在 demo/app 目录下创建:
$ touch app2.html
并写入以下内容:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>第二个应用</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">登录</a> </body> </html>
向 OIDC Provider 发起登录请求
现在我们启动一个 web 服务器,推荐使用 http-server
$ npm install -g http-server # 安装 http-server $ cd demo/app $ http-server .
我们访问第一个应用:http://localhost:8080/app1.html
然后点击「登录」,也就是访问 OIDC Provider 的授权接口。然后我们来到了 OIDC Provider 交互环节,OIDC Provider 发现用户没有登录,要求用户先登录。node-oidc-provider demo 会放通任意用户名 + 密码,但是你在真正实施单点登录时,你必须使用你的用户目录即中央数据表中的用户数据来鉴权用户,相关的代码可能会涉及到数据库适配器,自定义用户查询逻辑,这些在 node-oidc-provider 包的相关配置中需要自行插入。
现在点击「登录」,转到确权页面,这个页面会显示你的应用需要获取那些用户权限,本例中请求用户授权获取他的基础资料。
点击「继续」,完成在 OP 的登录,之后 OP 会将浏览器重定向到预先设置的业务回调地址,所以我们又回到了 app1.html。
在 url query 中有一个 code 参数,这个参数就是临时授权码。code 最终对应一条用户信息,接下来看我们如何获取用户信息。
Web App 从 OIDC Provider 获取用户信息
事实上,code 可以直接发送到后端,然后在后端使用 code 换取 access_token。这里我使用 postman 演示如何通过 code 换取 access_token。
你可以使用 curl 命令来发送 HTTP 请求:
$ 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/app2.html' \ --data-urlencode 'code=QL10pBYMjVSw5B3Ir3_KdmgVPCLFOMfQHOcclKd2tj1' \ --data-urlencode 'grant_type=authorization_code'
获取到 access_token 之后,我们可以使用 access_token 访问 OP 上面的资源,主要用于获取用户信息,即你的应用从你的用户目录中读取一条用户信息。
你可以使用 curl 来发送 HTTP 请求:
$ curl --location --request POST 'http://localhost:3000/me' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'access_token=I6WB2g0Rq9G307pPVTDhN5vKuyC9eWjrGjxsO2j6jm-'
到此,App 1 的登录已经完成,接下来,让我们看进入 App 2 是怎样的情形。
登录第二个 Web App
我们打开第二个应用,http://localhost:8080/app2.html
然后点击「登录」。
用户已经在 App 1 登录时与 OP 建立了会话,User ←→ OP 已经是登录状态,所以 OP 检查到之后,没有再让用户输入登录凭证,而是直接将用户重定向回业务地址,并返回了授权码 code。
同样,App 2 使用 code 换 access_token
curl 命令代码:
$ 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/app2.html' \ --data-urlencode 'code=QL10pBYMjVSw5B3Ir3_KdmgVPCLFOMfQHOcclKd2tj1' \ --data-urlencode 'grant_type=authorization_code'
再使用 access_token 换用户信息,可以看到,是同一个用户。
curl 命令代码:
$ curl --location --request POST 'http://localhost:3000/me' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'access_token=I6WB2g0Rq9G307pPVTDhN5vKuyC9eWjrGjxsO2j6jm-'
到此,我们实现了 App 1 与 App 2 之间的账号打通与单点登录。
登录态管理
到目前为止,看起来还不错,我们已经实现了两个应用之间账号的统一,而且在 App 1 中登录时输入一次密码,在 App 2 中登录,无需再次让用户输入密码进行登录,可以直接返回授权码到业务地址然后完成后续的用户信息获取。
现在我们来考虑一下退出问题
只退出 App 1 而不退出 App 2
这个问题实质上是登录态的管理问题。我们应该管理三个会话:User ←→ App 1、User ←→ App 2、User ←→ OP。
当 OP 给 App 1 返回 code 时,App 1 的后端在完成用户信息获取后,应该与浏览器建立会话,也就是说 App 1 与用户需要自己保持一套自己的登录状态,方式上可以通过 App 1 自签的 JWT Token 或 App 1 的 cookie-session。对于 App 2,也是同样的做法。
当用户在 App 1 退出时,App 1 只需清理掉自己的登录状态就完成了退出,而用户访问 App 2 时,仍然和 App 2 存在会话,因此用户在 App 2 是登录状态。
同时退出 App 1 和 App 2
刚才说到单点登录,与之相对的就是单点登出,即用户只需退出一次,就能在所有的应用中退出,变成未登录状态。
最先想到的是这种方式,我们在 OIDC Provider 进行登出。
之后我们的状态是这样的:
好吧,其实没有任何效果,因为用户和 App 1 之间的会话依然保持,用户和 App 2 之间的会话同样依然保持,所以用户在 App 1 和 App 2 的状态仍然是登录态。
所以,有没有什么办法在用户从 OIDC Provider 登出之后,App 1 和 App 2 的会话也被切断呢?我们可以通过 OIDC Session Mangement 来解决这个问题。
简单来说,App 1 的前端需要轮询 OP,不断询问 OP:用户在你那还登录着吗?如果答案是否定的,App 1 主动将用户踢下线,并将会话释放掉,让用户重新登录,App 2 也是同样的操作。
当用户在 OP 登出后,App 1、App 2 轮询 OP 时会收到用户已经从 OP 登出的响应,接下来,应该释放掉自己的会话状态,并将用户踢出系统,重新登录。
刚刚我们提到 OIDC Session Management,这部分的核心就是两个 iframe,一个是我们自己应用中写的(以下叫做 RP iframe),用于不断发送 PostMessage 给 OP iframe,OP iframe 负责查询用户登录状态,并返回给 RP iframe。
让我们把这部分的代码加上:
首先打开 node-oidc-provider 的 sessionManangement 功能,编辑 ./support/configuration.js
文件,在 42 行附近,进行以下修改:
... features: { sessionManagement: { enabled: true, keepHeaders: false, }, }, ...
然后和 app1.html、app2.html 平级新建一个 rp.html 文件,并加入以下内容:
<script> var stat = 'unchanged'; var url = new URL(window.parent.location); // 这里的 '1' 是我们的 client_id,之前在 node-oidc-provider 中填写的 var mes = '1' + ' ' + url.searchParams.get('session_state'); console.log('mes: ') console.log(mes) function check_session() { var targetOrigin = 'http://localhost:3000'; var win = window.parent.document.getElementById('op').contentWindow; win.postMessage(mes, targetOrigin); } function setTimer() { check_session(); timerID = setInterval('check_session()', 3 * 1000); } window.addEventListener('message', receiveMessage, false); setTimer() function receiveMessage(e) { console.log(e.data); var targetOrigin = 'http://localhost:3000'; if (e.origin !== targetOrigin) { return; } stat = e.data; if (stat == 'changed') { console.log('should log out now!!'); } } </script>
在 app1.html 和 app2.html 中加入两个 iframe 标签:
<iframe src="rp.html" hidden></iframe> <iframe src="http://localhost:3000/session/check" id="op" hidden></iframe>
使用 Ctrl + C 关闭我们的 node-oidc-provider 和 http-server,然后再次启动。访问 app1.html,打开浏览器控制台,会得到以下信息,这意味着,用户当前处于未登录状态,应该进行 App 自身会话的销毁等操作
然后我们点击「登录」,在 OP 完成登录之后,回调到 app1.html,此时用户变成了登录状态,注意地址栏多了一个参数:session_state,这个参数就是我们上文用于在代码中向 OP iframe 轮询时需要携带的参数。
现在我们试一试单点登出,对于 node-oidc-provider 包提供的 OIDC Provider,只需要前端访问 localhost:3000/session/end
收到来自 OP 的登出成功信息
我们转到 app1.html 看一下,此时控制台输出,用户已经登出,现在要执行会话销毁等操作了。
不想维护 App 1 与用户的登录状态、App 2 与用户的登录状态
如果不各自维护 App 1、App 2 与用户的登录状态,那么无法实现只退出 App 1 而不退出 App 2 这样的需求。所有的登录状态将会完全依赖用户与 OP 之间的登录状态,在效果上是:用户在 OP 一次登录,之后访问所有的应用,都不必再输入密码,实现单点登录;用户在 OP 登出,则在所有应用登出,实现单点登出。
使用 Authing 解决单点登录
以上就是一个完整的单点登录系统的轮廓,我们需要维护一份全体用户目录,进行用户注册、登录;我们需要自己搭建一个 OIDC Provider,并申请一个 OIDC Client;我们需要使用 code 换 token,token 换用户信息;我们需要在自己的应用中不断轮询 OP 的登录状态。
读到这里,你可能会觉得实现一套完整的单点登录系统十分繁琐,不仅要对 OIDC 协议非常熟悉,还要自己架设 OIDC Provider,并且需要自行处理应用、用户、OP 之间登录状态。有没有开箱即用的登录服务呢?Authing 能够提供云上的 OP,云上的用户目录和直观的控制台,能够轻松管理所有用户、完成对 OP 的配置。
Authing 对开发者十分友好,提供丰富的 SDK,进行快速集成。
如果你不想关心登录的细节,将 Authing 集成到你的系统必定能够大幅提升开发效率,能够将更多的精力集中到核心业务上。
欢迎体验:https://authing.cn
实现单点登录:https://docs.authing.cn/authing/quickstart/implement-sso-with-authing
相关阅读
- 为什么所有软件都应该使用单点登录来管理用户?
- 用 Authing 10 分钟实现单点登录
- 案例 | 在 Odoo 中集成 Authing 完成单点登录
- Authing 插件上架 Odoo 官方市场,单点登录即可拥有
本文由博客一文多发平台 OpenWrite 发布!
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
小米开源 Redmi K30 Pro 内核源码
小米已开源基于 Android Q 的 Redmi K30 Pro 的内核源码,可从 GitHub 进行下载。 Redmi K30 Pro 的预装系统为 MIUI 11(基于 Android Q),随着内核源码的发布,开发者和愿意折腾的用户能够充分利用硬件的潜力,相信市场上也会很快出现该机型的第三方固件。 小米在开源内核方面的动作一直以来都比较积极,比如在发布 POCO X2 时开源了 Redmi K30 5G 的内核源码,以及小米10、小米9系列发布当天即开源内核源码。小米手机系统软件部总监张国全也表示欢迎下载编译,“使用开源系统,回馈开源社区”。 稿源:https://cntechpost.com/
- 下一篇
Swift 将增加对 Windows 和其他 Linux 发行版的支持
Swift 开发团队表示,其即将推出的 5.3 版本的目标包括“增加对 Windows 和其他 Linux 发行版的支持”。 他们提到 Swift 5.3 将包括重大的质量和性能增强。更重要的是,此版本还将扩展 Swift 可用和受支持的平台的数量,特别是增加对 Windows 和其他 Linux 发行版的支持。 苹果开源了 Swift 编程语言,但除了自家的平台,似乎没有动力去扩大对其他平台的支持,所以 Swift 跨平台的进展比较缓慢,目前仅支持 macOS 和 Ubuntu。 正因如此,不少社区成员十分积极将 Swift 移植到更多平台。例如,IBM 在服务器端方面为 Swift 贡献了 Kitura 框架,但由于令人失望的使用情况,IBM 在2019年12月放弃了对它的大部分支持。尽管如此,目前仍然有一个官方的Swift Server 工作组(SSWG),其主导的项目包括 Swift NIO(事件驱动的网络框架)。此外,还有知名的Vapor框架,这是一个可在 macOS 和 Ubuntu 上运行的 Web 开发框架。 对于 Windows 平台,曾经有过一个开源的SwiftFo...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装