前言#
本文主要是为了调研实现跨站点认证的技术基础。
包括是否必须 iframe 来实现跨站携带同一认证,iframe 中跨站认证的实现方案可行性。
如,auth.com
作为统一认证服务,b.com
作为业务一的域名,c.com
作为业务二的域名,而且 b 和 c 的业务共用 auth 账号体系。
是否可以直接在
b.com
中通过请求auth.com
的接口获取认证 token,同理c.com
。业务 server 层直接请求auth.com
获取 key 之后对 token 进行校验,管理账号直接请求 auth 服务 来实现账号体系的复用。在业务 server 中,遇到需要更多用户信息的场景时,支持从 Auth Server 获取所需数据
对内的认证系统,可以直接暴露内网接口,不用担心信息的泄露。对外的认证系统,需要更细致全面的权限认证。
本文主要探讨对内的认证系统,即对于业务服务具有较高的信任度。
跨域问题#
涉及到多个域名,当然离不开浏览器跨域问题。
关于跨域问题和解决方案请参看之前的 blog 《跨域问题》
强烈建议先搞清楚跨域问题后再进行下文的阅读
由于跨域导致的问题#
无法在 b.com
和 c.com
直接请求 auth.com
,且浏览器本地登录状态无法跨域共享
可行方案罗列与优缺点分析#
1. CORS + 正向代理,利用 cookies 等手段储存登录状态#
此方案思路是让业务网站可以直接请求 auth server
方案流程:
- auth server 配置 cors 白名单支持业务网站跨域请求。
- 业务网站正向代理 auth 请求
利用 auth 请求的 cookies 储存登录状态和信息
优势#
- 方案直观:配置跨域之后,可以像正常请求一样和 auth server 进行通信
劣势#
- 跨域 cookies 存在限制:参看 《跨域问题 #浏览器的跨域限制有哪些》
- 业务和认证耦合:认证能力升级后需要业务项目更新
- 认证数据存储受限:由于通过 cookies 储存认证信息,所以认证信息存储受限于 cookies
- 流程偏长:后续网站自动登录需要通过再次请求
2. 利用 iframe 作为中间人,使用 window.postMessage 实现跨域通讯,避免直接跨域请求#
此方案利用 iframe + postMessage,避免跨域请求,并利用 auth 域的 localStorage 进行认证信息存储
方案流程:
- 业务网站实例化 auth iframe
- iframe 处理所有 auth 相关的数据请求和存储
- 业务页面 通过 postMessage 和 iframe 进行跨域信息互通
优势#
- 方案安全:postMessage 的作用就是安全地实现跨源通信,且方案中请求不跨域。不用担心方案失效
- 存储无限制:直接利用 auth origin 下的 localStorage 进行存储
- 认证系统可以无感升级:auth iframe 的升级不会影响业务逻辑
- 实时同步认证信息:多个 auth iframe 间可以通过
window.addEventListener('storage')
进行消息同步
劣势#
- 理解成本高:引入了 iframe,不直观
- auth iframe 样式难以个性化:业务逻辑难以个性化 auth iframe 里的节点样式
3. 业务 server 反向代理 auth 服务#
利用业务 server 方向代理 auth 请求,来规避跨域请求问题
方案流程:
- 业务 server 对于某个规则的请求进行转发到 auth server。
- 业务 client 直接利用业务域名进行认证请求
优势#
- 方案直观:client 直接和业务 server 通讯
劣势#
- 无法同步认证状态:所有的 auth 请求都合并到了业务请求中,client 端无法共享认证信息
- 业务和认证耦合:认证能力升级后需要业务项目更新
最优方案选择和细节设计#
综上所述,我更偏向于使用 iframe + postMessage 的方案。
此方案的缺点影响对于我来说影响是最低的。
整体方案定了之后,需要对实现细节进行设计,来实现业务端最优的使用体验
认证系统需处理的内容#
从几个业务场景出发:
登录、注册、等:
- 将用户输入作为凭证发送请求换取认证信息
- 缓存认证信息
- 推送业务层更新认证信息
主动退出、刷新认证信息:
- 业务逻辑向 认证系统 iframe 推消息
- iframe 接受消息,并分发动作(清除本地认证信息,发起对应请求等)
- 缓存最新的认证信息
- 推送更新认证信息到业务层
a 业务更新认证信息后,b 业务同步获得最新的认证信息
- a 业务向 认证系统 iframe 推消息,需要更新认证信息
- iframe 接受消息,并分发动作(清除本地认证信息,发起对应请求等)
- 缓存最新的认证信息
- a 和 b 的 iframe 都接收到认证信息的更新,并推送到业务层
从技术角度出发:
- 认证系统更新时,应做到业务无感
- 一些配置化的逻辑应该被封装(如 Event Key 等)
责任罗列#
- 输入交互
- 请求认证
- 认证数据缓存
- 认证数据更新广播
- 跨域通信
- 错误处理
责任划分探讨#
1. 输入交互应该归由那部分负责?业务、auth 组件、iframe#
业务:业务获取用户输入 -> 调用 auth 组件 postMessage 到 iframe 中 -> iframe 发起认证请求
auth 组件:auth 组件负责用户输入并将输入 postMessage 到 iframe -> iframe 发起认证请求
iframe:iframe 负责用户输入并发起认证请求
考虑到 认证系统更新升级 让业务无感,前两者划分方案会要求业务同步更新。
所以建议将所有认证相关的交互内容都放到 iframe 内部
这样,每次 auth 系统添加新的登录方式时,只需独立更新 auth server 和 iframe 输入,业务层面对于 iframe 的引用地址和 message 监听还是不变的。从而达到业务无感。
责任总结#
业务逻辑
- 监听 auth 组件的数据更新通知
- 主动调用 auth 组件操作认证数据
- 调用 auth 组件实例化 iframe
auth 组件
- 处理跨域通信:处理所有 postMessage 的收发和转译
- 优化 auth 组件的使用:业务层一处监听 + 适当实例化
iframe 页面
- 输入交互处理
- 认证请求通信
- 错误提示
- 认证数据 缓存
- 认证数据 更新广播
- 跨域通信:
- 接受:动作分发
- 发送:认证更新,错误通知,
流程解析#
关键环节技术调研#
浏览器跨域数据交互#
即实现 业务页面 和 auth iframe 之间的数据交互
postMessage#
父 向 iframe 发消息
document
.getElementById("iframe")
.contentWindow.postMessage("parent message", "*");
iframe 向 父 发消息
window.parent.postMessage("child message", "*");
监听消息
function receiveMessage({ origin, data }) {
console.log({ origin, data });
}
window.addEventListener("message", receiveMessage, false);
同源 iframe 数据广播#
在一个 业务 iframe 更新 token 后,另一个业务的 auth iframe 被通知 token 已被更新
localStorage#
一个窗口更新 localStorage,另一个窗口监听 window 对象的”storage”事件,来实现通信。
消息发送端:
localStorage.setItem("token", "**********");
消息监听端:
window.addEventListener("storage", function (e) {
// e.key
// e.oldValue
// e.newValue
});
安全控制#
1. postMessage 跨域消息安全#
相关知识《postMessage 安全》
由于 auth 系统将被多个业务系统引用,
所以域名白名单是无法避免的,
而由于严格指定targetOrigin
只能指定一个
所以使用 whitelist 方案:
const getParentOrigin = () => {
var url = null;
if (parent !== window) {
try {
url = parent.location.origin;
} catch (e) {
url = document.referrer;
}
}
return url && /^https?:\/\/[\w-.]+(:\d+)?/i.exec(url)[0];
};
const whitelist = ["https://yrobot.top"];
if (whitelist.includes(getParentOrigin())) {
window.parent.postMessage("secret", getParentOrigin());
}