跨域 ASP.NET 应用程序的单点登录 (SSO):第一部分 - 设计蓝图






4.98/5 (133投票s)
ASP.NET 应用程序中域独立单点登录实现的架构方法。
引言
"又是一个周一的早上,你坐在办公室里。你刚开始想周末过得有多快,以及这周的日子会有多难。你收到了一封邮件!哦不,不是一份丰厚的 Offer。这只是你的新客户的另一个需求。你的客户有许多 ASP.NET 网站,他只是希望用户只需登录一个网站,就能登录所有网站。显然,只需从一个网站注销,就能注销所有网站。
好的,所以你的客户想要一个“单点登录”实现。你心想,哦,这没那么难。ASP.NET 窗体身份验证就是解决方案。它允许使用<machineKey>
元素在同一域下的网站之间使用相同的配置键共享相同的 Cookie。快速的 Google 搜索会给你一些使用 ASP.NET 应用程序中的<machineKey>
实现单点登录的相当好的例子。好的,所以程序员的生活毕竟没那么艰难。
等等。你注意到了什么。需求中写道,“你的客户有许多网站,但它们不一定在同一域下”。你刚刚忽略了这一重要点,而你上班的第一天就开始觉得更难了。对于不同域下的网站实现单点登录并不容易,原因很简单,一个特定域的 Cookie 不能与另一个域共享。谁不知道 Cookie 是用于在不同页面请求之间维护身份验证信息的呢?”
我刚刚描述了一个现在很常见的场景。正如他们所说,这是一个 Web 2.0 和社交网络时代。独立和“孤岛”般的系统现在非常罕见。你可以在 Twitter 上发推文,同时在 LinkedIn 和 Facebook 上更新你的状态,而无需执行任何其他操作。你可以在 CodeProject 上写一篇文章,并在几秒钟内将其分享到数百个网站。因此,你很自然会期望登录一个网站并跳转到另一个相关网站,而无需重新登录,无论这些网站部署在哪个域下。
所以我想,开发一些能够轻松实现跨域网站的单点登录(当然,对于 ASP.NET 网站)的东西怎么样?人们可能尝试过许多不同的方式来实现这一点,商业解决方案也可以购买。但是,如果我尝试开发一些简单、免费且最重要的是有效的东西呢?
ASP.NET 中的身份验证工作原理
嗯,这可能对你来说不是什么新鲜事。但是,当我们试图解决地球上最困难的问题时,我们常常需要回到基础,尝试理解事物是如何真正运作的。所以,重新回顾 ASP.NET 窗体身份验证机制的基础知识也不错。
ASP.NET 窗体身份验证的工作原理如下
操作序列
- 您访问了一个 ASP.NET 应用程序中的页面,该页面仅供应用程序的已认证用户(已登录用户)访问。
- ASP.NET 运行时在请求中查找 Cookie(表单身份验证 Cookie),如果未找到 Cookie,它会将当前请求重定向到登录页面(登录页面位置在 web.config 中配置)。
- 您提供登录凭据并点击“登录”按钮。假设凭据正确,您的系统会成功地根据数据存储验证凭据,将用户名设置为
Thread.CurrentPrincipal.Identity.Name
属性,在响应中写入一个 Cookie(同时用用户信息设置 Cookie 值并设置一些属性,如 Cookie 名称、过期日期/时间等),并重定向到最初请求的页面。 - 您访问应用程序中的另一个页面(或者,点击一个导航到另一个页面的链接),您的浏览器会发送身份验证 Cookie(以及之前由同一网站写入的任何其他 Cookie),这次是因为浏览器已在上次响应中获得了身份验证 Cookie。
- 与往常一样,ASP.NET 运行时在请求中查找身份验证 Cookie,这次它找到了 Cookie。它检查 Cookie 属性(如过期日期/时间、路径等),如果尚未过期,则读取 Cookie 值,从 Cookie 中检索用户信息,将用户名设置到
Thread.CurrentPrincipal.Identity.Name
属性中,检查用户是否有权限访问请求的页面,如果有权限,则执行并将页面提供给浏览器。
很简单,对吧?
同一域下的多个 ASP.NET 站点如何进行身份验证
如前所述,ASP.NET 窗体身份验证完全依赖于 Cookie。因此,如果两个不同的站点可以共享相同的身份验证 Cookie,那么用户只需登录一个站点,就可以登录这两个站点。
现在,HTTP 协议规定,只有当两个不同的站点都部署在同一域(或子域)下时,它们才能共享一个 Cookie。在内部,您的浏览器会根据网站的 URL 将 Cookie 本地存储(在磁盘或内存中)。当您向任何站点发送后续请求时,浏览器会读取那些与当前请求 URL 匹配域或子域名的 Cookie,并将这些 Cookie 随请求发送。
那么,假设您有两个网站如下
- www.mydomain.com/site1
- www.mydomain.com/site2
这两个站点共享相同的宿主地址(相同的域 *mydomain.dom* 和子域 *www*),并且都已配置为使用窗体身份验证进行用户身份验证和授权。假设您刚刚登录了 *www.mydomain.com/site1* 站点,如上所述,您的浏览器现在有一个源自 *www.mydomain.com/site1* 的窗体身份验证 Cookie。
现在,如果您点击/访问 *www.mydomain.com/site1* 的任何后续 URL,表单身份验证 Cookie 将随请求发送。为什么?因为该 Cookie 最初属于此站点?是的,但不完全正确。确切地说,该 Cookie 将被发送,因为请求 URL *www.mydomain.com/site1* 具有相同的匹配子域和域名(主机地址)*www.mydomain.com*。
因此,在您登录 *www.mydomain.com/site1* 之后,如果您访问 *www.mydomain.com/site2* 的 URL,表单身份验证 Cookie(或任何其他匹配的 Cookie)将随请求发送,原因完全相同,即 *www.mydomain.com/site2* 具有相同的匹配主机地址 (*www.mydomain.com*),即使这是一个不同的应用程序 (*site2*)。因此,很明显,由于相同的表单身份验证 Cookie 可以跨同一主机地址下的两个不同的 Web 应用程序共享,因此只需登录一个 Web 应用程序即可同时登录这两个 Web 应用程序(因此,单点登录)。
然而,ASP.NET 不允许您仅仅通过在同一主机地址下为多个 Web 应用程序实现窗体身份验证来自动实现单点登录。为什么?因为每个不同的 ASP.NET Web 应用程序都使用自己的加密密钥来加密和解密 Cookie 数据(以及 ViewState 等其他数据)以确保安全性。因此,除非您为每个 ASP.NET 应用程序指定一个单一的加密密钥,否则 Cookie 将从浏览器发送,但应用程序将无法读取源自另一个应用程序的身份验证 Cookie 的值。
指定一个单一的认证密钥是解决方案。对于每个 ASP.NET 应用程序,您必须在 web.config 中使用相同的 <machinekey>
元素,如下所示
<machineKey
validationKey="21F090935F6E49C2C797F69BBAAD8402ABD2EE0B667A8B44EA7DD4374267A75D"
decryptionKey="ABAA84D7EC4BB56D75D217CECFFB9628809BDB8BF91CFCD64568A145BE59719F"
validation="SHA1"
decryption="AES"/>
如果在同一域下的所有应用程序中都使用相同的 machinekey
(包含 validationKey
和 decryptionKey
),则每个应用程序都能够读取源自其他应用程序的 Cookie 值。
如果两个网站具有相同的域但子域不同呢?
假设您的网站部署在以下主机地址下
- site1.mydomain.com
- site2.mydomain.com
这两个站点共享相同的域(相同的二级域 *mydomain.com*),但在三级域(不同的子域 *site1* 和 *site2*)上有所不同。
默认情况下,浏览器仅发送匹配主机地址(匹配域和子域)的 Cookie。因此,站点 *site1.mydomain.com* 不会收到任何源自 *site2.mydomain.com* 的 Cookie(因为它们没有相同的主机地址,它们的子域不同),因此即使您为这两个站点配置了相同的 machineKey
,一个站点也无法获取源自另一个站点的 Cookie。
因此,除了在所有站点中实现相同的 machineKey
之外,您还需要指示 ASP.NET 运行时定义身份验证 Cookie 的域,以便浏览器随任何具有匹配域名的请求发送该 Cookie。您需要按如下方式配置表单身份验证 Cookie
<forms name="name" loginUrl="URL" defaultUrl="URL" domain="mydomain.com"/>
那么,如何在多个域之间共享身份验证 Cookie 呢?
嗯,绝对没有办法做到这一点。HTTP 协议的基本障碍阻止您跨不同域共享 Cookie,这主要是出于安全原因。
假设您在两个不同域下有以下两个站点
- www.domain1.com
- www.domain2.com
现在,如果您使用表单身份验证登录到 *www.domain1.com* 站点,并访问 *www.domain2.com* 的 URL,浏览器将不会简单地将 *domain1.com* 的身份验证 Cookie 发送到 *domain2.com*。ASP.NET 中没有内置机制可以实现这两个不同站点之间的单点登录。
为了在不同域下的这两个站点之间实现单点登录,必须有一些“变通方法”或某种实现模型,允许这两个站点通过某种“间接”机制访问单个 Cookie。
一种非常基本的跨域 SSO 实现模型
假设我们必须在这些站点中实现 SSO
- www.domain1.com
- www.domain2.com
- www.domain3.com
为了在这些站点之间实现 SSO,当用户在两个站点中的任何一个站点进行身份验证时,我们需要为所有三个站点向客户端浏览器设置一个身份验证 Cookie。
如果 *user1* 在 *www.domain1.com* 中通过身份验证,浏览器将在返回 *www.domain1.com* 的响应之前,在响应中添加一个身份验证 Cookie。但由于我们需要能够登录 *www.domain2.com* 和 *www.domain3.com*,我们需要同时为 *www.domain2.com* 和 *www.domain3.com* 在同一个客户端浏览器中设置身份验证 Cookie。因此,在将响应返回给浏览器之前,*www.domain1.com* 必须将浏览器重定向到 *www.domain2.com* 和 *www.domain3.com* 以设置身份验证 Cookie。
下图描述了基本思想(箭头表示重定向)
以下序列图描绘了详细的想法
操作序列
- 用户在 URL 中访问 *www.domain1.com* 中已认证页面的 URL。
[Status: browser has no authentication cookie]
[Status: browser has no authentication cookie]
[Status: browser has no authentication cookie]
[Status: browser has no authentication cookie]
[Status : browser has no authentication cookie]
[Status : browser has no authentication cookie]
[Status : browser has an authentication cookie for www.domain2.com]
ReturnUrl
地址,并通过读取请求中的 Cookie 值来设置 *www.domain1.com* 的身份验证 Cookie。最终,身份验证 Cookie 也随重定向命令一起发送到响应中。[Status : browser has an authentication cookie for www.domain2.com]
[Status : browser has authentication cookie
for both www.domain1.com and www.domain2.com].
[Status : browser has authentication cookie
for both www.domain1.com and www.domain2.com].
因此,如果用户现在在浏览器中访问 *www.domain2.com* 的已认证页面,由于身份验证 Cookie 已经为 *www.domain2.com* 存储在浏览器中,它会随请求发送,*www.domain2.com* 从身份验证 Cookie 中检索用户信息,登录用户,并提供用户请求的页面输出。
由于浏览器现在拥有 *www.domain2.com* 和 *www.domain3.com* 的身份验证 Cookie,用户被视为已登录这两个站点,因此,在这两个站点中实现了单点登录 :)
那么“单点注销”呢?
如果我们只需要管理用户的站点登录,那么到目前为止我们已经完成了。但是作为单点登录的一部分,我们还需要管理“单点注销”。也就是说,当用户从一个站点注销时,该用户应该被标记为已从所有站点注销。
在内部,当用户从单个站点(例如,*www.domain1.com*)注销时,所有其他站点的身份验证 Cookie 都需要从浏览器中删除(在本例中为 *www.domain2.com*)。可以通过在将响应发送到浏览器之前从响应中删除 Cookie 来从浏览器中删除 Cookie。
要移除所有站点的所有身份验证 Cookie,应遵循上述请求-重定向-响应流程,但这次不是设置身份验证 Cookie,而是应从响应中移除身份验证 Cookie。
此 SSO 模型的问题
此模型对于两个站点应该运行良好。对于登录站点和注销站点,相应的站点应内部遵循与所有其他站点(SSO 下的站点)的请求-重定向-响应流程。一旦用户登录到一个站点,对于任何站点的所有后续请求,都没有请求-重定向-响应循环,因为每个站点在浏览器中都有自己的身份验证 Cookie。
但是,如果站点数量超过2个,复杂性就会增加。也就是说,如果用户登录 *www.domain1.com*,该站点将内部重定向到 *www.domain2.com* 和 *www.domain3.com* 以在浏览器中设置身份验证 Cookie,最后 *www.domain3.com* 重定向回 *www.domain1.com* 并将原始请求的页面输出提供给浏览器。这将使每个站点的登录和注销功能变得昂贵和复杂。如果有超过 3 个站点呢?如果我们需要在一个单点登录模型下集成 20 多个部署在不同域下的站点呢?这个模型显然不适用。
此模型还要求每个站点都了解在同一 SSO 伞形下配置的所有其他站点(因为每个站点都必须重定向到所有其他站点以设置身份验证 Cookie)。此外,这还需要在每个站点上实现用户身份验证和授权逻辑。
显然可以理解,随着站点数量的增加,这种 SSO 模型将变得混乱,并会失去其可接受性,这种模型不能真正被视为跨域应用程序的“通用”SSO 模型。因此,我们需要一个更好的模型,该模型应该独立于要集成在单点登录伞形下的站点数量。
一种用于跨域 SSO 的提议模型
之前的模型有点像“混搭”,每个站点都必须重定向到 N-1 个其他站点以设置和删除身份验证 Cookie,以进行登录和注销。每个站点都必须配置对其他 N-1 个站点的了解,并且必须实现一些复杂且昂贵的登录和注销逻辑。
如果我们维护一个单一的身份验证 Cookie 来跟踪所有站点的用户身份验证呢?引入一个专门的站点来验证用户并设置身份验证 Cookie 怎么样?听起来很有吸引力。
为了实现多个站点的SSO,用户数据库应该统一,因此,在一个专门的站点中实现用户认证和授权功能很有意义,每个站点都可以通过Web或WCF服务访问这些功能。这使得站点可以摆脱冗余的用户认证/授权功能。但最重要的问题是,这个专门的站点将如何帮助实现单点登录。
嗯,在这个模型中,浏览器不会为每个站点存储身份验证 Cookie。相反,它只会为专门的站点存储一个身份验证 Cookie,其他站点将使用该 Cookie 来实现单点登录。我们称这个站点为 *www.sso.com*。
在这个模型中,对任何站点(参与 SSO 模型)的每一个请求都将在内部重定向到 SSO 站点(*www.sso.com*),以设置和检查身份验证 Cookie 的存在。如果找到 Cookie,则将已认证页面提供给浏览器;如果未找到,则将用户重定向到相应站点的登录页面。
下图描绘了基本思想
为了详细理解该模型,让我们假设我们正在尝试应用此模型来实现以下两个站点的 SSO
- www.domain1.com
- www.domain2.com
我们现在有一个专门管理认证 Cookie 的站点:www.sso.com。
该模型的工作原理如下
操作序列
- 用户访问 *www.domain1.com* 的已认证页面的 URL。
- *www.domain1.com* 将请求重定向到 *www.sso.com*,并添加一个
ReturnUrl
查询字符串参数,该参数设置为原始请求的 URL。 - *www.sso.com* 检查是否存在任何身份验证 Cookie,或者请求中是否存在任何用户
Token
。都没有。因此,它将重定向到站点 *www.domain1.com*,并在 URL 中指示用户需要登录。它还会将ReturnUrl
参数值附加到查询字符串中。 - *www.domain1.com* 检查刚刚从 *www.sso.com* 重定向过来的查询字符串参数。查询字符串中包含用户身份验证 Cookie 未找到的指示,因此,它将当前请求重定向到 *www.domain1.com* 的登录页面,同时指示此请求不应重定向到 *www.sso.com*。
- 用户提供凭据并点击“登录”按钮。在此模型中,不会将回发请求重定向到 SSO 站点。这次,*www.domain1.com* 调用 *www.sso.com* 的 web/WCF 服务来检查提供的用户凭据,并在成功后返回带有
Token
属性的用户对象,该Token
属性在用户每次登录时生成(例如,一个GUID
)。 - *www.domain1.com* 将用户标记为已登录(例如,将用户对象存储在会话中),准备一个带有用户
Token
的 URL,并将当前请求重定向到 *www.sso.com* 以设置身份验证 Cookie,同时将ReturnUrl
参数值设置为原始请求 URL。 - *www.sso.com* 检查传入的请求 URL,并找到一个用户
Token
,但尚未提供身份验证 Cookie。这表明用户已在客户端站点 (*www.domain1.com*) 完成身份验证,现在需要在浏览器中为 *www.sso.com* 设置身份验证 Cookie。因此,它使用Token
从缓存中检索用户,准备一个带有用户信息并设置其他 Cookie 属性(过期日期/时间等)的身份验证 Cookie,将 Cookie 添加到响应中,最后重定向到ReturnUrl
查询字符串值中找到的原始请求 URL (*www.domain1.com* 的 URL),同时在查询字符串中添加Token
值。 - 浏览器现在收到一个重定向命令,
ReturnUrl
指向 *www.domain1.com*,并从 *www.sso.com* 获取一个身份验证 Cookie。因此它存储 *www.sso.com* 的身份验证 Cookie,并向 *www.domain1.com* 的 URL 发送请求。 - *www.domain1.com* 现在看到查询字符串中存在一个用户
Token
。它通过 *www.sso.com* 的 web/WCF 服务调用验证用户 Token,并在验证后执行 *www.domain1.com* 的原始请求页面,并将输出写入响应中。 - 用户现在访问 *www.domain2.com* 的已认证页面 URL。
- *www.domain2.com* 将请求重定向到客户端浏览器中的 *www.sso.com*,并将
ReturnUrl
属性设置为 *www.domain2.com* 的原始请求 URL。 - 浏览器收到重定向到 *www.sso.com* 的命令。它已经为该站点存储了身份验证 Cookie,因此在将请求发送到 *www.sso.com* 之前将此 Cookie 添加到请求中。
- *www.sso.com* 检查传入请求并在请求中查找任何身份验证 Cookie。如果这次找到身份验证 Cookie,则检查 Cookie 是否已过期。如果没有,它会从 Cookie 中检索用户
Token
并将请求重定向到 *www.domain2.com* 的原始请求页面 URL,同时在查询字符串中设置用户Token
。 - *www.domain2.com* 发现查询字符串中存在一个用户
Token
。因此,它通过 *www.sso.com* 的 web/WCF 服务调用验证用户 Token,并在验证后执行 *www.domain2.com* 的原始请求页面,并将输出写入响应中。
总结:
哇,听起来发生了好多事情!让我在这里总结一下
最初,浏览器没有任何 *www.sso.com* 的身份验证 Cookie。因此,在浏览器中访问 *www.domain1.com* 或 *www.domain2.com* 的任何已认证页面都会将用户重定向到登录页面(通过内部重定向到 *www.sso.com* 以检查身份验证 Cookie 的存在)。一旦用户登录到某个站点,*www.sso.com* 的身份验证 Cookie 就会在浏览器中设置,其中包含已登录的用户信息(最重要的是用户 Token
,该 Token
仅对用户的登录会话有效)。
现在,如果用户访问 *www.domain1.com* 或 *www.domain2.com* 的任何已认证页面 URL,请求会在用户的浏览器中内部重定向到 *www.sso.com*,浏览器会发送已设置的身份验证 Cookie。*www.sso.com* 找到身份验证 Cookie,提取用户 Token
,然后将浏览器重定向到带有用户 Token 的原始请求 URL,原始请求站点会验证 Token
并提供用户最初请求的页面。
一旦用户登录到此 SSO 模型下的任何站点,访问 *www.domain1.com* 或 *www.domain2.com* 上的任何已认证页面都会导致内部重定向到 *www.sso.com*(用于检查身份验证 Cookie 并检索用户 Token
),然后将身份验证页面作为浏览器输出提供。
Traffic
以下是不同场景的流量摘要
场景1:公共页面访问
从浏览器到客户端站点的一次行程 + 从客户端站点到浏览器的一次行程
摘要:1 个请求 + 1 个响应
场景2:未登录时访问已认证页面
从浏览器到客户端站点的一次行程(页面访问)+ 重定向命令到浏览器以及从浏览器到 SSO 站点的一次行程(重定向到 SSO 进行 Cookie 检查)+ 重定向命令到浏览器以及从浏览器到客户端站点的一次行程(重定向到客户端站点而不带 Cookie)+ 从客户端站点到浏览器的一次行程(登录页面)
摘要:1 个请求 + 2 次重定向 + 1 个响应
场景3:登录
从浏览器到客户端站点的一次行程(回发)+ 认证 Web 服务的调用(用户认证)+ 重定向命令到浏览器以及从浏览器到 SSO 站点的一次行程(带 Token 重定向到 SSO)+ 重定向命令到浏览器以及从浏览器到客户端站点的一次行程(设置认证 Cookie)+ Web 服务方法的调用(Token 验证)+ 从客户端站点到浏览器的一次行程(提供认证页面)
摘要:1 个请求 + 2 次服务器端 Web 服务调用 + 2 次重定向 + 1 个响应
场景4:登录后浏览已认证页面
从浏览器到客户端站点的一次行程(URL 访问)+ 重定向命令到浏览器以及从浏览器到 SSO 站点的一次行程(带认证 Cookie 重定向到 SSO)+ 重定向命令到浏览器以及从浏览器到客户端站点的一次行程(检查认证 Cookie)+ Web 服务方法的调用(Token 验证)+ 从客户端站点到浏览器的一次行程(提供页面)
摘要:1 个请求 + 2 次重定向 + 1 次服务器端 Web 服务调用 + 1 个响应
场景5:注销
从浏览器到客户端站点的一次行程(点击注销链接)+ 重定向命令到浏览器以及从浏览器到 SSO 站点的一次行程(注销指令)+ 重定向命令到浏览器以及从浏览器到客户端站点的一次行程(移除认证 Cookie 后)+ 从客户端站点到浏览器的一次行程(提供登录页面)
摘要:1 个请求 + 2 次重定向 + 1 个响应
权衡
比较基本 SSO 模型和提出的模型,基本模型似乎适用于 2 个或最多 3 个站点。在基本 SSO 模型的情况下,登录和注销功能在实现和执行时间方面会很复杂,但后续页面将在正常的请求-响应周期中提供(1 个请求和 1 个响应)。
然而,扩展 SSO 困难(添加新站点),配置管理最多(每个站点必须管理自己的 Cookie,并且每个站点都依赖于其他站点),并且必须在多个站点上实现冗余的用户身份验证功能。
另一方面,所提出的模型不依赖于要集成的站点数量(您可以根据需要添加任意数量的站点,SSO 模型不会改变)。参与站点的配置管理最少(没有站点需要了解其他站点,它们只需要了解 SSO 站点),身份验证和 Cookie 管理交由专门的站点 (*www.sso.com*) 处理,SSO 相关功能如果实现得巧妙,可以轻松地与站点集成,并且可以轻松扩展(可以轻松添加新站点)。
然而,存在小的性能损失。与基本模型不同,每次访问已认证页面都需要从浏览器到 SSO 站点和客户端站点发起 3 个请求(额外的两个请求是由于两次内部重定向)。尽管额外的两次请求执行时间最短(一个空白重定向请求,仅用于设置和检查 Cookie),因此考虑到当今可用的高互联网带宽,可以认为是可接受的。
拟议的 SSO 模型的实现
理论足够了,我们现在应该做好充分准备,开始动手编写代码。太好了,我已经完成了,并在下一篇文章中分享了。以下文章将介绍跨域 ASP.NET 站点拟议 SSO 模型的实际实现,并指出一些实现挑战及其解决方法。