OpenId学习及DotNetOpenAuth初探

      最近有朋友向我咨询单点登录的相关问题,并多次提到了OAuth这个名词.本人不才,由于工作关系尚未有过相关经验.于是上网搜索相关资料并初步研究了在.net下单点登录的实际应用.略有微小心得,现记录如下

 

      1.什么是OpenId

      OpenId是一个分布式的身份管理系统,也叫做分散的单点登录平台。通过在多系统间减化登录过程来提高用户体验.

      参考:

      OpenID对决IT三巨头之识别系统

      OpenId:身份认证技术要革命?

 

      2.OpenId与OAuth的区别

      OAuth和OpenID的区别在于应用场景的区别,OAuth用于授权的,是一套授权(Authorization)协议;OpenID是用来认证的,是一套认证(Authentication)协议。很多人现在错误的把OAuth当做OpenID使用,但是其实也不会照成什么影响。

      参考:

      Difference Between oAuth and OpenID

      OpenID流程概述及其与OAuth的区别

      OAuth和OpenID的区别

 

      3.OpenId的一般认证过程

      A.用户希望访问其在example.com的账户
      B.example.com (在OpenID的黑话里面被称为“Relying Party”) 提示用户输入他/她/它的OpenID
      C.用户给出了他的OpenID,比如说"http://user.myopenid.com"
      D.example.com 跳转到了用户的OpenID提供商“mypopenid.com”
      E.用户在"myopenid.com"(OpenID provider)提示的界面上输入用户名密码登录
      F.“myopenid.com" (OpenID provider) 问用户是否要登录到example.com
      G.用户同意后,"myopenid.com" (OpenID provider) 跳转回example.com
      H.example.com 允许用户访问其帐号

      可以看到,所谓的OpenId,其实就是以Url的方式表达某个认证中心网站的某个用户.如上面的http://user.myopenid.com,首先它是一个网址,其次myopenid.com是认证中心网站地址,user是实际的用户名.

 

      4.OpenId在.Net下的应用

      DotNetOpenAuth是本技术在.Net下广泛应用的类库,书写本文时最新的版本为4.2.2.13055.下载后在其Sample文件夹下可以看到其一整套的示例程序.可以看到其不仅支持OpenId,还支持OAuth和InfoCard技术.

      本文通过分析认证过程来研究其在Asp.Net Mvc场景下的应用.主要涉及两个示例项目:OpenIdProviderMvc为服务提供者(即认证中心,以下简称P,地址为Http://192.168.0.217:85),OpenIdRelyingPartyMvc为服务使用者(以下简称A,地址为Http://192.168.0.217:87).

 

      A.获取OpenId

      使用OpenId的第一步是要获取自己的OpenId.如同一般的认证系统使用前需注册一样.但在实际使用中,也可只需输入认证网站的地址.如同上面的例子,输入http://192.168.0.217:85与http://192.168.0.217:85/user/bob(bob为用户名)都可进入认证过程.后面将会结合例子做具体的分析.

 

      B.服务发现

      即然A将认证过程交给P完成,则用户在登录A时,A系统会提示用户输入自己的OpenId.A系统提交代码如下:

 1 private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
 2 
 3 public ActionResult Authenticate(string returnUrl)
 4 {
 5     var response = openid.GetResponse();
 6     if (response == null)
 7     {
 8         // Stage 2: user submitting Identifier
 9         Identifier id;
10         if (Identifier.TryParse(Request.Form["openid_identifier"], out id))
11         {
12             try
13             {
14                 return openid.CreateRequest(Request.Form["openid_identifier"]).RedirectingResponse.AsActionResult();
15             }
16             catch (ProtocolException ex)
17             {
18                 ViewData["Message"] = ex.Message;
19                 return View("Login");
20             }
21         }
22         else
23         {
24             ViewData["Message"] = "Invalid identifier";
25             return View("Login");
26         }
27     }
28     else
29     {
30         // Stage 3: OpenID Provider sending assertion response
31         switch (response.Status)
32         {
33             case AuthenticationStatus.Authenticated:
34                 Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
35                 FormsAuthentication.SetAuthCookie(response.ClaimedIdentifier, false);
36                 if (!string.IsNullOrEmpty(returnUrl))
37                 {
38                     return Redirect(returnUrl);
39                 }
40                 else
41                 {
42                     return RedirectToAction("Index", "Home");
43                 }
44             case AuthenticationStatus.Canceled:
45                 ViewData["Message"] = "Canceled at provider";
46                 return View("Login");
47             case AuthenticationStatus.Failed:
48                 ViewData["Message"] = response.Exception.Message;
49                 return View("Login");
50         }
51     }
52     return new EmptyResult();
53 }

      代码第5行是获取P系统的响应.如果为空,如这里首次提交,便进入Stage 2.向用户输入的地址发起一个Http请求.如果不为空,则分析其状态码.33行是认证成功,则在本地设置Cookie,并跳回上一次使用的页面.

      这个OpenId不是随便输的.如果输入一个错误的地址,A系统则会提示No OpenID endpoint found.上面说过,这里可以输入两类地址,http://192.168.0.217:85与http://192.168.0.217:85/user/bob.下面就将目光转到服务端,来看看这两个地址后面究竟对应什么处理代码

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "User identities",
        "user/{id}/{action}",
        new { controller = "User", action = "Identity", id = string.Empty, anon = false });
    routes.MapRoute(
        "PPID identifiers",
        "anon",
        new { controller = "User", action = "Identity", id = string.Empty, anon = true });
    routes.MapRoute(
        "Default",                                              // Route name
        "{controller}/{action}/{id}",                           // URL with parameters
        new { controller = "Home", action = "Index", id = string.Empty }); // Parameter defaults
}

      可以看到,两个地址对应了两个不同的Controller.首先看看Default

