PS:本文讲的是 SSO 单点登录,不是指用户只能在一处登录、把账号顶下线的 单设备登录。

单点登录系统适用的场景有哪些?#

  • B 端:公司需要一个员工统一登录平台,一个账号登录内部应用、产品后台。
  • C 端:用户需要一个账号通行公司旗下多个产品,比如一个 QQ 可以登录腾讯网、IM、QQ 音乐等。

单点登录有哪些协议?#

  • CAS
  • OIDC
  • SAML
  • 其他协议和自定义协议……

其中,CAS 协议的官方服务端实现是 Java,对其他语言不友好,比如 PHP 的 SDK 写得奇烂无比;SAML 则比较复杂。

单点登录大概原理是什么?#

各种协议的基本原理都类似:

  1. SSO 客户端重定向到 SSO 服务端,并带上客户端站点地址,让 SSO 服务端将用户重定向过去。
  2. 如果用户未登录,SSO 服务端先让用户完成登录;登录后,SSO 服务端再重定向回 SSO 客户端,同时在重定向地址的 query 中带上一个与当前用户关联的票据(CAS 为 ticket,OIDC 为 code)。
  3. SSO 客户端在后端发起 HTTP 请求,用票据向 SSO 服务端交换用户信息。
  4. SSO 客户端在本系统查询用户信息(不存在则新增),并写入登录状态。
SSO 单点登录基本流程时序图

SSO 基本流程:跳转式登录时序

依靠共享 SESSION 实现两个系统共享登录状态,是单点登录吗?#

单点登录一定是异系统,每个子系统都有自己的用户表;而共享 SESSION 往往是一个强关联的系统,甚至就是一张用户表。

内部用的 SSO 和 C 端 SSO 设计上有什么区别,是否通用?#

两种场景需求不同,设计上往往有较大差异。

举例来说,新员工入职录入 SSO 系统后,内网部署的 GitLab 中还没有该员工的账号信息。员工首次登录 GitLab 后,GitLab 从 SSO 拉取到员工信息并落库,这没有问题。

但是财务发工资的时候却会发现,财务系统中没有这个员工的信息,因为员工没有登录过财务系统,甚至财务系统压根没有员工登录的功能。所以,企业内部用的 SSO 一般需要上下游同步功能,比如公司使用了企业微信、钉钉、飞书、LDAP,SSO 需要把里面员工信息同步过来;同时,新员工录入信息、修改信息后,需要立即同步至公司内其他系统。

而在 C 端,这个需求往往不强烈。举例试想一下,腾讯有十数亿用户,如果腾讯新上线了一个小众产品,本身只有几十万用户,但是每天把十几亿不使用这个小众产品的用户修改昵称、头像等事件都同步到这个系统,就显得没有必要。

内部 SSO 除了接入自研应用,还会接入各种商用系统、开源系统、SaaS 系统。这些系统往往由不同厂商开发,五花八门,所以内部用的 SSO 往往必须支持 CAS 和 OIDC,几行配置就可以用员工账号登录这些系统;而 C 端往往只使用一种统一的协议,方便实现和维护。

OAuth2 和 OIDC 区别是什么,OAuth2 能不能用来做 SSO?#

  • OIDC 是叠加在 OAuth2 之上的认证协议:用 code 换 token 时,token 响应中除 access_token 外,还会多返回一个 JWT 格式的 id_token。access_token 用于授权,id_token 用于认证。
  • OAuth2 技术上可以搞成 SSO,很多人也确实在这样做,但不该这么做! OAuth2 的目的是授权。比如你微博授权登录某站,某站就能代替你发微博。 电商网站通过 OAuth2 授权给第三方工具,第三方工具就可以通过 OAuth2 代替你完成某些工作,比如上货、整理商品。 这是 OAuth2 设计出来的主要目的:把你在本站本身具有的功能权限授权给第三方。
  • 你应该使用 OIDC 来代替 OAuth2,从 id_token 中解析用户信息,和 SSO 客户端中的用户信息关联,完成登录,而不是通过 OAuth2 的 access_token 再去拉取一次用户信息。
  • 如果你接入过微信,就会发现腾讯为了防止你获得用户关系链,不同 appid 通过微信 OAuth2 获取到同一个微信用户的 openid 是不同的。OAuth2 做 SSO 会有很多潜在问题,尤其是同步退出问题。

怎么同步退出所有站点?#

举例来说,CAS 协议中,SSO、client1、client2 都处于登录状态。点击 client1 退出,重定向到 SSO 退出,此时 client2 还是登录状态。

  • 方案 1:异步通知退出 SSO 服务端退出时,启动一个异步队列任务,向当前登录用户当前登录 session 发放过 ticket 的 CAS client 发送 HTTP 通知,携带 ticket。SSO 客户端收到通知后,找到使用该 ticket 登录的用户 session_id,使 session_id 失效。

    大致原理如此。如果是自定义协议,可以直接在票据或用户信息中携带 session_id 给 SSO 服务端,SSO 服务端退出时直接 HTTP 通知 SSO 客户端把该 session_id 失效即可。 SSO 服务端可以在 Redis 存一个含当前 session_id 的 hash,hash 里存 SSO client_id 和 client session_id 即可,退出时把这个 hash 拉出来通知即可。

    不过不推荐这个方案,略复杂、可靠性不高。

  • 方案 2:同步通知退出 SSO 退出时,直接遍历请求 SSO client 的退出页面即可,JS 也行,iframe 也行。这个方案也是大厂都在用的方案,比如新浪退出时是 JS 请求一个 URL,返回一个数组:

