跨域/跨平台身份验证和数据传输
介绍了一种在跨域/跨平台中对用户进行身份验证以及在身份验证过程中将用户数据从一个站点传输到另一个站点的方法。
引言
在我之前的文章《ASP.NET 及其他平台中的单点登录》中,我主要讨论了在二级域名中使用表单身份验证实现单点登录。作为后续,本文将介绍一种方法来覆盖跨多个二级域名/平台实现的单点登录。我还将讨论在身份验证过程中如何将数据库中的客户数据传输到另一个站点。让我们来看以下两个示例 URL:
- 站点 1:http://www.authenticationsite.com/app1/default.aspx (ASP.NET 站点)
- 站点 2:http://www.thirdpartysite.net/app2/default.cfm (ColdFusion 站点)
Site1 是位于 authenticationsite.com 域下的 ASP.NET 应用程序,而 Site 2 是位于 thirdpartysite.net 域下的 ColdFusion 应用程序。跨这两个站点的身份验证可能具有挑战性。首先,这两个站点无法读取彼此的 cookie,因为它们位于不同的二级域名 authenticationsite.com 和 thirdpartysite.net。其次,它们运行在不同的平台 ASP.NET 和 ColdFusion 上。每个平台都有自己的身份验证机制。
在接下来的章节中,我将阐述一种方法,该方法可以实现跨域身份验证,并同时将用户数据从一个站点传输到另一个站点。
基本程序逻辑
创建一个集中的身份验证应用程序,该应用程序包含一个 ASP.NET 应用程序和一个 ASP.NET 身份验证 Web 服务。用户始终被引导到此站点进行登录。成功身份验证后,用户将被重定向到一个第三方站点,在那里第三方应用程序调用身份验证 Web 服务来确认用户的登录状态并检索一组用户数据以更新第三方数据库。为此,用户通过一个格式化的登录链接来到身份验证站点。该链接包含两个查询字符串参数:SiteID
和 ReturnUrl
。它看起来像这样:
http://www.AuthenticationSite.com/login.aspx?SiteID=1&ReturnUrl=http://www.Third PartySite.net/ LandingPage.aspx?Para1=xxx&Para2=yyy
SiteID
是身份验证站点用于确定网页展示风格以及数据传输逻辑(使用哪个存储过程将数据传递给第三方站点)的参数。ReturnUrl
表示用户将被重定向到的第三方站点的登陆页面。Para1
和 Para2
是 ReturnUrl
的示例参数,第三方站点可以根据自身需要使用它们。ReturnUrl
应该进行 UrlEncoded()
处理,以便包括参数和任何保留字符(如“?”, “=”, “&”)在内的整个字符串被视为一个单独的参数,如下所示:
http://www.AuthenticationSite.com/login.aspx?SiteID=1&ReturnUrl= http%3a%2f%2fwww. ThirdPartySite.net%2fLandingPage.aspx%3fPara1%3dxxx%26Para2%3dyyy
根据第三方应用程序运行的平台,登陆页面可以使用任何适用的 Web 技术实现,包括 ASP.NET。身份验证站点从查询字符串获取 SiteID
和 ReturnUrl
,然后根据 SiteID
向用户展示一个具有与第三方站点相似图形界面的登录页面。用户输入用户名和密码,然后单击“登录”按钮。在凭据成功验证后,应用程序会将身份验证会话数据(包括 SiteID
、UserID
、身份验证 ExpirationDateTime
和 ReturnUrl
(如果需要,还可以添加更多))保存到数据库中,并生成一个唯一的 AuthID 来表示保存的数据,然后将 AuthID 追加到 ReturnUrl
的末尾。此时 ReturnUrl
将如下所示:
http://www.ThirdPartySite.net/LandingPage.aspx?Para1=xxx&Para2=yyyy&AuthID=RD69usAsVDrAf56Hdd8R
然后用户使用上述 ReturnUrl
被重定向到第三方站点的登陆页面。
在第三方站点,登陆页面从查询字符串中获取 URL 参数 **AuthID
**,然后调用身份验证 Web 服务。该 Web 服务检索之前保存的身份验证会话数据,并检查 ExpirationDateTime
。有效的 ExpirationDateTime
表示用户已通过身份验证。然后,Web 服务使用由 SiteID
确定的存储过程从数据库中检索一组用户数据,并将数据返回给第三方应用程序。登陆页面将用户数据插入/更新到自己的数据库中,并执行必要的操作以编程方式将用户登录到其系统中。
运行演示
为了说明跨域身份验证是如何工作的,我准备了一个演示应用程序供下载。该解决方案包含两个网站。一个是提供登录界面以验证用户以及供第三方使用的 Web 服务的 AuthenticationSite。另一个是 ThirdPartySite,它模拟了一个第三方站点。尽管这两个站点都在同一个解决方案中并在本地计算机上运行,但该方法适用于任何不同域/平台的站点。
要运行演示,您需要 SQL Server 2005 Express Edition(确保服务正在运行)。可能需要从 Visual Studio 2008 中的服务器资源管理器测试其连接。还需要 Microsoft Enterprise Library 进行数据访问。但是,DLL 已包含在演示中。
在 Visual Studio 2008 中打开解决方案。展开 ThirdPartySite 树,右键单击 LandingPage1.aspx,然后选择在浏览器中查看。页面加载后,单击“检查身份验证状态”按钮。您会看到用户此时未通过身份验证。关闭浏览器。此操作是为了启动 ThirdPartySite 的本地 Web 服务器,因为它是一个基于文件系统的应用程序。展开 AuthenticationSite 树。右键单击 Default.aspx 页面,然后选择在浏览器中查看。页面上现在显示了两个链接。查看这两个链接的 HTML 代码,您会发现一个链接的 ReturnUrl
指向第三方 #1 网站,另一个链接指向第三方 #2 网站。在演示中,它们实际上不是两个站点,而是 ThirdPartySite 应用程序中的两个页面:LandingPage1.aspx 和 LandingPage2.aspx。请检查链接中的端口号,并确保它是 ThirdPartySite 应用程序的正确端口号。
单击“第三方 #1 网站”链接,login.aspx 页面显示出来,带有 Site1 的标题图像和标题文本。输入用户名:johnd,密码:password(或者 janed/password,客户表中只有两个用户),然后单击“登录”按钮。您已通过身份验证,并被重定向到 ThirdPartySite 中的 LandingPage1.aspx。登陆页面调用 Web 服务并提取 John Doe 的数据,然后以编程方式将 John 登录到 ThirdPartySite。单击“检查身份验证状态”按钮。这次,您会看到用户已通过身份验证。如果更改 URL 中的“AuthID”或将页面打开超过一分钟然后刷新页面,您将看到一条消息表明 URL 已过期。重复上述步骤,使用 default.aspx 页面上的“第三方 #2 网站”链接,您将看到不同的标题图像和标题文本,并在身份验证后重定向到 LandingPage2.aspx。
身份验证站点应用程序
AuthenticationSite 应用程序包括一个 SQL Server 数据库、一个 ASP.NET 登录页面、一个主页、一个身份验证类、一个 SiteInfo
类和一个 Web 服务。让我们仔细看看其中的每一个。
数据库
CustomerDB 是应用程序的后端数据存储,一个 SQL 数据库。数据库中有三个表:Customer、SiteInfo 和 AuthenticationLog,以及六个存储过程。Customer 表存储用户联系信息,包括登录凭据。SiteInfo 表指定每个第三方站点的样式信息以及用于检索一组客户数据以进行数据传输的存储过程名称。AuthenticationLog 表存储每次用户在 AuthenticationSite 登录时的身份验证会话数据。这些数据将由第三方应用程序通过 Web 服务调用检索,以确认用户的身份验证状态。
六个存储过程在此处描述:
- Customer_Login - 验证用户凭据并返回
CustomerID
。如果需要,可以编辑此项返回更多字段。 - SiteInfo_GetSiteInfo - 根据
SiteID
检索样式信息,例如样式表名称、标题图像路径和标题文本,以及用于数据传输的存储过程名称。 - Site1_GetCustomerInfo - 为第三方 #1 站点检索完整的客户数据集。请注意,在此示例中,存储过程中只有一个
Select
语句。但是,可以根据业务需求添加更多语句(带有Join
和条件)。多个Select
语句将在DataSet
中返回多个表。 - Site2_GetCustomerInfo - 为第三方 #2 站点检索与上述类似的客户数据。
- AuthencationLog_insert – 成功身份验证后,将身份验证会话数据保存到 AuthenticationLog 表中。这包括
SiteID
、UserID
、ExpirationDateTime
和ReturnUrl
。 - AuthencationLog_Get – Web 服务使用此过程检索身份验证会话数据并确认用户身份验证状态。
类
AuthenticationSite 应用程序中有两个类:SiteInfo
和 Authentication
。
SiteInfo
类有助于根据 SiteID
设置登录页面的显示。此类很简单,无需解释。
身份验证类执行大部分身份验证工作。VerifyCredentials
方法执行用户凭据检查,并以 DataTable
的形式返回用户的身份。返回的字段数取决于存储过程 - Customer_Login。在我们的示例中,只返回 UserID
(CustomerID
)。WellFormReturnUrl
方法通过正确地将 AuthID
追加到原始 ReturnUrl
来创建一个格式正确的 ReturnUrl
。RetrieveUserDataSet
方法检索完整的用户数据集以传输到第三方。参数 siteID
决定使用哪个存储过程。每个方法中的代码如下所示。
public static DataTable VerifyCredentials(string userName, string password)
{
//confirm credentials. upone success, return a single
//record in a DataTable for this user
return ExecuteDataSet("Customer_Login",
new object[] { userName, password }).Tables[0];
}
//The return url to be used to send user back to a third
//party site needs to be parsed to add the AuthID properly
public static string WellFormReturnUrl(string originalReturnUrl,
string authID)
{
string WellFormedUrl = "";
//check if the original return url has parameters attached already.
//encryptedParameter has to be UrlEncoded.
int Position = originalReturnUrl.IndexOf("?");
if (Position != -1)
{
//? exists. original url has some parameters already,
// append the ecryptedParameter to the end with a "&"
WellFormedUrl = originalReturnUrl + "&AuthID=" +
HttpUtility.UrlEncode(authID);
}
else //original url does not have any parameters, append AuthID with "?"
{
WellFormedUrl = originalReturnUrl + "?AuthID=" +
HttpUtility.UrlEncode(authID);
}
return WellFormedUrl;
}
//this method retrieve a complete set of user data that a third party app needs
public static DataSet RetrieveUserDataSet(int siteID, string userID)
{
//siteID determines storedproc name.
DataSet ds = ExecuteDataSet(GetDataTransferProc(siteID), new object[] { userID });
return ds;
}
Authentication
类中的其余方法执行数据操作和与数据库的交互。在此,身份验证会话数据首先使用 BuildUserDataCollection
方法放入一个名称值集合中。这样可以轻松地从身份验证会话数据中检索任何名称值对。然后通过调用 SerializeParameters
方法将数据序列化为单个字符串,并将其保存到 AuthenticationLog 表中。通过使用序列化,可以在不修改表结构的情况下添加更多数据对(如果需要)。检索身份验证会话数据是一个反向操作,它通过调用 DeserializeUserData
来反序列化保存的数据字符串,从而创建一个用户数据集合。
主页模板
身份验证站点的图形界面样式通过主页 AuthMaster.master 实现。样式是根据 SiteID
动态应用的,以便身份验证站点看起来与目标第三方站点相似。在演示中,样式相对简单。但是,它确实说明了更复杂的图形显示的原理。
有两种样式表、两个标题图像和两个标题文本,分别与演示应用程序中的两个第三方站点(实际上是两个登陆页面)匹配。下面列出的代码是在 AuthMaster.master 页面中应用样式的函数。TableWidth
变量控制页面的显示宽度,它直接放置在主页的 HTML 表格标签中,因此需要调用 DataBind()
来绑定该变量。其余的是 ASP.NET 服务器控件(例如 imgHeader
和 lblHeaderText
)或 HTML 服务器控件(例如 MainStyle
)。它们的属性会根据 SiteID
改变。
private void ApplySiteStyle(SiteInfo siteInfo)
{
MainStyle.Href = siteInfo.StyleSheetName;
imgHeader.ImageUrl = siteInfo.HeaderImagePath;
imgHeader.Width = Unit.Pixel((int)siteInfo.HeaderWidth);
imgHeader.Height = Unit.Pixel((int)siteInfo.HeaderHeight);
lblHeaderText.Text = siteInfo.HeaderText;
TableWidth = (imgHeader.Width == 0 ? "800" :
imgHeader.Width.ToString());
DataBind();
}
登录页面
登录页面在用户身份验证过程中基本上执行三项操作:
- 调用
Authentication
类中的 VerifyCredentials 方法,根据数据库检查用户凭据,并以DataTable
的形式返回UserID
。此处使用DataTable
的原因是,通过修改 Customer_Login 存储过程,可以灵活地返回更多字段; - 序列化身份验证会话数据,包括
SiteID
、UserID
、ExpirationDateTime
和ReturnUrl
,然后生成 AuthID 并将信息保存到 AuthenticationLog 表中。ExpirationDateTime
设置为比执行日期时间晚 1 分钟,表示 AuthID 将在用户凭据验证后 1 分钟内过期; - 将 AuthID 追加到
ReturnUrl
的末尾,并将用户重定向到第三方站点。
在 Page_Load
事件中请求 SiteID
和 ReturnUrl
,并将它们的值赋给两个隐藏的标签 lblSiteID
和 lblReturnUrl
(Label.Visible=false
)。它们在单击登录按钮时使用。登录按钮单击事件中的代码是不言自明的,如下所示。
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
lblReturnUrl.Text = (Request.QueryString["ReturnUrl"]??"").ToString();
lblSiteID.Text = (Request.QueryString["SiteID"] ?? "").ToString();
if (lblReturnUrl.Text == ""||lblSiteID.Text=="")
{
lblError.Text = "The ReturnUrl or SiteID is missing. Can't proceed.";
btnLogin.Enabled = false;
return;
}
txtUserName.Focus();
//provide testing accounts for user to log in
lblError.Text = "Login credentials: user: johnd " +
"pw: password or user: janed pw: password";
}
}
protected void btnLogin_Click(object sender, EventArgs e)
{
try
{
DataTable tbl = Authentication.VerifyCredentials(txtUserName.Text,
txtPassword.Text);
if (tbl.Rows.Count>0)
{
//Authentication will expire in 1 minute. Can be set to other values
int MinutesToExpire = 1;
ProcessAuthenticationData(tbl, MinutesToExpire,
Convert.ToInt16(lblSiteID.Text), lblReturnUrl.Text);
}
else
{
lblError.Text = "No user with these credentials has been found.";
}
}
catch (Exception ex)
{
lblError.Text = ex.Message.ToString();
}
}
private void ProcessAuthenticationData(DataTable tbl,
int minutesToExpire, int siteID, string returnUrl)
{
//place user data into a collection which is easy to handle
NameValueCollection UserData = Authentication.BuildUserDataCollection(tbl,
minutesToExpire, siteID, returnUrl);
//build all data into a text string so that it can be stored in database
string UserString = Authentication.SerializeParameters(UserData);
//Save querystring parameter and user data string
string AuthenticationID = Guid.NewGuid().ToString().Replace("-", "");
//save user information. When the third party app calls web service,
// this information will be retrieved
Authentication.ExecuteNonQuery("AuthenticationLog_Insert",
new object[] { AuthenticationID, UserString});
Response.Redirect(Authentication.WellFormReturnUrl(lblReturnUrl.Text,
AuthenticationID));
}
Web服务
AuthenticationService.asmx 是一个 Web 服务,供第三方登陆页面调用,以确认用户的身份验证状态并传输用户数据。收到 AuthID(Request.QueryString[“AuthID”]
)后,第三方登陆页面将 AuthID 调用 RetrieveUserDataSet
Web 方法(或根据需要调用任何其他 Web 方法)来检索完整的用户数据集。在此过程中,如果 AuthID 已过期或被篡改,Web 服务调用将失败。
该 Web 服务提供了三个 Web 方法,如下所述:
RetrieveUserDataSet
– 传递 AuthID。成功时,以DataSet
的形式返回一组用户数据。失败时,返回null
,并在引用参数returnMessage
中发送错误消息。RetrieveUserDataXml
- 与上述功能相同,但以序列化的 XML 字符串形式返回一组用户数据。RetrieveUserID
- 传递 AuthID。成功时,以字符串形式返回UserID
。失败时,返回空字符串,并在引用参数returnMessage
中发送错误消息。如果您只需要确认用户的身份验证状态,而不需要传输用户数据,则调用此方法。
RetrieveUserDataSet
和 RetriveUserDataXml
Web 方法返回完全相同的数据。此处公开两种方法是为了兼容非 .NET 第三方应用程序,在这些应用程序中 DataSet
不是一种可识别的数据类型。同样,出于相同的原因,错误消息通过引用参数 ReturnMessage
发送回调用方,而不是抛出异常。根据第三方使用的平台,可能需要更多方法变体,以以特定第三方应用程序可以使用的正确格式返回数据。在演示中,只调用了 RetrieveUserDataSet
。此处列出了更多 Web 方法,以说明程序开发的灵活性。
第三方站点
ThirdPartySite 应用程序模拟了一个远程第三方站点,用户在成功身份验证后被重定向到该站点。如前所述,ThirdPartySite 上的登陆页面从 URL 接收 AuthID,然后调用身份验证 Web 服务,该服务返回完整的用户数据集。然后,第三方应用程序处理返回的数据并更新其数据库,然后以编程方式将用户登录到其站点。如果 Web 服务调用因 AuthID 过期或被篡改而失败,则会向第三方应用程序返回错误消息。作为演示,返回给第三方的用户数据不会保存到数据库中,而是显示在 GridView
中。
一个指向同一解决方案中 AuthenticationSite/AuthenticationService.asmx 的 Web 引用已添加到站点中,并命名为 AuthenticationService。该应用程序中有两个登陆页面,模拟了两个不同的第三方站点。LandingPage1.aspx 和 LandingPage2.aspx 中的代码在演示应用程序中完全相同。因此,我们只需要查看 LandingPage1.aspx。查看下面列出的代码,页面从查询字符串获取 AuthID,并请求 Parameter1
和 Parameter2
,这两个参数供第三方用于自身目的。在演示中,与 Parameter1
和 Parameter2
相关的任何代码都已省略。然后,页面声明 AuthenticationService
的实例并调用 RetrieveUserDataSet
Web 方法。如果返回 DataSet
,表示跨域身份验证成功,则此页面执行必要的操作来处理用户数据,然后以编程方式登录用户。否则,身份验证失败,并显示错误消息。
//request for the AuthenticationID
string AuthenticationID = Request.QueryString["AuthID"];
if (AuthenticationID == null)
{
lblError.Text = "A required parameter is missing from url. ";
return;
}
//Request p1 and p2 from Url. p1 and p2
//are the parameters that the third party app needs
string p1 = Request.QueryString["Para1"].ToString();
string p2 = Request.QueryString["Para1"].ToString();
//additional code here to process the parameters
//Add a web reference to your app and name it anything you like.
//Here it is named as AuthenticationService.
//declare web service and a reference variable - ReturnMessage
AuthenticationService.AuthenticationService AuthService =
new AuthenticationService.AuthenticationService();
string ReturnMessage = "";
DataSet ds = null;
//Call Web Method: RetrieveUserDataSet
//success: user authenticated, get a DataSet.
//Failure: user not authenticated or Url exipred. Return null and error message.
try
{
ds = AuthService.RetrieveUserDataSet(ref ReturnMessage, AuthenticationID);
}
catch (Exception ex)
{
lblError.Text += ex.Message.ToString();
}
if (ReturnMessage != "")
{
lblError.Text += ReturnMessage;
return;
}
if (ds != null)
{
//depending on the stored procedure used to retrieve the data
//The DataSet can contain multiple tables
//Write code here to loop through the DataSet
//insert or update the third party database
//and then programmatically log in the user
//by setting up appropriate cookies and session variables
//For asp.net form authentication, call SetAuthCookie method
FormsAuthentication.SetAuthCookie("LoginUser", false);
//display user data for demo purpose
gvUserData.DataSource = ds.Tables[0];
gvUserData.DataBind();
}
else
{
lblError.Text += ReturnMessage;
}
摘要
我已经讨论了跨域/跨平台身份验证和数据传输的方法。演示应用程序是一个大大简化的版本,旨在说明概念。在实际应用中,至少需要实现几个额外的部分,例如“记住我”、“忘记密码”、“注册”等。此外,Web 服务的可访问性应仅限于与您有合作关系的第三方。根据第三方使用的编程平台,可能需要更多 Web 方法的变体,以返回第三方可以处理的适当格式(例如,字符串数组或特殊字符(如“|”)分隔的字符串等)的数据。
本文基于我之前在 aspalliance.com 上发表的作品,跨站点身份验证和数据传输。此后,通过 URL 传递长加密数据字符串已不再使用,而是创建了一个数据库表来跟踪用户身份验证会话数据。这种方法降低了程序实现的复杂性,并最大限度地减少了与加密和长 URL 可能产生的副作用。