public ActionResult Index()
{
    if (Request.AcceptTypes.Contains("application/xrds+xml"))
    {
        ViewData["OPIdentifier"] = true;
        return View("Xrds");
    }

    ViewData["Message"] = "Welcome to ASP.NET MVC!";
    return View();
}

public ActionResult Xrds()
{
    ViewData["OPIdentifier"] = true;
    return View();
}

      其实就是返回一个名为Xrds的View,再看看User identities

public ActionResult Identity(string id, bool anon)
{
    if (!anon)
    {
        var redirect = this.RedirectIfNotNormalizedRequestUri(id);
        if (redirect != null)
        {
            return redirect;
        }
    }

    if (Request.AcceptTypes != null && Request.AcceptTypes.Contains("application/xrds+xml"))
    {
        return View("Xrds");
    }

    if (!anon)
    {
        this.ViewData["username"] = id;
    }

    return View();
}

public ActionResult Xrds(string id)
{
    return View();
}

      这样就明白了,虽然使用了提供了两个认证地址,对应两个处理Controller,但其实做了同一件事,就是返回一个Xrds.下面就来看看它

 1 <%@ Page Language="C#" AutoEventWireup="true" ContentType="application/xrds+xml" %>
 2 <%@ OutputCache Duration="86400" VaryByParam="none" Location="Any" %><?xml version="1.0" encoding="UTF-8"?>
 3 <%--
 4 This XRDS view is used for both the OP identifier and the user identity pages.
 5 Only a couple of conditional checks are required to share the view, but sharing the view
 6 makes it very easy to ensure that all the Type URIs that this server supports are included
 7 for all XRDS discovery.
 8 --%>
 9 <xrds:XRDS
10     xmlns:xrds="xri://$xrds"
11     xmlns:openid="http://openid.net/xmlns/1.0"
12     xmlns="xri://$xrd*($v*2.0)">
13     <XRD>
14         <Service priority="10">
15 <% if (ViewData["OPIdentifier"] != null) { %>
16             <Type>http://specs.openid.net/auth/2.0/server</Type>
17 <% } else { %>
18             <Type>http://specs.openid.net/auth/2.0/signon</Type>
19 <% } %>
20             <Type>http://openid.net/extensions/sreg/1.1</Type>
21             <Type>http://axschema.org/contact/email</Type>
22             
23             <%--
24             Add these types when and if the Provider supports the respective aspects of the UI extension.
25             <Type>http://specs.openid.net/extensions/ui/1.0/mode/popup</Type>
26             <Type>http://specs.openid.net/extensions/ui/1.0/lang-pref</Type>
27             <Type>http://specs.openid.net/extensions/ui/1.0/icon</Type>--%>
28             <URI><%=new Uri(Request.Url, Response.ApplyAppPathModifier("~/OpenId/Provider"))%></URI>
29         </Service>
30 <% if (ViewData["OPIdentifier"] == null) { %>
31         <Service priority="20">
32             <Type>http://openid.net/signon/1.0</Type>
33             <Type>http://openid.net/extensions/sreg/1.1</Type>
34             <Type>http://axschema.org/contact/email</Type>
35             <URI><%=new Uri(Request.Url, Response.ApplyAppPathModifier("~/OpenId/Provider"))%></URI>
36         </Service>
37 <% } %>
38     </XRD>
39 </xrds:XRDS>

      里面含有少许逻辑,简单讲,就是如果输入的地址不带用户,则使用第16行,然后不使用30到37行,反之则使用18行,也使用30到37行.

      OpenId使用Yadis协议.如同Wsdl是WebServices的描述文档一样,Xrds是此协议的描述文档.Tpye元素描述技术版本,如http://specs.openid.net/auth/2.0/server表示使用OpenId2.0版本,Uri元素描述实际的认证地址:http://192.168.0.217:85/OpenId/Provider

      参考:

      Yadis

      Yadis标准

      XRDS

      在OpenId2.0之前的版本(1.0,1.1),用户在A系统只能通过http://192.168.0.217:85/user/bob进行登录,.在2.0之后的版本中可以直接使用http://192.168.0.217:85,即认证中心地址进行登录.这种能力官方名称为:OP-driven identifier selection.只需输入认证中心地址,然后去认证中心进行用户与密码的输入.

      这个改进给我最大的感觉是在A中输入方便了一点,但在参考文中却还提到了能解决标识难以记忆的问题,标准文档中"选择"的概念在这里也没有体现出来.估计我还是经验不足(其实是完全没有经验).其实在使用中,使用这两个地址登录还是有微小区别:使用http://192.168.0.217:85,则跳转到85后你可以使用任意用户登录,而使用http://192.168.0.217:85/user/bob,则跳转后只能使用bob登录.

      参考:

      Users vs. Identity Providers in OpenID

      Directed Identity vs Identifier Select

      小结一下,当用户在A系统输入http://192.168.0.217:85或http://192.168.0.217:85/user/bob并提交后,A系统访问P系统并获取了Xrds,然后跟据里面的内容将浏览器跳转到http://192.168.0.217:85/OpenId/Provider

      A系统也是有Xrds的,通过在首页加上一个X-XRDS-Location头来发布自己Xrds路径

