单点登录设计方案
随着业务复杂性的增加,作为一个整体的应用程序会被划分成许多“模块”,这通常需要考虑为这些子应用程序建立一个共同的基础,或者说,一开始就选择SOA的设计方式。不管怎样,我们开始运行多个单独的应用程序,并且需要它们无缝交互时,共同的基础就是必备的。实现单点登录服务便是其中必备功能之一。
授权与认证的概念
很多人容易搞混这两个概念。最典型的就是OAuth。比如我们要建立一个集中式统一认证体系,有人就会跳出来建议使用OAuth。但实际上OAuth是一个授权系统,而不是身份认证系统。
这个很具有迷惑性。比如,你可能想某网站X通过OAuth“验证”你的QQ账户,让你能直接访问该网站。 但你真正做的是允许该网站X使用OAuth provider(服务提供方,比如-腾讯)存储的你的信息。
通俗的讲:是OAuth provider存储有你的信息,而OAuth“认证”,只是通过你的授权,让其他系统得到你的信息。这是一个授权。
OAuth里面的Auth表示的是Authorization(授权)而不是Authentication(认证)。
授权和认证的区别:
- 认证:识别你是谁
- 授权:知道你被允许做什么,或者你允许其他人做什么。
本文重点讲的是认证-集中式认证服务-CAS(Central Authentication Service)。
典型场景
这可能是最典型的场景。这里有3个web应用程序部署在三个不同的子域名下,通过一个集中的身份验证服务使用共享的账户数据。
目标:
- 认证和账户数据的分离
- 允许用户在浏览不同的应用时保持已登录状态
简单来看,要实现这样的目标很容易。只需要将现有代码移到认证的架构下,分离认证的代码,然后看看哪些需要集中放,哪些可以分布式放。
集中并隔离共享的帐户数据
最简单的方式是:有一个共享数据库存储了用户账户的数据,每个应用都直连该数据库。当然,这样显然是不安全的,首先,我们要拉开它们的距离。我们需要一个接口,它是唯一的入口点,通过它来创建或更新共享的帐户数据。原则就是应用特有的数据就放在应用那里,任何跨应用共享的数据都应该搬到这个接口后面。
一般集中认证系统将会存储的信息有:
- ID-账户ID
- first name-名
- last name-姓
- login/nickname-登录名/昵称
- email-邮箱地址
- hashed password-哈希散列之后的密码(比如md5+salt)
- salt-时间变量
- creation timestamp-创建时间戳
- update timestamp-更新时间戳
- account state (verified-已认证, disabled-无效 …)
不要在每个系统去重复这些信息。技术上讲,就是你不能直接用SQL join账户表,你必须通过认证系统得到ID,再通过这些ID作为条件在你自己的数据库里查信息。
另外一种简单的做法就是,使用只读视图提供给各系统查询。(适用于内网,政府部门比较多)
登录流程
每个应用程序都有一个用户登录。我们不想去改变用户体验,我们需要的是使用透明的统一的身份认证检查来代替以前每个应用自己的本地校验。要做到这一点,最简单的方式就是保持你当前的登录表单但不post到你本地应用,我们post它到集中认证API。(链接建议通过SSL加密)
如图所示,登录表单提交到身份认证服务器。这个表单很可能包含的信息有:登录名或者email、一个明文password和一个隐藏的回调/跳转路径url(便于认证API能够跳转到用户的原始浏览页)。考虑到安全性,可以设置一个域名白名单,只有在白名单内的才被允许重定向。
在内部,身份验证应用程序将验证标识符(电子邮件或登录名)加上哈希之后的密文密码来匹配帐户数据。
如果认证成功,系统将生成一个包含用户数据的令牌token,(比如包含: id, first name, last name, email, created date, authentication timestamp)。
如果认证失败,不会生成token。最后,用户的浏览器会重定向到之前传过来的URL。
当然了,为了进一步安全,这个token信息是要加密的。以便于客户端能够验证和信任token是来自受信任的服务器。
方案一:RSA-所有的client端使用公钥,只有服务器上使用私钥。即私钥加密公钥解密。
方案二:签名-服务器返回签名参数,client端检查签名的真实性。HMAC或者DSA签名都是可以的。当然,在某些场景我们不希望人们看到服务器返回的参数,这又是另一回事。我们关心的重点在于我们返回的参数不能够被篡改,同时也能防止重放攻击。
从客户端这边来看,它接收token参数。如果token是空的或者不能解密,验证失败。这时我们需要再次给用户显示登录页面让他再试。如果token能解密,内容应该保存在session中,以便于将来request可以复用这些数据。
以上描述了登录认证的流程,但是如果用户登录到X系统,他怎么自动登进Y或者Z系统呢?
方法:设置一个顶级域的cookie,这个cookie能被所有子域的系统访问到。当然,这种方式只能适用于相同的域的情况。先来看看这种cookie的方式。后面再讨论不同域的情况。
这个cookie不需要包含很多数据,它需要的有:账户ID,一个时间戳(知道什么时候认证通过的)和一个受信签名。这个签名是至关重要的,因为这个cookie将允许用户自动登录其他网站。建议使用HMAC或者 DSA加密生成的签名。DSA加密,很像RSA加密,都是一种依赖于公钥/私钥的不对称加密。这种方式比HMAC这样基于一个共享密钥的加密安全性上强一些。但是最终使用哪一种还是取决于公司。
最后,我们需要在应用上配置一个自动登录的过滤器。自动登录过滤器将检查顶层域cookie是否存在,本地session是否缺失。如果验证cookie没问题,本session还没有,那么就用这个cookie里的ID自动创建一个session。
我们当然也能让所有应用共享一个session,但多数情况下,每个应用自己要存储的数据都是很特别的(应用自己的session除了存当前登录信息以外,还有其他的信息要存),而且让session独立可以更安全更干净,同时一个应用要集成不同的服务也更容易。
注册流程
注册和登录一样,两种方式:把用户的浏览器指向认证服务器API或者使用S2S(服务到服务)的调用,内部指向认证服务器。国内国外的互联网公司基本都是直接post表单到认证服务器,这种方式可以避免每个应用重复的逻辑和重复的与认证服务通信,所以我们采用这种方式。
如图所示,过程和登录的流程一样。不同点在于不是返回一个token,而是返回一些参数:id,email,可能的错误信息errors。
回调/重定向的url也是完全不同的:在该场景下,当用户注册成功,账户生成时,我们需要在顶级域.domain.com设置一个认证cookie让他能够自动登录。如果没成功,返回的信息就是用户注册的错误信息和他已经填过的邮箱地址。
至此,实现基本完成了。我们能够创建账户,使用凭证登录,我们的共享的顶级cookie只能被认证服务器创建,同时被所有client验证,让用户可以在不同的系统间切换而不用再次登录。代码实现简单、安全、高效。
更新或删除账户
更新或者删除账户,我们需要在client和认证服务之间做点事,我们使用S2S(服务到服务)的调用。为了安全我们使用一种更好的方式去记录请求,每个client和认证服务器之间通信时我们使用API token/key。这个API key能够通过X-header进行传递,这样我们可以分开处理request参数和通过X-header传递的认证相关信息。S2S服务应该有个过滤器来验证和记录这个API的请求。下面的过程就没什么说的了。
不同域的情况
上面,我们讨论过使用相同的顶级域的情况(京东就是这么干的)。实际上,不同域的情况也是有的。这就意味着之前顶级cookie的方式不行了。当然,不同域照样可以实现。
简单来说就是:当一个本地session没有的时候,在不同域那边的应用上使用一个iframe,这个iframe加载认证应用服务器的某个服务(比如hello),认证服务校验顶级域cookie的有效性。如果ok,返回一个认证token和跳转页面url。这个client将创建一个session并重定向到url。接下来的request请求就使用本地session了,不会再走这个过程。如果没有找到签名的cookie,iframe就显示登录页面或者重定向整个iframe的host到登录页面,这个看具体需要。
始终要记得在使用多个应用或者域的时候,需要保持共享的cookie是同步的。比如从一个系统登出了,那么就要删除在cookie里的信息以保证他是全局登出的。(也意味着可能需要一直去使用一个ifame去检查登录状态然后自动登出用户。或者使用时效)
(通常,使用ajax代替ifame进行跨域请求也是可行的)
手机客户端
手机端也需要能够注册/登录和更新账户。差别在于手机端和服务器通讯的模式可能不同或者存在限制。那么我们可以在登录的过程中提供一种不透明的token(比如SSL加密过的),然后在每个request请求时将其放入X-Header,然后传给服务器,服务器能够认证即可。手机APP一般是要存储登录信息到手机的内存或SD卡中。
具体的实现方案选择
- 我们自己设计实现,使用顶层cookie,京东是这么干的。
- 使用开源项目
l 开源项目:耶鲁大学的 CAS
它是基于 Java 的,源代码是开放的。只需使用过滤器,就能够相当轻松地在 Java 应用程序环境中实现它。Yale University 正在使用它,这说明它的品质满足我们的条件。它还允许轻松地改变或扩展实际的身份验证机制。
http://www.ibm.com/developerworks/cn/opensource/os-cn-cas/
http://blog.csdn.net/small_love/article/details/6664831
源码:https://github.com/Jasig/cas
l 开源项目:josso
纯java的互联网开源跨域跨平台单点登录项目
http://www.josso.org/confluence/display/JOSSO1/JOSSO+-+Java+Open+Single+Sign-On+Project+Home
l 开源项目:openSSO
Sun公司大力推出的开源SSO项目,Oracle收购之后已关闭该项目。