SSO 单点登录
单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自独立的软件系统,提供访问控制的属性。当拥有这项属性时,当用户登录时,就可以获取所有系统的访问权限,不用对每个单一系统都逐一登录。这项功能通常是以轻型目录访问协议(LDAP)来实现,在服务器上会将用户信息存储到LDAP数据库中。相同的,单一退出(single sign-off)就是指,只需要单一的退出动作,就可以结束对于多个系统的访问权限。
web 系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问 web 系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了。
简言之,系统内部通过某种技术实现用户统一登录和注销,所以单点登录技术一定要包括两部分:登录、注销。
使用单点登录的好处包括:
- 降低访问第三方网站的风险(不存储用户密码,或在外部管理)。
- 减少因不同的用户名和密码组合而带来的密码疲劳。
- 减少为相同的身份重新输入密码所花费的时间。
- 因减少与密码相关的调用IT服务台的次数而降低IT成本。
SSO为所有其它应用程序和系统,以集中的验证服务器提供身份验证,并结合技术以确保用户不必频繁输入密码。
为什么要用单点登录? 因为 Cookie 不能跨域。
同顶级域名 Cookie
因为cookie不能跨域,所以无法在 app1.a.com
和 app2.a.com
这两个域名发送请求时携带cookie, 可以将Cookie的域设置为顶域
- 当前在 ccc.bbb.aaa.com 域名下,可以设置在 ccc.bbb.aaa.com 和 bbb.aaa.com 和 aaa.com 域名下的 cookie
|
|
Cookie 的作用域由 domain 属性和 path 属性共同决定。domain 属性的有效值为当前域或其父域的域名/IP地址,在 Tomcat 中,domain 属性默认为当前域的域名/IP地址。path 属性的有效值是以“/”开头的路径,在 Tomcat 中,path 属性默认为当前 Web 应用的上下文路径。
如果将 Cookie 的 domain 属性设置为当前域的父域,那么就认为它是父域 Cookie。Cookie 有一个特点,即父域中的 Cookie 被子域所共享,换言之,子域会自动继承父域中的Cookie。
LocalStorage 跨域
前面,我们说实现单点登录的关键在于,如何让 Session ID(或 Token)在多个域中共享。
父域 Cookie 确实是一种不错的解决方案,但是不支持跨域。那么有没有什么奇淫技巧能够让 Cookie 跨域传递呢?
很遗憾,浏览器对 Cookie 的跨域限制越来越严格。Chrome 浏览器还给 Cookie 新增了一个 SameSite 属性,此举几乎禁止了一切跨域请求的 Cookie 传递(超链接除外),并且只有当使用 HTTPs 协议时,才有可能被允许在 AJAX 跨域请求中接受服务器传来的 Cookie。
不过,在前后端分离的情况下,完全可以不使用 Cookie,我们可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端。这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID (或 Token )放在响应体中传递给前端。
在这样的场景下,单点登录完全可以在前端实现。前端拿到 Session ID (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中。
关键代码如下:
|
|
前端通过 iframe+postMessage() 方式,将同一份 Token 写入到了多个域下的 LocalStorage 中,前端每次在向后端发送请求之前,都会主动从 LocalStorage 中读取 Token 并在请求中携带,这样就实现了同一份 Token 被多个域所共享。
总结:此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域。
分布式 Session
Spring Session
不同域下的单点登录
CAS 认证中心 + LocalStorage 存储状态
在 B/S 系统中,登录功能通常都是基于 Cookie 来实现的。当用户登录成功后,一般会将登录状态记录到 Session 中,或者是给用户签发一个 Token,无论哪一种方式,都需要在客户端保存一些信息(Session ID 或 Token ),并要求客户端在之后的每次请求中携带它们。
单点登录的本质就是在多个应用系统中共享登录状态。如果用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session,比如可以将 Session 序列化到 Redis 中,让多个应用系统共享同一个 Redis,直接读取 Redis 来获取 Session。
仅此是不够的,因为不同的应用系统有着不同的域名,尽管 Session 共享了,但是由于 Session ID 是往往保存在浏览器 Cookie 中的,因此存在作用域的限制,无法跨域名传递,也就是说当用户在 app1.com 中登录后,Session ID 仅在浏览器访问 app1.com 时才会自动在请求头中携带,而当浏览器访问 app2.com 时,Session ID 是不会被带过去的。实现单点登录的关键在于,如何让 Session ID(或 Token)在多个域中共享。
认证中心
如何实现单点登录? 建立权限认证中心来处理登录和注销的问题,真正提供服务的应用服务端通过Filter将鉴权任务重定向给认证中心。
认证客户端应该具备的能力:
- 必须以Filter或者插件等形式提供,方便系统接入SSO。
- 未登陆的用户重定向到SSO认证中心
- 接收SSO发来的令牌并将该令牌发回给SSO做令牌认证
- 处理令牌认证结果并创建局部会话
- 拦截用户注销请求并重定向到SSO
- 处理SSO发来的注销会话请求
认证服务端应该具备的能力:
- 独立的web服务
- 提供登陆页面,和对用户的校验
- 创建全局会话、提供token
- 校验token有效性并维护client端地址
- 处理注销请求、销毁全局会话、并通知维护的client端地址
- 判断用户有没有登录
我们可以部署一个认证中心,认证中心就是一个专门负责处理登录请求的独立的 Web 服务。
用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 Token 写入 Cookie。(注意这个 Cookie 是认证中心的,应用系统是访问不到的。)
应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。
如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统。
应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的,其他应用系统是访问不到的。)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了。
这里顺便介绍两款认证中心的开源实现:
- Apereo CAS 是一个企业级单点登录系统,其中 CAS 的意思是”Central Authentication Service“。它最初是耶鲁大学实验室的项目,后来转让给了 JASIG 组织,项目更名为 JASIG CAS,后来该组织并入了Apereo 基金会,项目也随之更名为 Apereo CAS。
- XXL-SSO 是一个简易的单点登录系统,由大众点评工程师许雪里个人开发,代码比较简单,没有做安全控制,因而不推荐直接应用在项目中,这里列出来仅供参考。
Apereo CAS
Apereo CAS 为 Web 提供企业单点登录服务
- 一个开放且有据可查的协议
- 一个开源的 Java 服务器组件
- 可插拔身份验证支持(LDAP、数据库、X.509、2 因素)
- 支持多种协议(CAS、SAML、OAuth、OpenID)
- 适用于 Java、.Net、PHP、Perl、Apache、uPortal等的客户端库
- 与uPortal、BlueSocket、TikiWiki、Mule、Liferay、Moodle 等集成
- 社区文档和实施支持
- 广泛的采用者社区
上面的流程大概为:
- 用户输入网址进入业务系统Protected App,系统发现用户未登录,将用户重定向到单点登录系统CAS Server,并带上自身地址service参数
- 用户浏览器重定向到单点登录系统,系统检查该用户是否登录,这是SSO(这里是CAS)系统的第一个接口,该接口如果用户未登录,则将用户重定向到登录界面,如果已登录,则设置全局session,并重定向到业务系统
- 用户填写密码后提交登录,注意此时的登录界面是SSO系统提供的,只有SSO系统保存了用户的密码,
- SSO系统验证密码是否正确,若正确则重定向到业务系统,并带上SSO系统的签发的ticket
- 浏览器重定向到业务系统的登录接口,这个登录接口是不需要密码的,而是带上SSO的ticket,业务系统拿着ticket请求SSO系统,获取用户信息。并设置局部session,表示登录成功返回给浏览器sessionId(tomcat中叫JSESSIONID)
- 之后所有的交互用sessionId与业务系统交互即可
系统A发现用户并没有登录,于是重定向到sso认证中心,并将自己的地址作为参数。www.sso.com?service=www.a.com
sso认证中心发现用户未登录,将用户引导至登录页面,用户进行输入用户名和密码进行登录,用户与认证中心建立全局会话(生成一份Token,写到Cookie中,保存在浏览器上)
随后,认证中心重定向回系统A,并把Token携带过去给系统A
www.a.com?token-xxxxxxxxx
接着,系统A去sso认证中心验证这个Token是否正确,如果正确,则系统A和用户建立局部会话(创建Session)。到此,系统A和用户已经是登录状态了。
此时,用户想要访问系统B受限的资源,系统B发现用户并没有登录,于是重定向到sso认证中心,并将自己的地址作为参数。
www.sso.com?service=www.b.com
因为之前用户与认证中心www.sso.com
已经建立了全局会话(当时已经把Cookie保存到浏览器上了),所以这次系统B重定向到认证中心www.sso.com
是可以带上Cookie的。
认证中心根据带过来的Cookie发现已经与用户建立了全局会话了,认证中心重定向回系统B,并把Token携带过去给系统B
www.b.com?token-xxxxxxxxx
接着,系统B去sso认证中心验证这个Token是否正确,如果正确,则系统B和用户建立局部会话(创建Session)。到此,系统B和用户已经是登录状态了。
安全问题
明文传输的HTTP流量
HTTP明文传输数据的特性,使得攻击者可从网路上抓包获取Cookie。
解决方案:
- 服务端使用HTTPS
- 指定cookie的secure属性,该属性使cookie只能在HTTPS请求中带出。
利用XSS漏洞读取保存在cookie的通行证
假如网站存在XSS漏洞,那么恶意JS可直接读取Cookie中的通行证。可以通过指定Cookie的HttpOnly属性。对于指定了HttpOnly的Cookie,浏览器会拒绝JS读写。
XSS仍有可能利用服务端漏洞获取cookie: - 服务端有可能在请求的正常响应中包含通行证。不要笑,这真的有,尤其现在很多公司app和web共用一套后台; - 服务端可能有漏洞导致cookie包含在请求响应中,从而被窃取。比如:Apache CEV-2012-005
所以HttpOnly不是银弹,XSS也有很多其它的危害。但至少,HttpOnly可以避免通行证在浏览器被JS直接读取。
跨站请求伪造(CSRF)
比如在不安全聊天室或论坛上的一张图片,它实际上是一个给你银行服务器发送提现的请求:
<img src="http://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory">
当你打开含有了这张图片的HTML页面时,如果你之前已经登录了你的银行帐号并且Cookie仍然有效(还没有其它验证步骤),你银行里的钱很可能会被自动转走。有一些方法可以阻止此类事件的发生:
- 设置Cookie的SameSite属性。该属性规定cookie只会在当前正在访问(浏览器地址栏)的域名与cookie的domain可匹配时,才会带出。
- 任何敏感操作都需要确认;
- 用于敏感信息的Cookie只能拥有较短的生命周期;
- 更多方法可以查看OWASP CSRF prevention cheat sheet。
总结
还有一种解决系统间统一认证的思路,OAuth2.0,虽然也是解决多系统登录的,但是跟这里的单点登录思路完全不一样。