public ActionResult Index()
{
    Response.AppendHeader(
        "X-XRDS-Location",
        new Uri(Request.Url, Response.ApplyAppPathModifier("~/Home/xrds")).AbsoluteUri);
    return View("Index");
}

public ActionResult Xrds()
{
    return View("Xrds");
}

      A系统Xrds文档如下

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" ContentType="application/xrds+xml" %><?xml version="1.0" encoding="UTF-8"?>
<%--
This page is a required for relying party discovery per OpenID 2.0.
It allows Providers to call back to the relying party site to confirm the
identity that it is claiming in the realm and return_to URLs.
This page should be pointed to by the 'realm' home page, which in this sample
is default.aspx.
--%>
<xrds:XRDS
    xmlns:xrds="xri://$xrds"
    xmlns:openid="http://openid.net/xmlns/1.0"
    xmlns="xri://$xrd*($v*2.0)">
    <XRD>
        <Service priority="1">
            <Type>http://specs.openid.net/auth/2.0/return_to</Type>
            <%-- Every page with an OpenID login should be listed here. --%>
            <%-- We use the Authenticate action instead of Login because Authenticate
                 is the action that receives OpenId assertions. --%>
            <URI><%=new Uri(Request.Url, Response.ApplyAppPathModifier("~/user/authenticate"))%></URI>
        </Service>
    </XRD>
</xrds:XRDS>

      意思很明白,就是告诉P系统当验证成功后返回A系统的http://192.168.0.217:87/User/Authenticate

      另外,Xrds是通过修改文档头来完成自我标识,如同将Json数据的文档标识修改为application/json一样,当A系统向P系统访问时会将标识修改为application/xrds+xml.

      参考:

      DotNetOpenAuth and X-XRDS-Location header

      OpenID登录之去掉yahoo的欺诈警告

 

      C.用户验证

      接上所述,当浏览器跳转到http://192.168.0.217:85/OpenId/Provider后,执行的是OpenId类的Provider方法,代码如下,有删节

IRequest request = OpenIdProvider.GetRequest();
if (request != null)
{
    // Some requests are automatically handled by DotNetOpenAuth.  If this is one, go ahead and let it go.
    if (request.IsResponseReady)
    {
        return OpenIdProvider.PrepareResponse(request).AsActionResult();
    }
return this.ProcessAuthRequest();
}

      如果当前是未验证状态,则执行ProcessAuthRequest方法,如下,有删节

// Try responding immediately if possible.
ActionResult response;
if (this.AutoRespondIfPossible(out response))
{
    return response;
}

// We can't respond immediately with a positive result.  But if we still have to respond immediately...
if (ProviderEndpoint.PendingRequest.Immediate)
{
    // We can't stop to prompt the user -- we must just return a negative response.
    return this.SendAssertion();
}

return this.RedirectToAction("AskUser");

      验证模式分为两种,交互式验证表示有显式的登录页面,有诸如用户手动输入用户名密码等交互行为,反之就是即时验证,所需信息都被包含在请求中,系统处理后立即返回结果.上面的代码中两个If就是尝试处理即时请求.否则跳转到AskUser方法.

      注意,这里使用了RedirectToAction进行跳转,也就是说,当执行到这里时,浏览器会再次跳转.此方法签名如下

[Authorize]
public ActionResult AskUser()

      可以看到,此方法需用户登录后才能执行.于是浏览器会再次跳转.这是第三次跳转了.下面为Account类的LogOn方法