// 请求 https://login.sina.com.cn/sso/logout.php
[
  "https://passport.weibo.com/wbsso/logout",
  "https://passport.krcom.cn/sso/crossdomain?action=logout&entry=krvideo",
  "https://passport.sina.cn/sso/crossdomain?action=logout",
  "https://passport.weibo.cn/sso/crossdomain?action=logout"
]

然后 JS 轮流请求返回的这些退出页面实现退出。

同步退出原理示意图

同步通知退出:JS/iframe 依次请求各站点的 logout 页面

小米退出则是跳转至 https://account.xiaomi.com/pass/logout,然后把各个域名下的退出页面作为图片调用:

https://logout.xiaomi.net/
https://logout.xiaomi.cn/
https://logout.miui.com/
https://logout.xiaomi.com/
https://logout.mi.com/
https://logout.duokan.com/
https://logout.miwifi.com/
https://logout.mipay.com/
https://i.mi.com/logout
https://credit.mixiaojin.com/gw/mixiaojin/logout

如果一个页面不需要登录也可以访问,即不会强制跳转,如何从 SSO 获得登录信息?#

比如 CAS 需要跳转 cas/login 才能拿到票据交换用户信息。如果一个 To C 网站,用户不登录就可以浏览新闻,此时新闻站不知道 SSO 登录状态,也不会强制跳转到 SSO 去。

这种情况在 B 端不是问题,毕竟内部系统往往都需要登录,或者就让用户自己点一下跳转;在 C 端体验就比较差了。

常见方案是:SSO 登录时除了 session_id,还在根域名下写一个 cookie,记录自己已经处于登录状态。新闻站读取到这个 cookie,知道 SSO 已经登录,则与 SSO 交互进行登录;如果没有这个 cookie,则继续游客状态。cookie 可以加密一下防止篡改。

如果你的站点处于多个根域名下,则使用上面同步退出的方案,登录时请求多个域写登录状态标识。伪例子:

你在 account.taobao.com/login 登录,成功后这个页面 JS 调用:

https://login.1688.com/sso/crossdomain
https://login.tmall.com/sso/crossdomain

crossdomain 页面写下 cookie is_login=1。

然后你再访问 www.tmall.com,读取到 is_login,自动重定向到 account.taobao.com/sso/login,再重定向回 www.tmall.com/login?ticket=票据。

多根域名登录状态同步示意图

多根域名下的登录态同步:crossdomain 写 cookie + 首次访问换票

我不想跳转登录,想弹窗登录如何做?#

CAS 和 OIDC 都是跳转登录的,你可以像百度和微博一样,自定义协议实现。

比如从百度首页弹窗登录后,返回一个链接数组,JS 依次请求:

https://baike.baidu.hk/common/api/crossdomain?bdu={bdu}&t={timestamp}
https://www.yoojia.com/sync/crossdomain?bdu={bdu}&t={timestamp}
https://user.hao123.com/static/crossdomain.php?bdu={bdu}&t={timestamp}

其中 bdu 是加密票据,crossdomain 页面解密这个 bdu 之后,在根域名下写入名为 BDUSS 的 cookie。

任意一个百度站点,如贴吧,检测到 BDUSS 这个 cookie 后,拿 BDUSS 在后端发送请求向 https://passport.baidu.com 交换用户信息,通过唯一 id 查询用户。 如果贴吧没有这个用户,写入用户表后再登录;有这个用户则直接登录,写入贴吧站自己的 session_id。

如果贴吧自己的 session_id 还处于登录状态,但是 BDUSS 没有了,则贴吧已经无法确认 SSO 的登录状态——可能是 passport.baidu.com 已经退出,也可能是该域下的 cookie 被清空了。稳妥起见,贴吧也退出自己的账号系统即可。

重点:百度和新浪均为这个方案,但都是有缺陷的。比如你在登录后把新浪网的 cookie 清空,此时你就会发现新浪退出了,非同域的微博却还登录着,除了再输密码登录一次别无他法;而必须跳转获取票据的方案均不存在该问题。

如何同步用户信息?#

SSO 除了登录名、密码,往往还集中储存头像、邮箱、手机号等用户信息。

假设贴吧登录状态是一个月,但是头像在拿 BDUSS 向 passport.baidu.com 交换用户信息时就已经拿到了。如果不退出的话,贴吧和 passport.baidu.com 在这一个月内也不会再交互。

如果用户修改了头像,贴吧会一直是旧头像,除非退出重新登录拉取。

在 CAS、OIDC 中都没有关于这部分的定义,因为它们只管认证,所有用户资料的同步,不管你是使用 CAS、OIDC 还是自定义协议实现的 SSO,都要自己设计同步方案。

可以客户端主动拉取,也可以 SSO 主动推送。最好每个 SSO 客户端站点都可以独立配置自己要订阅的事件和信息变更,没有必要做全量推送。

由于 Chrome 更新了 cookie 策略(跨站请求默认 SameSite=Lax),跨域写 cookie 必须将 SameSite 设置成 None,而设置 SameSite=None 的前提是必须同时设置 Secure,即该 cookie 只能在 HTTPS 下被浏览器接受。所以除非你的所有站点都使用 HTTPS,否则最好使用跳转方案。