if (!this.ValidateLogOn(userName, password))
{
    return View();
}

this.FormsAuth.SignIn(userName, rememberMe);
if (!string.IsNullOrEmpty(returnUrl))
{
    return Redirect(returnUrl);
}
else
{
    return RedirectToAction("Index", "Home");
}

      ValidateLogOn就是验证用户名密码处,本例使用的Membership,实际应用中会进行替换.

      FormsAuth.SignIn是将用户登录状态持久到Cookie中,代码如下

public void SignIn(string userName, bool createPersistentCookie)
{
    FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
}

      可以看到,其就是FormsAuthentication类的简单封装.

      成功后,浏览器将页面再次跳转到AskUser处.这是第四次跳转.代码如下,有删节

if (!ProviderEndpoint.PendingAuthenticationRequest.IsDirectedIdentity &&
    !this.UserControlsIdentifier(ProviderEndpoint.PendingAuthenticationRequest))
{
    return this.Redirect(this.Url.Action("LogOn", "Account", new { returnUrl = this.Request.Url }));
}

this.ViewData["Realm"] = ProviderEndpoint.PendingRequest.Realm;

return this.View();

      这里就是上文所提到的两种登录地址使用区别的实现.不带用户名的地址,IsDirectedIdentity属性为True,则将跳过此If执行.

      本View将会提示用户是否登录A系统.如果点击确定,则接下来会相继执行AskUserResponse方法与SendAssertion方法.其基本就是构建OpenId的各个返回值,最终浏览器会再次跳转至A系统提交处,即Authenticate方法处进行再次验证.此方法说明上文已表,恕不赘述.

      这里有个细节问题.在P系统中经过多次跳转后,在没有附带相关信息的前提下,如何能跳转回A系统正确的地址呢?答案是在A系统首次跳转P系统时就将地址附带了过来,存储中用户的信息区中.

 

      D.其它需要注意的问题

      a.关于Ssl安全

      OpenId支持Ssl传输协议.在web.config中将其配置项打开即可,貌似还有一个与之相关的类:AnonymousIdentifierProvider,在Global.asax中也对其进行了配置

protected void Application_BeginRequest(object sender, EventArgs e)
{
    InitializeBehaviors();
}

private static void InitializeBehaviors()
{
    if (DotNetOpenAuth.OpenId.Provider.Behaviors.PpidGeneration.PpidIdentifierProvider == null)
    {
        lock (behaviorInitializationSyncObject)
        {
            if (DotNetOpenAuth.OpenId.Provider.Behaviors.PpidGeneration.PpidIdentifierProvider == null)
            {
                DotNetOpenAuth.OpenId.Provider.Behaviors.PpidGeneration.PpidIdentifierProvider = new Code.AnonymousIdentifierProvider();
                DotNetOpenAuth.OpenId.Provider.Behaviors.GsaIcamProfile.PpidIdentifierProvider = new Code.AnonymousIdentifierProvider();
            }
        }
    }
}

      我只捣鼓了几分钟,没有将其Ssl功能测试成功,放弃了.

 

      b.相关信息存储模式

      ProviderEndpoint对象是本框架的关键对象之一,是P系统与A系统的关联对象.按道理每个用户登录都有其自身的数据,应该都拥有一个此对象的实例.但此对象大部分属性与方法却是静态的.读了注释才明白,虽然是静态的,但其取出的数据仍与具体用户相关.每个用户自己的资料都保存在Session中.

 

      E.个人总结

      其实跟据上面的学习,我个人感觉OpenId也是比较好理解的,其都是基于Cookie的认证模式.对于上面的中心P与网站A,

      a.如果A已登录,则访问A时正常运行.

      b.如果A未登录P已登录,登录A时跳转至P并询问是否允许在A上登录,如果是,则在A上登录.

      c.如果A未登录P也未登录,登录A时跳转到P并要求填入相关信息.然后询问是否允许在A上登录,如果是,则在A上登录.

      其实使用Cookie是只实现OpenId众多方式中的一种,但是一来网站上主要使用基于Cookie的验证,二来这种方式使用也最广泛,就没有再去研究其它小众了.

      在现实使用中一般都会面对现有系统的改造,认证与授权的联合使用等问题.这个就只能修改相应实现,具体问题具体分析了.或者以后再继续研究OAuth

 

      PS:以上纯属一家之言,如果不对的地方还请多多包涵,多多指教

 

      参考的文章:

      OpenID百度百科

      OpenID认证2.0——最终版(中文)

      OpenID 中文

      ASP.NET MVC 3 使用 DotNetOpenAuth 实现SSO

      如何在ASP.NET中创建OpenID

      基于DotNetOpenAuth实现OpenID 服务提供者

posted @ 2013-03-18 21:39  永远的阿哲  阅读(5891)  评论(5编辑  收藏  举报