Step1帳戶登錄系統

0.整體思路

我一直想做一個帳戶登錄系統,到今天,終於做出了一個雛形,非常高興,因此,我會在下面的幾篇文章對這個系統進行詳細的介紹,這是第一篇,介紹一下整體思路,到後面,基本上就是以代碼爲主了,先看個截圖:

Step1帳戶登錄系統

        這個系統起源於單點登錄系統(實際上它本身也是一個單點登錄系統),這個技術現在在互聯網上使用的十分廣泛了,畢竟,無論是大的還是小的網站,是否有多個域名,都可太可能爲每個欄目來設計一個單獨的登錄系統,等不可能讓用戶在每個欄目都去輸入帳號密碼,下面我粗略的圖解一下單點登錄系統(這篇文章之中提到的登錄都應該兼容跨域名之間的接口的,如果在同一個域名內,將會容易得多)

        以上這個圖只是一個登錄流程的順序圖,按照這個圖,我們一步一步看一下:
        1.用戶要求進行某一操作;
        2.Web欄目通過自身的Cookie或Session判斷用戶是否已經登錄,如果已經登錄,則直接處理,否則,將用戶導向到登錄系統,並附帶上參數通知登錄系統在登錄完成之後返回到哪一個頁面;
        3.登錄系統通過Cookie或Session判斷用戶是否已經登錄,如果已經登錄,則直接將用戶跳轉回欄目,並附帶用戶的帳戶信息,如果沒有登錄,則提示用戶登錄;
        4.用戶輸入帳戶密碼;
        5.登錄系統記錄判斷用戶登錄是否正常,然後先設置自己的Cookie或Session,再使用用戶的帳戶信息跳轉到對應的欄目;
        6.欄目收到跳轉回來的請求之後,先驗證請求,然後設置自己的Cookie,再根據用戶的帳戶信息向用戶返回內容。
        本來這些文字描述都可以直接直觀的畫在圖上的,不過我的發現機器上沒有工具,連Word都沒有,只好隨便用畫圖畫了一幅,不過因爲大家對單點登錄應該都很瞭解,因此,肯定會跳過不看,也就無所謂了。

        我並不是要做一個單點登錄系統,我主要是想,現在的網站,無論大小,都有一個註冊、登錄什麼的,對用戶造成很大的困擾,用戶不可能去記那麼多帳號密碼的,對網站來講,維護用戶的數據也是一個很難的問題,因此,如果網站能夠相互公用用戶信息,讓用戶的一個密碼在多個網站都可以登錄,這是一個皆大歡喜的事情。這個思路,實際上就是傳說中的OpenId,我之所以沒有去專門研究OpenId,是因爲現在國內知道的人很少,而且OpenId還是有一定的侷限性,我仔細的研究了各個大的網站提供的對外接口(國內的很少,基本都是國外的),決定寫一個基於多種網站用戶來源的單點登錄系統,這就是剛纔看到的Step1帳戶登錄系統.

        其實,總體原理非常簡單(因爲上面的那幅圖畫起來很費勁,這裏就不想再畫圖了),只要想象上圖之中的Passport服務器同時又是另外一個或多個其他Passport服務器的客戶端,就會覺得一切都容易起來:這個Passport服務器先作爲Google,Live,Yahoo等網站的帳戶服務的客戶端,將這個服務都整合在一起,然後作爲一個統一的Passport提供給自身的一個或多個網站的欄目。

        例如,參照如下流程:
        1.用戶打開http://www.dituren.cn/ ,並點擊右上角的“登錄”;
        2.頁面會轉向到http://account.step1.cn/account/login.aspx ,開始進行登錄,也就是文章開頭看到的那張圖片;
        3.在頁面上顯示了多種帳戶來源類型,用戶選擇自己有帳號的一種(以Google爲例);
4.帳戶服務器會將用戶轉向到Google的登錄頁面(此接口由Google Accounts Authentication 提供);
        5.Google會首先讓用戶輸入帳號密碼,然後出現一個頁面提示用戶第三方網站正在請求帳戶信息,請用戶確認;
        6.用戶確認後,則Google會返回到一個頁面,這個頁面URL是在第4步的時候隨參數發送給Google的;
        7.在這個頁面上接收Google傳遞的參數,寫入到Cookie,然後將用戶再次轉向到最終欄目頁面;
        8.欄目寫入Cookie,整個登錄過程完成。

        這個過程所起來比單點登錄難不了多少,不過最大的問題在於這些網站提供的帳戶服務互不相同,而且文檔並不是很健全,目前研究這個服務的人又不多(國內更加少了),因此開發的時候不停的遇到很多很鬱悶的問題(至今我都沒有使用Google的OAuth成功登錄,後來只好用AuthSub),因爲不停的遇到一些小問題而且沒有代碼參考,心情十分鬱悶,在最鬱悶的時候,我決定將這所有的代碼都整個公開提供下載,以減少還有別的同樣對這個感興趣的人研究的時候走的彎路。
        我會首先順序次在博客上貼出每一個網站的帳戶接口的詳細使用和代碼,等到這個服務完善之後,就將整個源碼提供下載。

        預覽地址是http://account.step1.cn/account/login.aspx ,目前就能登錄,登錄之後什麼都不能做,呵呵!

1.程序結構

上次粗略的講解了Step1帳戶登錄系統的思路之後,這一次我將介紹一下我的程序實現的結構,從這篇文章之後,就將細化到每一個接口,例如Google,Yahoo,Live等,對這些接口的使用進行詳細的介紹。

先看看文件結構,說起來很簡單因爲一共只有以下三個文件:

1.Login.aspx,最重要的頁面,在用戶登錄時讓用戶選擇採用哪種帳號登錄,用戶選擇對應的帳號之後,將用戶轉向到對應的網址;

2.Logout.aspx,將用戶註銷,並轉向到原來的URL;

3.Handler.aspx,接收帳戶服務器(例如Google的服務器)回發的登陸請求,根據回傳的資料請求用戶的信息,設置到Cookie並將用戶轉向到原先請求的網址;

再看看配置,爲便於擴展和開發,我採用了在Web.config文件之中建立XML格式的配置的方法,我的網站目前的配置如下(其中的***是本站的私有Key,因此被我屏蔽了):

Web.Config配置

1
2
3
4
5
6
7
8
9

簡單的介紹一下配置項的內容,根節點AccountServerConfiguration表明這是一個帳戶登錄系統配置的內容,而它的幾個屬性是登錄系統的全局配置,cookieName代表寫入到Cookie的鍵名稱,cookieDomain是寫入Cookie的域,paramName是向客戶Web欄目回傳登錄信息的參數名稱,rootUrl是整個程序的部署URL。

AccountServers節點下的每一個AccountServer代表一個種登錄類型服務器,屬性name是該類型的唯一標示,屬性type是該類型對應的實現類的名稱,其他所有的屬性都是對該類型的配置參數,例如對於Yahoo 的BBAuth,就必須配置在Yahoo登記的appid等。

瞭解文件結構和配置結構之後,再對照一下類的設計結構:

Step1帳戶登錄系統(1.程序結構)

上圖之中的LoginPage.cs,LogoutPage.cs,HandlePage.cs分別代表上面提到的3個頁面,Configuration.cs是用來讀取上圖提到的XML配置的類,AccountHelper.cs包含一些靜態方法,例如讀寫用戶Cookie等,其中最重要的是BaseServer,以及AccountServers目錄下的那些類。

AccountServers都繼承BaseServer,每一個類都代表一種登陸類型,例如OAuth,OpenID,Google的AuthSub等(其中的有些我還沒有完全實現),因此添加新的類型支持,只需要在AccountServers文件夾之中添加一個對BaseServer的繼承即可。

Tools文件夾包含一些附加的工具類,例如對WebService的訪問等等。

因爲我設計的時候,並沒有從開始就考慮使用一個非常結構化的模式來設計,因此這個結構設計也顯得比較簡單,不過我個人比較喜歡這種簡單明瞭的設計。本來計劃這篇文章會包含代碼的,可是發現現在已經很長了,只好在下一篇再貼上上面的一些重要代碼

2.基礎代碼

在前面的文章之中,我介紹了一下Step1帳戶登錄系統的基本編程架構,而在這篇文章,將直接貼出相關的源碼,由於這次的開發比較倉促,代碼存在很多不完善的地方,因此,適合僅僅用來作爲如何實現的代碼,而不是適合直接使用,廢話少說,直接看代碼:

首先是登錄頁面的代碼,代碼分爲代碼文件和頁面文件兩個部分:

Login.aspx代碼文件

1 public partial class LoginPage : System.Web.UI.Page
2 {
3 public string url,returnUrl=null;
4 public System.Collections.Specialized.NameValueCollection userInfo=null;
5 protected void Page_Load(object sender, EventArgs e)
6 {
7 url = Request.QueryString["url"];//獲得登錄完成後迴轉的URL
8 if (url == null || url.Length <= 0)
9 {
10 if (Request.UrlReferrer != null)
11 {
12 url = Request.UrlReferrer.ToString();
13 }
14 }
15 userInfo=AccountHelper.getUserInfo();//獲得當前已經登錄的用戶信息
16 if (userInfo != null && url != null && url.Length > 1)
17 {
18 returnUrl = AccountHelper.getReturnUrl(url);//如果已經登陸,則直接將回轉地址顯示在頁面的連接上
19 }
20 BaseServer server = AccountHelper.getServerByName(Request["ass"]);//如果通過ass參數指定了登錄的類型(用戶已經點擊圖標登錄)
21 if (server != null)
22 {
23 if (url != null && url.Length > 0)
24 {
25 AccountHelper.saveUrl(url);//將登錄迴轉的地址記錄到Cookie
26 }
27 Response.Redirect(server.getLoginUrl(),true);//轉向到相應的登錄頁面
28 }
29 }
30 }

下面是登錄頁面的頁面文件,實際上就是顯示登錄界面的HTML內容:

Login.aspx頁面文件

1<%@ Page Language="C#" AutoEventWireup="true" Inherits="Step1.AccountServer.LoginPage"%>
2
3
4
5
6


8 live.gif
9

10 google.gif
11

12 yahoo.gif
13

14 xiaonei.gif
15

17 Live,MSN,Hotmail用戶
18

19 Google,Gmail用戶
20

21 Yahoo,Flickr用戶
22

23 校內網用戶(即將推出)
24

26<% if(userInfo!=null){%>
27

28 <%=userInfo["name"]%>,您好!您已經使用 <%=userInfo["type"]%>賬號登錄
29 <%if(returnUrl!=null) {%>點擊返回<%} %>
30

31<%}%>
32
說明:

33
34

上可以看出,服務端支持哪幾種登錄方式和登錄界面完全沒有關係,前臺登錄界面並不是自動生成的。

然後是註銷的Logout.aspx代碼,註銷的代碼因爲沒有界面,因此沒有頁面文件(空文件),僅僅有一個代碼文件:

Logout.aspx代碼

1 public partial class LogoutPage : System.Web.UI.Page
2 {
3 protected void Page_Load(object sender, EventArgs e)
4 {
5 //退出系統,如果URL不存在,則返回登錄頁
6 string url = Request.QueryString["url"];
7 if (url == null || url.Length <= 0)
8 {
9 url = Request.UrlReferrer.ToString();
10 }
11 AccountHelper.clearCookie();//清除用戶的Cookie
12 AccountHelper.redirect(url);
13 }
14 }

註銷的代碼要簡單得多,不過不是最簡單的,最簡單的是Handler.aspx,這個文件之所以簡單是因爲同樣沒有頁面文件,再加上其中的代碼文件所做的事情都已經調用其它的類來完成,因此只有一個非常簡單的代碼文件:

Handler.aspx代碼文件

1public partial class HandlePage : System.Web.UI.Page
2{
3 protected void Page_Load(object sender, EventArgs e)
4 {
5 BaseServer server = AccountHelper.getServerByName(Request["ass"]);//根據ASS參數找到對應的服務器類型對象
6 server.parseHandle(this.Context);//由該對象來處理返回請求
7 }
8});

在上面的三個aspx文件的代碼之中,無一例外的調用了AccountHelper類,這個類包含一些重要的靜態方法,內容如下:

AccountHelper.cs

1 public class AccountHelper
2 {
3 //清除Cookie
4 public static void clearCookie()
5 {
6 HttpCookie loginCookie = new HttpCookie(Configuration.Instance().cookieName);
7 loginCookie.Expires = DateTime.Now.AddYears(-10);
8 loginCookie.Domain = Configuration.Instance().cookieDomain;
9 loginCookie.HttpOnly = false;
10 HttpContext.Current.Response.Cookies.Add(loginCookie);
11 }
12 //將用戶信息加入到Cookie
13 public static void setUserInfo(string account, string name, string type)
14 {
15 HttpCookie loginCookie = new HttpCookie(Configuration.Instance().cookieName);
16 loginCookie.Domain = Configuration.Instance().cookieDomain;
17 loginCookie.Values.Add("account", Convert.ToBase64String(Encoding.UTF8.GetBytes(account)));
18 loginCookie.Values.Add("name", Convert.ToBase64String(Encoding.UTF8.GetBytes(name)));
19 loginCookie.Values.Add("type", Convert.ToBase64String(Encoding.UTF8.GetBytes(type)));
20 loginCookie.Expires = DateTime.Now.AddYears(1);
21 HttpContext.Current.Response.Cookies.Add(loginCookie);
22 }
23 //從Cookie之中獲取用戶信息
24 public static NameValueCollection getUserInfo()
25 {
26 HttpCookie cookie = HttpContext.Current.Request.Cookies[Configuration.Instance().cookieName];
27 if (cookie != null)
28 {
29 NameValueCollection userInfo = new NameValueCollection();
30 foreach (string key in cookie.Values)
31 {
32 userInfo.Add(key,Encoding.UTF8.GetString(Convert.FromBase64String(cookie.Values[key])));
33 }
34 return userInfo.HasKeys()?userInfo:null;
35 }
36 return null;
37 }
38 //保存URL以便在完成後轉向
39 public static void saveUrl(string url)
40 {
41 HttpCookie loginCookie = new HttpCookie(Configuration.Instance().cookieName + "_url");
42 loginCookie.Value = url;
43 loginCookie.Domain=Configuration.Instance().cookieDomain;
44 loginCookie.Expires = DateTime.Now.AddDays(1);
45 HttpContext.Current.Response.Cookies.Add(loginCookie);
46 }
47 //將用戶的登錄參數加入到URL並回轉給Web應用
48 public static void returnOpener()
49 {
50 HttpCookie urlCookie = HttpContext.Current.Request.Cookies[Configuration.Instance().cookieName + "_url"];
51 string url = getReturnUrl(urlCookie == null ? "./" : urlCookie.Value);
52 urlCookie.Expires = DateTime.Now.AddYears(-10);
53 HttpContext.Current.Response.Cookies.Add(urlCookie);
54 redirect(url);
55 }
56 //根據已登陸用戶信息和迴轉的基礎URL地址得到迴轉的URL
57 public static string getReturnUrl(string url)
58 {
59 url += (url.IndexOf("?") > 0) ? "&" : "?";
60 System.Collections.Specialized.NameValueCollection userInfo = HttpContext.Current.Request.Cookies[Configuration.Instance().cookieName].Values;
61 string[] infoArr = new string[userInfo.AllKeys.Length];
62 userInfo.CopyTo(infoArr, 0);
63 url += Configuration.Instance().paramName + "=" + HttpContext.Current.Server.UrlEncode(string.Join(",", userInfo.AllKeys) + ";" + string.Join(",", infoArr));
64 return url;
65 }
66 //轉向到指定URL,否則轉向到登錄頁
67 public static void redirect(string url)
68 {
69 if (url == null || url.Length <= 0)
70 {
71 HttpContext.Current.Response.Redirect("Login.aspx");
72 }
73 else
74 {
75 HttpContext.Current.Response.Redirect(url);
76 }
77 }
78 //根據帳戶類型的名稱返回對應的帳戶服務對象
79 public static BaseServer getServerByName(string name)
80 {
81 if (name == null || name.Length < 1) { return null; }
82 BaseServer[] servers=Configuration.Instance().AccountServers;
83 for (int i = 0; i < servers.Length; i++)
84 {
85 if (servers[i].name == name)
86 {
87 return servers[i];
88 }
89 }
90 return null;
91 }
92 public static string getHandleUrl()
93 {
94 return Configuration.Instance().rootUrl + "Handler.aspx";
95 }
96 public static string getLoginUrl()
97 {
98 return Configuration.Instance().rootUrl + "Login.aspx";
99 }
100 }

上面的代碼就是比較長的一段了,不過確實是比較重要的一些方法,而下面的BaseServer.cs是一個抽象類,所有的登錄類型(例如Google AuthSub類型,Yahoo BBAuth類型)都繼承自此類,此類本身不能實例化:

BaseServer.cs

1 public abstract class BaseServer
2 {
3 public string name;
4 public BaseServer(System.Xml.XmlNode node)
5 {
6 for (int i = 0; i < node.Attributes.Count; i++)
7 {
8 switch (node.Attributes[i].LocalName)
9 {
10 case "name":
11 name = node.Attributes[i].Value;
12 break;
13 }
14 }
15 }
16 public virtual string getLoginUrl()
17 {
18 return "";
19 }
20 public virtual void parseHandle(HttpContext page)
21 {
22 }
23 public virtual string getHandleUrl()
24 {
25 return AccountHelper.getHandleUrl()+"?ass=" + name;
26 }
27 }

到這裏,這些基礎的類就都介紹完畢,我之所以不厭其煩的將這些代碼都貼上來,主要是爲後面介紹每一種登錄類型的時候,能夠比較清晰的看出是登錄過程如何實現的,上面的代碼都比較簡單,因爲都是本站自己的邏輯,但是當涉及到和Google、Yahoo等的帳戶服務器交互的時候,很多時候必須完全按照對應的接口來做,因此會比較難懂

3.使用Google的Auth Sub登錄網站

前面的文章之中,我介紹了Step1帳戶登錄系統的基本實現架構和代碼,從這一篇開始,我開始逐次講解各種帳戶的登錄過程,在文章的最後,我都會貼出相應的代碼,在代碼之中有很多調用了前一篇文章之中講到的基礎代碼,因此,可能需要對照才能明白。

先從Google的AuthSub開始講起,爲什麼呢,因爲AuthSub最簡單,實現起來很容易,記得我開始焦頭爛額的研究了一個多星期Google的OAuth,始終沒有成功,可是當我改成調用AuthSub之後,幾個小時就實現了整個登錄過程。

有關Google AuthSub的文檔和相關的內容,請參考:AuthSub Authentication for Web Applications

廢話少說,現直接看一個Google的AuthSub登陸過程介紹圖片:

2008126103941491.png

1.生成用來轉向給Google認證系統的網址並轉向(圖中1);

2.接受從Google認證系統迴轉的頁面參數(圖中4)

3.使用Google的返回的參數向Google請求用戶信息(圖中5和6)

現在,讓我們來按照順序完成以上流程:

首先,你的程序必須在Google註冊才能調用此接口,因此先到Google的Manage your domains 頁面註冊你的程序,註冊界面如下:

2008126103941414.jpg

1."Target URL path prefix"只是個前綴而已,並不是完整的迴轉的路徑,在生成登錄的時候,還是要將回轉的路徑發送給Google,而且必須和這裏註冊的前綴相符,這應該算是用來保障安全的一個設置。

前面的文章之中,我介紹了Step1帳戶登錄系統的基本實現架構和代碼,從這一篇開始,我開始逐次講解各種帳戶的登錄過程,在文章的最後,我都會貼出相應的代碼,在代碼之中有很多調用了前一篇文章之中講到的基礎代碼,因此,可能需要對照才能明白。

先從Google的AuthSub開始講起,爲什麼呢,因爲AuthSub最簡單,實現起來很容易,記得我開始焦頭爛額的研究了一個多星期Google的OAuth,始終沒有成功,可是當我改成調用AuthSub之後,幾個小時就實現了整個登錄過程。

有關Google AuthSub的文檔和相關的內容,請參考:AuthSub Authentication for Web Applications

廢話少說,現直接看一個Google的AuthSub登陸過程介紹圖片:

2008126103941491.png

1.生成用來轉向給Google認證系統的網址並轉向(圖中1);

2.接受從Google認證系統迴轉的頁面參數(圖中4)

3.使用Google的返回的參數向Google請求用戶信息(圖中5和6)

現在,讓我們來按照順序完成以上流程:

首先,你的程序必須在Google註冊才能調用此接口,因此先到Google的Manage your domains 頁面註冊你的程序,註冊界面如下:

2008126103941414.jpg

1."Target URL path prefix"只是個前綴而已,並不是完整的迴轉的路徑,在生成登錄的時候,還是要將回轉的路徑發送給Google,而且必須和這裏註冊的前綴相符,這應該算是用來保障安全的一個設置。

前面的文章之中,我介紹了Step1帳戶登錄系統的基本實現架構和代碼,從這一篇開始,我開始逐次講解各種帳戶的登錄過程,在文章的最後,我都會貼出相應的代碼,在代碼之中有很多調用了前一篇文章之中講到的基礎代碼,因此,可能需要對照才能明白。

先從Google的AuthSub開始講起,爲什麼呢,因爲AuthSub最簡單,實現起來很容易,記得我開始焦頭爛額的研究了一個多星期Google的OAuth,始終沒有成功,可是當我改成調用AuthSub之後,幾個小時就實現了整個登錄過程。

有關Google AuthSub的文檔和相關的內容,請參考:AuthSub Authentication for Web Applications

廢話少說,現直接看一個Google的AuthSub登陸過程介紹圖片:

2008126103941491.png

1.生成用來轉向給Google認證系統的網址並轉向(圖中1);

2.接受從Google認證系統迴轉的頁面參數(圖中4)

3.使用Google的返回的參數向Google請求用戶信息(圖中5和6)

現在,讓我們來按照順序完成以上流程:

首先,你的程序必須在Google註冊才能調用此接口,因此先到Google的Manage your domains 頁面註冊你的程序,註冊界面如下:

2008126103941414.jpg

1."Target URL path prefix"只是個前綴而已,並不是完整的迴轉的路徑,在生成登錄的時候,還是要將回轉的路徑發送給Google,而且必須和這裏註冊的前綴相符,這應該算是用來保障安全的一個設置。

 

2.OAuth Consumer Key和OAuth Consumer Secret在OAuth認證的時候相當的重要,不過在AuthSub之中是沒有用到的。

註冊完成之後,就要開始生成登錄的URL,按照Google的文檔,AuthSub的登陸URL應該是https://www.google.com/accounts/AuthSubRequest 加上一些參數組成,我用到了兩個參數:

1.next參數,指定用戶登錄完成之後迴轉的URL地址,注意必須以上面註冊的"Target URL path prefix"開頭;

2.scope參數,指定需要訪問哪些資源,這裏的資源是以URL來描述的,例如我們在系統之中需要得到用戶的登錄用戶名,所以要訪問用戶的通訊錄(Google登錄迴轉參數並不包含用戶名),因此可以從Google Contacts Data API 上查到需要使用的scope爲http://www.google.com/m8/feeds/,注意,如果需要訪問多個資源,則以空格隔開每個URL

結合以上兩個參數(注意,每一個參數都必須經過URL編碼),就可以的到一個URL地址,例如https://www.google.com/accounts/AuthSubRequest?next=http%3a%2f%2faccount.step1.cn%2faccount%2fHandler.aspx%3fass%3dgoogle.com&scope=http%3a%2f%2fwww.google.com%2fm8%2ffeeds%2f,我們將用戶轉向過去,用戶就會到達Google的登陸界面(假如用戶已經登錄到Google,則會跳過此界面):

2008126103942403.jpg

Step1帳戶登錄系統(3.使用Google的Auth Sub登錄網站)

用戶只有點擊“授予訪問權”纔會正常登錄,在登錄完成之後,Google會將用戶轉向到訪問你在上面的next參數之中指定的URL地址,並在地址之中加入一個token參數包含一個訪問令牌,例如這個網址:http://account.step1.cn/account/Handler.aspx?ass=google.com&token=CJaHiYeKEBCE7qWi-v____8B

我們只需要在迴轉頁面(我係統之中的Hander.aspx)之中對token參數進行處理即可,需要說明的是,我不願意爲每一種登錄類型都建立一個迴轉頁面,這樣會帶來維護上的麻煩,因此我的所有迴轉頁面都是Handler.aspx然後通過ass參數進行區分。

下一步就是根據我們已經得到的token參數獲取用戶的用戶名(E-mail地址),我研究了好久,最後發現發現只需要訪問用戶的地址本即可,因爲用戶即使地址本之中沒有任何內容,返回的內容也包含他本人的E-mail地址,因此,我通過HttpWebRequest訪問如下網址:http://www.google.com/m8/feeds/contacts/default/thin?max-results=0

,這個大體含義是以最簡單的結果(thin)返回地址本之中的0條記錄,當然,訪問的時候要使用剛纔獲取到的token令牌,否則會得到需要認證的提示,具體的方法請參考下面的代碼。

最後,將得到的用戶名和用戶暱稱保存到Cookie,大功告成!

以下是我實現的以上過程的類AuthSubServer.cs的源碼:

AuthSubServer.cs的源碼

1 public class AuthSubServer:BaseServer
2 {
3 private string urlAuthSubRequest, urlScope, urlData;
4 //採用Web.Config之中的XML節點作爲構造函數參數
5 public AuthSubServer(System.Xml.XmlNode node)
6 : base(node)
7 {
8 for (int i = 0; i < node.Attributes.Count; i++)
9 {
10 switch (node.Attributes[i].LocalName)
11 {
12 case "urlAuthSubRequest":
13 urlAuthSubRequest = node.Attributes[i].Value;
14 break;
15 case "urlScope":
16 urlScope = node.Attributes[i].Value;
17 break;
18 case "urlData":
19 urlData = node.Attributes[i].Value;
20 break;
21 }
22 }
23 }
24 public override string getLoginUrl()//生成登錄的URL
25 {
26 return urlAuthSubRequest + "next=" + HttpUtility.UrlEncode(getHandleUrl()) + "&scope=" + HttpUtility.UrlEncode(urlScope);
27 }
28 public override void parseHandle(HttpContext page)//處理迴轉請求
29 {
30 string token = page.Request["token"];
31 HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(urlData));//訪問通訊錄數據
32 request.Headers.Add("Authorization", "AuthSub token="" + token + """);//這一句將token令牌加入到數據訪問請求之中
33 request.Method = "GET";
34 HttpWebResponse response = (HttpWebResponse)getResponse(request);
35 XmlDocument doc = new XmlDocument();
36 if (response != null)
37 {
38 doc.Load(response.GetResponseStream());
39 XmlNode node = doc.SelectSingleNode("*/*[local-name()='id']");//讀取用戶的ID(E-mail地址)
40 string account = node != null ? node.InnerText : "";
41 node = doc.SelectSingleNode("*/*[local-name()='author']/*[local-name()='name']");//讀取用戶暱稱
42 string name = node != null ? node.InnerText : "";
43 AccountHelper.setUserInfo(account, name, this.name);
44 AccountHelper.returnOpener();
45 page.Response.End();
46 }
47 }
48 public static HttpWebResponse getResponse(HttpWebRequest request)
49 {
50 try
51 {
52 return (HttpWebResponse)request.GetResponse();
53 }
54 catch (WebException e)
55 {
56 HttpContext.Current.Response.Write(e.Message);
57 string result = new StreamReader(e.Response.GetResponseStream()).ReadToEnd();
58 HttpContext.Current.Response.Write(result);
59 HttpContext.Current.Response.End();
60 }
61 return null;
62 }
63 }

4.使用Windows Live ID登錄網站

在上一篇文章之中,我具體的講解了使用Google的Authsub接口來讓自己的網站支持Google帳號登陸,今天要講到的是Windows Live ID,即使用MSN或Hotmail的帳號來登錄網站,因爲其中的基礎原理類似,因此,假如對登錄的流程和原理不清楚的話,建議去看看我前面的幾篇文章,在這裏,我主要講解具體的實現和源碼。

關於Windows Live ID接口的更多信息,建議去參考查看Windows Live ID接口介紹網站的更多內容,我在這篇文章之中,僅僅會講到Windows Live ID在Web Authentication上的一個應用。

首先,和Google AuthSub一樣,你必須先在Live的雲計算開發中心Azure Services Developer Portal去註冊你的程序,其實幾個星期前我註冊程序的時候應該還不叫這個比較時髦的名字的,今天我因爲寫這個文章再上去一看,居然變成“雲”了,這麼說來,我已經“雲計算”了一把了,呵呵!

現在的註冊界面如圖(我這裏截圖是修改界面,添加界面和這個類似):

2008126103938516.gif

註冊完成之後,就可以開始進行開發,需要說明的是,在Live ID站點上提供了一個非常簡單的類WindowsLiveLogin.cs,應該可以在這個例子之中得到這個類:Running the C# QuickStart Sample,而且可能是因爲有了這個類,Microsoft覺得不需要再去提供什麼和服務端交互的文檔了(反正我沒有找到),因此,我就直接不改動任何程序的情況下使用了這個類,這樣,一切就方便了很多了。

先看如何得到用來讓用戶登錄的轉向地址,這個太容易了,因爲這正是WindowsLiveLogin類的方法,需要說明的是,WindowsLiveLogin類有幾個方法來獲得轉向地址的,其中GetLoginUrl方法得到的地址僅僅進行用戶登錄,而不向用戶申請任何權限,而GetConsentUrl(string scope)方法可以指定向用戶申請讀取相應數據的權限,和Google AuthSub一樣,我們在使用Live的登錄的時候也需要讀取用戶的地址本信息來獲得用戶的登錄帳號地址,因此,我們使用wll.GetConsentUrl("Contacts.View");方法來獲得轉向URL,("Contacts.View"代表對地址本的只讀訪問),當然在使用這個方法之前,要先設置好PolicyUrl屬性(隱私申明地址)和ReturnUrl屬性(登錄完成之後的迴轉地址);

有一點需要特別說明的是:在使用GetLoginUrl或者GetConsentUrl方法之前,Live要求用戶必須必須已經指定WindowsLiveLogin的PolicyUrl屬性,也就是隱私申明的地址,而且,要求指定的網址必須是能夠訪問的,似乎Live還會去檢查這個網址是否能夠訪問的,我在系統之中將隱私申明的網址指定爲我的登錄頁面,因爲在那個頁面有我關於保護用戶隱私的申明。

將用戶轉向到剛纔獲取的URL之後,用戶就會被轉向到Live ID的登錄頁:

2008126103939908.gif

Live轉回到登錄迴轉地址的時候,會有多種參數(delauth,login,logout,clearcookie),根據Live接口的要求,這些類型都應該實現,具體每個類型代表如下含義:

1.delauth,這個是我主要用到的類型,就是在用戶登錄並授與訪問權限之後,迴轉的類型,注意,必須調用GetConsentUrl方法纔會得到這種迴轉,從參數之中可以獲取到ConsentToken參數,就是後面要用到的數據訪問令牌。

2.login,這是調用GetLoginUrl方法之後迴轉的類型,也就是說,是不需要訪問用戶的任何數據的情況下回轉訪問的參數類型;

3.logout,註銷

4.clearcookie,清除Cookie

在本系統之中,僅僅使用了delauth,從參數之中獲得ConsentToken參數,然後,我們可以使用這個數據令牌去獲取用戶的帳戶名稱和暱稱,同樣,也是訪問Windows Live Contacts接口,因爲Windows Live Contacts接口不是我要講的主題,因此不做詳細的介紹,需要了解我如何使用,參考我的代碼即可。

取得用戶的登錄ID和暱稱之後,寫入到用戶的Cookie之中,然後登錄迴轉,採用Live登錄的過程就算完成了,還有什麼不清楚的地方的話,請參考我下面貼出的整體代碼:

LiveServer.cs代碼

1 public class LiveServer : BaseServer
2 {
3 public WindowsLiveLogin wll;
4 //採用Web.Config之中的XML節點作爲構造函數參數
5 public LiveServer(System.Xml.XmlNode node):base(node)
6 {
7 wll = new WindowsLiveLogin(false);
8 for (int i = 0; i < node.Attributes.Count; i++)
9 {
10 switch (node.Attributes[i].LocalName)
11 {
12 case "appid":
13 wll.AppId = node.Attributes[i].Value;
14 break;
15 case "secret":
16 wll.Secret = node.Attributes[i].Value;
17 break;
18 case "securityalgorithm":
19 wll.SecurityAlgorithm = node.Attributes[i].Value;
20 break;
21 }
22 }
23 }
24 //採用回傳得到的令牌獲取用戶的信息
25 public XmlDocument getData(string token)
26 {
27 string lid = wll.ProcessConsentToken(token).LocationID;
28 //訪問用戶的地址本
29 HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri("https://livecontacts.services.live.com/users/@L@" + lid + "/rest/LiveContacts/owner"));
30 request.Method = "GET";
31 request.Headers.Add("Authorization", "DelegatedToken dt="" + wll.ProcessConsentToken(token).DelegationToken + """);
32 HttpWebResponse response = getResponse(request);
33 XmlDocument doc = new XmlDocument();
34 if (response != null)
35 {
36 doc.Load(response.GetResponseStream());
37 }
38 return doc;
39 }
40 public static HttpWebResponse getResponse(HttpWebRequest request)
41 {
42 try
43 {
44 return (HttpWebResponse)request.GetResponse();
45 }
46 catch (WebException e)
47 {
48 HttpContext.Current.Response.Write(e.Message);
49 string result = new StreamReader(e.Response.GetResponseStream()).ReadToEnd();
50 HttpContext.Current.Response.Write(result);
51 HttpContext.Current.Response.End();
52 }
53 return null;
54 }
55 //得到用戶的登錄URL地址
56 public override string getLoginUrl()
57 {
58 wll.PolicyUrl = AccountHelper.getLoginUrl();
59 wll.ReturnUrl = getHandleUrl();
60 return wll.GetConsentUrl("Contacts.View");
61 //return wll.GetLoginUrl();
62 }
63 //處理登錄迴轉信息
64 public override void parseHandle(HttpContext page)
65 {
66 string action = page.Request["action"];
67 switch (action)
68 {
69 case "delauth":
70 //獲得用戶地址本XML數據
71 XmlDocument doc = getData(page.Request["ConsentToken"]);
72 //獲取用戶帳號
73 XmlNode node = doc.SelectSingleNode("Owner/WindowsLiveID");
74 string account = node != null ? node.InnerText : "";
75 //獲取用戶的暱稱
76 node = doc.SelectSingleNode("Owner/Profiles/Personal/DisplayName");
77 string name = node != null ? node.InnerText : "";
78 //設置用戶信息到Cookie
79 AccountHelper.setUserInfo(account, name, this.name);
80 //迴轉
81 AccountHelper.returnOpener();
82 page.Response.End();
83 break;
84 case "login"://這是Live接口要求定義支持的類型,系統之中沒有主動使用這種請求
85 WindowsLiveLogin.User user = wll.ProcessLogin(page.Request.Form);//從URL參數之中解析出用戶的登錄信息
86 AccountHelper.setUserInfo(user.Id, user.Id, this.name);//這裏的user.ID實際上已經是用戶的E-mail
87 AccountHelper.redirect(wll.GetConsentUrl("Contacts.View", user.Token));
88 page.Response.End();
89 break;
90 case "logout"://這是Live接口要求定義支持的類型,系統之中沒有主動使用這種請求
91 AccountHelper.clearCookie();
92 AccountHelper.returnOpener();
93 page.Response.End();
94 break;
95 case "clearcookie"://這是Live接口要求定義支持的類型,系統之中沒有主動使用這種請求
96 default:
97 AccountHelper.clearCookie();
98 string type;
99 byte[] content;
100 wll.GetClearCookieResponse(out type, out content);
101 page.Response.ContentType = type;
102 page.Response.OutputStream.Write(content, 0, content.Length);
103 page.Response.End();
104 break;
105
106 }
107 }
108 }

5.使用Yahoo BBAuth登錄網站

從系列之中的上一篇文章,我介紹瞭如何支持Windows Live ID來登錄自己的網站,這一篇按照順序,我介紹如何支持使用Yahoo BBAuth來登錄網站,和上面的一篇文章一樣,我將會僅僅對具體的邏輯進行實現,不再重複基礎的原理。

關於Yahoo BBAuth的更多信息,請參考:Browser-Based Authentication。

可以先看Yahoo提供的BBAuth的原理示意圖:

2008126103935936.png

2008126103935915.gif

2008126103936286.gif

好,閒話少說,在經過註冊之後,得到了這樣幾個參數:appid,secret,這幾個參數在程序之中需要使用的。

先看如何得到用來讓用戶登錄的轉向地址,這個非常容易,那幾個參數也是現成的,只是格式是嚴格要求的,一定不能出錯,否則反正Yahoo的BBAuth服務器反正就是給你出個錯,讓你摸不着頭腦了,詳細的代碼還是在下面看看代碼吧。

得到轉向地址並轉向給Yahoo之後,就可以將用戶轉向到Yahoo的公共登錄界面,用戶登錄之後,就會出現關於登錄許可的提示,用戶只有點擊同意才能繼續登錄的過程。

2008126103936131.gif

1.檢查回傳得Token是否正常;

2.根據回傳的Token向Yahoo的服務器請求WSSID;

3.根據WSSID向Ymail接口請求用戶的E-mail地址;

得到E-mail地址之後,就完成了整個登錄過程。

下面是實現過程的代碼:

BBAuthServer.cs代碼

1 public class BBAuthServer:BaseServer
2 {
3 private string appid, secret, server, pathLogin, pathPwtoken_login;
4 private int timeout=300;
5 //採用Web.Config之中的XML節點作爲構造函數參數
6 public BBAuthServer(System.Xml.XmlNode node)
7 : base(node)
8 {
9 for (int i = 0; i < node.Attributes.Count; i++)
10 {
11 switch (node.Attributes[i].LocalName)
12 {
13 case "appid":
14 appid = node.Attributes[i].Value;
15 break;
16 case "secret":
17 secret = node.Attributes[i].Value;
18 break;
19 case "server":
20 server = node.Attributes[i].Value;
21 break;
22 case "pathLogin":
23 pathLogin = node.Attributes[i].Value;
24 break;
25 case "pathPwtoken_login":
26 pathPwtoken_login = node.Attributes[i].Value;
27 break;
28 case "timeout":
29 timeout = int.Parse(node.Attributes[i].Value);
30 break;
31 }
32 }
33 }
34 public bool checkRequest(HttpRequest Request)
35 {
36 string ts = Request["ts"];
37 string sig = Request["sig"];
38 //先檢查時間
39 if (Math.Abs(long.Parse(ts) - ((long)((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds))) > timeout)
40 {
41 // throw new Exception("Error parsing timeout.");
42 }
43 //再檢查簽名
44 string baseString = System.Text.RegularExpressions.Regex.Replace(Request.Url.PathAndQuery, "&sig=[^&]+", "");
45 if (System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(baseString + secret, "MD5").ToLower() != sig)
46 {
47 throw new Exception("Signature mismatch:" + baseString);
48 }
49 return true;
50 }
51 public string getWSSID(string token,out string cookie)
52 {
53 string ts = ((long)((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds)).ToString();
54 string baseString = pathPwtoken_login+"appid=" + HttpUtility.UrlEncode(appid) + "&token=" + HttpUtility.UrlEncode(token) + "&ts=" + ts + "";
55 string sig = System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(baseString + secret, "MD5").ToLower();
56 HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(server + baseString + "&sig=" + sig));
57 request.Method = "GET";
58 HttpWebResponse response = (HttpWebResponse)request.GetResponse();
59 XmlDocument doc = new XmlDocument();
60 doc.Load(response.GetResponseStream());
61 cookie = doc.SelectSingleNode("//*[local-name()='Cookie']").InnerText.Trim().Substring(2);
62 return doc.SelectSingleNode("//*[local-name()='WSSID']").InnerText.Trim();
63 }
64 public string getUserName(string token,string wssid,string cookie)
65 {
66 try
67 {
68 HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri("http://mail.yahooapis.com/ws/mail/v1.1/soap?appid=" + HttpUtility.UrlEncode(appid) + "&WSSID=" + HttpUtility.UrlEncode(wssid)));
69 request.Method = "POST";
70 request.CookieContainer = new System.Net.CookieContainer();
71 CookieCollection collection = new CookieCollection();
72 request.CookieContainer.Add(new Uri("http://mail.yahooapis.com/"), new Cookie("Y", cookie));
73 byte[] bytes = Encoding.UTF8.GetBytes("<?xml version="1.0" encoding="utf-8"?>http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:xsd="http://www.w3.org/1999/XMLSchema" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">xmlns="urn:yahoo:ymws">");
74 request.Headers.Add("SOAPAction", """");
75 request.ContentType = "application/soap+xml; charset=utf-8";
76 request.ContentLength = bytes.Length;
77 Stream rs = request.GetRequestStream();
78 rs.Write(bytes, 0, bytes.Length);
79 rs.Close();
80 HttpWebResponse response = getResponse(request);
81 XmlDocument doc = new XmlDocument();
82 doc.Load(response.GetResponseStream());
83 return doc.SelectSingleNode("//*[local-name()='defaultID']").InnerText;
84 /**//*
85 ymws ymwsInstance = new ymws();
86 ymwsInstance.Url = "http://mail.yahooapis.com/ws/mail/v1.1/soap?appid=" + appid + "&wssid=" + wssid;
87 ymwsInstance.CookieContainer = new System.Net.CookieContainer();
88 CookieCollection collection = new CookieCollection();
89 ymwsInstance.CookieContainer.Add(new Uri("http://mail.yahooapis.com/"), new Cookie("Y", cookie));
90 GetUserDataResponse userData = ymwsInstance.GetUserData(new GetUserData());
91 return userData.data.userSendPref.defaultID;*/
92 }
93 catch (Exception)
94 {
95 return wssid;
96 }
97 }
98 public override string getLoginUrl()//生成登錄的URL
99 {
100 string ts = ((long)((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds)).ToString();
101 string baseString = pathLogin+"appid=" + HttpUtility.UrlEncode(appid) + "&appdata=" + HttpUtility.UrlEncode(name) + "&send_userhash=1&ts=" + ts + "";
102 string sig = System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(baseString + secret, "MD5").ToLower();
103 return server + baseString + "&sig=" + sig;
104 }
105 public override void parseHandle(HttpContext page)//處理迴轉請求
106 {
107 checkRequest(page.Request);//檢查回傳請求是不是合法
108 string cookie;
109 string wssid = getWSSID(page.Request["token"],out cookie);//先獲取wssid
110 string name = getUserName(page.Request["token"], wssid, cookie);//通過wssid獲取用戶名
111 //檢查完畢,開始獲得用戶的WSSID
112 AccountHelper.setUserInfo(page.Request["userhash"], name, this.name);
113 AccountHelper.returnOpener();
114 page.Response.End();
115 }
116 public static HttpWebResponse getResponse(HttpWebRequest request)
117 {
118 try
119 {
120 return (HttpWebResponse)request.GetResponse();
121 }
122 catch (WebException e)
123 {
124 HttpContext.Current.Response.Write(e.Message);
125 string result = new StreamReader(e.Response.GetResponseStream()).ReadToEnd();
126 HttpContext.Current.Response.Write(result);
127 HttpContext.Current.Response.End();
128 }
129 return null;
130 }
131 }

6.使用OpenID登錄網站

在我昨天的博客之中,我我介紹了我在帳戶登錄系統中對Yahoo的BBAuth的登錄的支持過程,上面那篇文章的代碼雖然很簡單,文章也不長,實際上卻是我研究的最深刻的一種類型,也是我對BBAuth的研究最終讓我決定將這個系統全部開源。,今天,我要講一下,OpenID的實現,下一篇應該是講如何支持校內網的登錄,再後面我可能簡單的講講Oauth登錄(如果我確實能夠研究清楚地話),有網友曾經問到什麼時候會提供整個代碼的下載,我並不是不提供,而是我覺得代碼還非常不完善,我在網站上提供代碼的下載肯定要在我每一個支持的登錄類型都介紹完畢之後,當時一定要在元旦之前,如果確實在此之前需要參考一下我的代碼,可以直接與我聯繫,我隨時可以提供所有的源碼。

講到OpenID,想來很多讀者都有一定的瞭解,沒錯,這是一個開放的協議,目的就是讓用戶能夠在所有的網站使用一個賬號和密碼登錄,這一點,和本系統的目的是不同的,因爲本系統是比較自私的,希望是讓所有的用戶都可以使用自己已經有的帳戶和密碼登錄,而OpenID則是希望所有的用戶在所有的網站都可以使用一個賬號和密碼登錄,當然要實現這個是比較難的,目前沒有實現,但是不能不承認這是一個很有意義的事情,而本系統也不能不支持這種帳戶的登錄。

關於OpenID的更多信息建議到OpenID的相關網站去看看,說老實話,這是我很看好的一個項目,如果感興趣可以看看OpenID的相關網站。

OpenID的實現原理和本系統,和Google的AuthSub,Yahoo的BBAuth,Live的Account Server,包括我還沒有介紹到的Oauth,實現的原理都是一樣的,都是採用一種可信的方式在用戶和Web應用程序之間建立交互,所不同的是,OpenID因爲是要做到開放和能夠到處兼容,所以有一些其它的考慮,至於具體有一些什麼考慮,我能瞭解的是這樣幾點:

1.採用URL作爲賬號,這樣的話,就可以支持任意的網站,任何一個網站都可以提供OpenID了

2.用戶提供的URL之中,通過http或者html的Header來指定OpenID服務器的地址,甚至可以指定牛轉向到另一個OpenID地址,這樣可以確保用戶可以使用自己喜歡的網址來作爲登錄帳號

我知道的不比你多多少,我知道的也就是這些了,對於怎麼才能支持OpenID登錄,雖然我研究了很久,不過最後得出一個結論,還是使用公用的庫吧,因爲OpenID相對還是比較複雜的,要完整地支持還是挺難的;

我採用的是ExtremeSwank的Dotnet OpenID2.0的庫,這個庫確實很好用,很快我就完成了對OpenID的支持,因爲我對OpenID的瞭解實際上並不夠,因此也不太合適對OpenID進行什麼長篇大論,因此,我也就簡單的提供OpenID的支持相關代碼了:

需要注意的是,以前我支持的多個類型都是只需要選擇登錄類型即可,而對於OpenID,因爲必須先讓用戶輸入登錄帳號(URL),根據url地址才能夠得到登錄的轉向地址,所以,必須先讓用戶輸入帳號才能進行登錄的轉向。

因爲在登錄界面根據URL的不同而不同,所以這次在本站也不再列出登錄相關的截圖,僅僅提供實現的代碼:

OpenidServer.js代碼

1 public class OpenidServer : BaseServer
2 {
3 private string urlIdentity=null,defaultNickName=null;
4 //採用Web.Config之中的XML節點作爲構造函數參數
5 public OpenidServer(System.Xml.XmlNode node)
6 : base(node)
7 {
8 for (int i = 0; i < node.Attributes.Count; i++)
9 {
10 switch (node.Attributes[i].LocalName)
11 {
12 case "urlIdentity"://如果指定了urlIdentity,就可以不讓用戶輸入帳好了,就可以支持OpenID類型的網站類型登錄了,例如指定爲yahoo.com,則用戶就可以直接支持Yahoo的登錄了
13 urlIdentity = node.Attributes[i].Value;
14 break;
15 case "defaultNickName"://指定默認的暱稱
16 defaultNickName = node.Attributes[i].Value;
17 break;
18 }
19 }
20 }
21 public override string getLoginUrl()//返回登錄地址
22 {
23 OpenIDConsumer openid = new OpenIDConsumer(new NameValueCollection(), null, null);
24
25 SimpleRegistration sr = new SimpleRegistration(openid);
26 sr.AddRequiredFields(SimpleRegistrationFields.Nickname);//設置附加的字段列表
27
28 openid.ReturnURL = this.getHandleUrl();
29 openid.Identity = urlIdentity!=null?urlIdentity:HttpContext.Current.Request["openid_url"];
30 return openid.BeginAuth(false, false);//獲得並返回登錄地址
31 }
32 public override void parseHandle(HttpContext page)//迴轉內容處理函數
33 {
34 OpenIDConsumer openid = new OpenIDConsumer(page.Request.QueryString, null, null);
35 switch (openid.RequestedMode)
36 {
37 case RequestedMode.IdResolution:
38 if (openid.Validate())
39 {
40 OpenIDUser user = openid.RetrieveUser();
41 string account=user.Identity;
42 if (user.ExtensionData.ContainsKey(SimpleRegistrationFields.Email))
43 {
44 account = user.ExtensionData[SimpleRegistrationFields.Email];
45 }
46 string nickName;//雖然設置了附加的字段類型,可是服務器未必支持,因此還是要判斷該字段是不是存在
47 if(user.ExtensionData.ContainsKey(SimpleRegistrationFields.Nickname))
48 {
49 nickName=user.ExtensionData[SimpleRegistrationFields.Nickname];
50 }
51 else
52 {
53 nickName=defaultNickName!=null?defaultNickName:account;
54 }
55 AccountHelper.setUserInfo(account, nickName, this.name);
56 AccountHelper.returnOpener();
57 page.Response.End();
58 }
59 break;
60 }
61 }
62 }

7.使用OpenSocial接口登錄支持校內網用戶的登錄

上面的幾篇文章之中,我按照順序講解了Google的AuthSub,Yahoo的BBAuth,Live的Account ID,OpenID的登錄,而這正是我在規劃這個Step1賬戶登錄系統時候設計要支持的幾個網站,現在已經全部支持了,不過可惜的是這些都是國外的網站,國內的很多用戶沒有這樣的賬戶的,這樣,這個系統的可用性就會大大降低了,可惜國內的大網站們現在似乎都沒有要開放API的意思,這一點是比較鬱悶的。

我仔細的想過如何才能支持國內的一些網站的登錄,結果還是沒有什麼辦法,不過我忽然有一天在上校內網的時候,忽然想到,可不可以使用這些SNS的網站的接口來完成這個功能呢?現在SNS網站開放接口確實是像一陣風一樣,包括51.com也開放了API,不過51.com是我比較鄙視的網站,也就不提它了。

我開始仔細的考慮整個登錄過程,按照以前的每一個接口的邏輯,首先要生成一個登錄網址,這個網址怎麼得來呢?可以用我們自己開發的SNS APP的頁面地址,這個網址通常是固定的,完全可以使用這個網址來作爲我們系統的登錄轉向地址。

我們將用戶轉向到這個地址之後,因爲SNS網站的內容都是需要用戶登錄的,所以,用戶會被再次轉向到SNS的登錄界面,這樣,用戶的登錄就開始了。

在用戶登錄完成之後,就會被重新轉向到APP的頁面,這樣的話,APP就會被加載。

APP加載之後,其任務就是獲取到用戶的資料(ID和名稱),然後帶上這些參數,將整個頁面轉向回到登錄系統,這樣的話,整個登錄過程就完成了。

上面所說的是一個基本原理,如果要確實的完成之一個過程,需要以下以下條件:

1.SNS系統之中,對每一個APP都有一個固定不變的網址,這個條件通常是具備的,因爲SNS網站考慮到用戶可能通過網址的複製來分享應用,所以這個網址肯定存在;

2.打開這個網址,就會直接轉向到登錄的頁面。要是哪個SNS網站允許未登錄用戶查看某個APP的簡單信息,需要用戶手工點擊一個“登錄”按鈕什麼的才能進入登錄過程,這樣的話需要用戶多點擊一次,就使登錄過程不怎麼順暢了;

3.用戶登錄完成之後,應該回到APP的頁面,要是哪個SNS網站在登錄之後總是到用戶的控制檯,而不是返回到登錄請求頁,那就不行了;

4.加載一個APP最好是在當前頁面上加載,而不是在Iframe上加載,假如確實是在Iframe上加載的(目前最多的就是這種模式),也希望是在同一個域,實在不行是同一個根域也行,這是爲了能夠在獲取到用戶信息之後讓網頁的頂端框架跳轉到登錄系統,如果實在不能通過JS來進行轉向,那就只能放target="_top"的鏈接提供給用戶點擊,那樣的話,登錄的流暢性也就大打折扣了。

能滿足上面的四個條件,再加上SNS的APP通常都是能夠獲取用戶的資料的,這樣就可以比較流暢的融入到Step1登錄系統之中,根據我使用的情況,校內網是能夠實現比較好的登錄過程的,下面我來介紹一下實現的具體過程(校內網有兩種接口,我是採用Google提供標準的OpenSocial接口實現的):

1.首先還是要去校內網申請一個應用程序,在"開發者應用"之中點擊"申請OpenSocial開發許可證",就會進入開發許可證申請頁面:

20081210155738429.gif

下面會逐步的顯示實現的代碼,因爲這個比較特殊,代碼分爲4塊:Web.config配置代碼,OpenSocial App頁面ASPX,OpenSocial App頁面

類,和OpenSocialServer.cs代碼

1.Web.Config配置代碼:

1 http://apps.xiaonei.com/passport/login.html"/>

2.OpenSocial APP頁面ASPX,在本系統之中被我部署到http://account.step1.cn/account/tools/opensocial.aspx(直接打開無效)

OpenSocia APP頁面代碼

1<%@ Page Language="C#" AutoEventWireup="true" Inherits="Step1.AccountServer.Tools.OpenSocialPage" ResponseEncoding="utf-8" ContentType="text/xml"%><?xml version="1.0" encoding="UTF-8" ?>
2
3
4
5
6
7
34 ]]-->
35
36

3.OpenSocial APP頁面代碼文件

OpenSocialPage.cs

1 public partial class OpenSocialPage : System.Web.UI.Page
2 {
3 protected string handleUrl,acc,domain;
4 protected void Page_Load(object sender, EventArgs e)
5 {//設置需要顯示給APP的變量
6 BaseServer server = AccountHelper.getServerByName(Request["ass"]);
7 handleUrl = server.getHandleUrl();
8 domain = Configuration.Instance().rootUrl;
9 }
10 }

4.OpenSocialServer.cs

OpenSocialServer.cs

1 public class OpenSocialServer:BaseServer
2 {
3 private string loginUrl;
4 //採用Web.Config之中的XML節點作爲構造函數參數
5 public OpenSocialServer(System.Xml.XmlNode node)
6 : base(node)
7 {
8 for (int i = 0; i < node.Attributes.Count; i++)
9 {
10 switch (node.Attributes[i].LocalName)
11 {
12 case "loginUrl":
13 loginUrl = node.Attributes[i].Value;
14 break;
15 }
16 }
17 }
18 public override string getLoginUrl()//直接將XML配置之中的登錄URL返回
19 {
20 return loginUrl;
21 }
22 public override void parseHandle(HttpContext page)//處理迴轉請求
23 {
24 System.Collections.Specialized.NameValueCollection request = HttpUtility.ParseQueryString(page.Request.Url.Query, Encoding.UTF8);
25 string id = request["id"];
26 string name = request["name"];
27 AccountHelper.setUserInfo(id, name, this.name);//設置用戶的Cookie
28 AccountHelper.returnOpener();//轉向到開始請求登錄時的頁面
29 }
30 }

8. Step1.AccountClient的實現

在前面的7篇文章之中我介紹了Step1.AccountServer的實現,現在,總體的結構和5種用戶登錄類型都已經介紹完畢,也就是AccountServer都已經介紹完了,原計劃還要介紹OAuth的,不過不論是Google 還是Yahoo對OAuth的實現,我最終都沒有能夠研究成功,都只是完成了登錄過程,卻沒有實現使用登陸後得到的Token向服務器獲取到信息,我會在後面的源碼之中同樣將這些沒有研究完成的內容包含在內,感興趣的用戶可以自己去研究了。
        今天,我將介紹Step1.AccountClient的邏輯,也就是如何在一個網站應用程序上使用AccountClient來連接AccountServer,每一個網站應用程序只需要引用AccountClient庫,就可以實現登錄的過程,以及獲得用戶信息了。
        Step1.AccoutClient是一個非常簡單的類庫,僅僅包含兩個類,AccountHelper類(包含一些靜態方法,用來提供給Web應用程序使用),和一個AccountPage類(是一個頁面的代碼,就是包含登錄和註銷的頁面邏輯,這個頁面沒有任何界面,僅僅在請求的時候進行Cookie的設置和轉向)
        因爲Step1.AccoutClient邏輯比較簡單,這篇文章以代碼爲主。
        先看看Web.Config的代碼,這個配置的參數因爲很少,因此,僅僅在appSettings標籤之中加上以下幾個參數就可以了:
Web.Config配置代碼

ContractedBlock.gifCode

再看看AccountHelper類:
AccountHelper.cs代碼

ContractedBlock.gifCode

回傳字段的參數名稱

ContractedBlock.gifCode

AccountHelper類包含一些核心的靜態方法,可以在Web應用程序之中調用這些方法,不過在需要登錄的時候,也可以讓Web應用程序自己將頁面轉向到登錄的頁面(通過JS的方式或者其他方式都可以轉向到登錄頁面,那個也是這個系統靈活的一個方面)
        下面是登錄頁面的代碼,這個頁面的ASPX是直接引用代碼類,不作任何處理,代碼非常簡單: 
        <%@ Page Language="C#" AutoEventWireup="true"  Inherits="Step1.AccountClient.AccountPage" %>
        主要的內容在AccountPage類之中,代碼也是很簡單:
AccountPage.cs代碼

ContractedBlock.gifCode

上面的網頁提供三種功能:登錄、註銷、和處理迴轉請求,如果Web應用程序需要轉向到此頁面,只需要加上相應的action參數,最好再加上一個url參數指定操作完成之後登錄迴轉的地址,如果不提供,將會自動讀取Referrer的地址作爲登錄地址。
        上面就已經是所有的Step1.AccountClient的實現代碼,可以看出,Step1.AccountClient實現的過程非常簡單,而且和那些帳戶服務API之間的沒有任何關係,這正是這個系統所希望的,這樣的話,AccountServer的更新,特別是添加和取消帳戶服務的話,對AccountClient的程序沒有任何影響。
        這個系統的設計完全是考慮到跨域名的模式的使用的,不過如果AccountClient和AccountServer是在同一個根域名下,其實是沒有必要部署AccountClient的,直接使用一個指向到根域名的Cookie就可以了。
        到這篇文章爲止,就已經介紹完了所有的Step1.AccountClient實現過程,下篇文章將是本系列的最後一篇文章,包含一些心得和最終的所有源碼下載。

 

9. 總結和源碼下載

因爲本次閉關期間還有很多任務沒有完成,時間已經安排不過來,因此,只能比原計劃提前結束此係列文章的介紹,不過反正需要介紹的內容差不多都已經介紹完畢了,只是提前進行源碼的整理而已。

因爲這是一個總結的貼子,因此,先列出之前的9篇文章內容:

Step1帳戶登錄系統(0.整體思路)

Step1帳戶登錄系統(1.程序結構)

Step1帳戶登錄系統(2.基礎代碼)

Step1帳戶登錄系統(3.使用Google的Auth Sub登錄網站)

Step1帳戶登錄系統(4.使用Windows Live ID登錄網站)

Step1帳戶登錄系統(5.使用Yahoo BBAuth登錄網站)

Step1帳戶登錄系統(6.使用OpenID登錄網站)

Step1帳戶登錄系統(7.使用OpenSocial接口登錄支持校內網用戶的登錄)

Step1帳戶登錄系統(8. Step1.AccountClient的實現)

在本文的最下方,將會包含整個源碼,而在此之前,我簡單的說明一下一些心得和需要注意的地方:

1.關於此係統的安全問題,要知道,本系統由最終用戶、Web應用(AccountClient)、AccountServer,帳戶服務器四個角色組成,每一個角色都需要考慮自己角色的安全問題,大體如下:

a.用戶對安全的考慮很直觀,就是不希望在不信任的網站上輸入自己的賬戶和密碼,同時希望僅僅將用戶自己授權的資料提供給Web應用網站,這個功能在本系統之中是滿足要求的,實際上,這也是本系統設計的核心原理;

b.Web應用對安全的考慮最重要的是不受僞裝的AccountServer的欺騙,要知道,AccountClient通過接收URL參數來得到用戶信息並保存爲Cookie,假如有一個僞裝的URL參數,實際上是可以欺騙Web應用的,在我提供的源碼以及目前線上的系統之中,並沒有對AccountServer和AccountClient之間的交互進行加密處理,這是因爲這只是一個研究性的項目,當我確實需要在此登錄系統上開發安全級別高的應用的時候,一定會考慮加上安全的限制,可以參照AccountServer的帳戶服務器的方法,爲每一個Web應用分配一個祕鑰,例如對雙方傳遞的URL參數進行MD5的校驗;

c.AccountServer的安全性有兩個方面,一方面避免受到僞裝的AccountClient的欺騙,實際上這種欺騙影響很小,通過b裏面提到的md5校驗的方式可以防止這種欺騙,另一個方面是避免受到僞裝的帳戶服務器的欺騙,這一點和各個帳戶服務類型相關,例如Google的AuthSub,實際上AccountServer和AuthSub服務器之間的交互沒有任何祕鑰,很可能會被僞裝,可能因爲這種僞裝對Google的AuthSub服務器沒有影響,只是影響使用AuthSub服務端的程序,所以Google也不是很在意;同樣,我實現的校內網使用的OpenSocial的模式,也沒有在迴轉請求上加上密鑰,而且以這種OpenSocial APP的模式來看,也很難能夠加上什麼很保險的密鑰了;

d.至於各個帳戶服務器的安全,則是帳戶服務器提供方需要考慮的事情,因此,不在本文討論之列;

2.大家可能注意到,每一種帳戶類型,尤其是Google的AuthSub,Yahoo的BBAuth,Live ID,我除了完成用戶的登錄過程之外,還特別的費盡心機去獲取用戶的E-mail地址,Google帳戶和Live帳戶我採用讀取地址本的方法,Yahoo的賬戶我採用讀取Ymail發件人地址的方法,這些方法都需要去額外的向用戶申請信息訪問權限,我爲什麼一定要獲得用戶的E-mail地址呢?實際上這些帳戶服務本身就會提供一個獨一無二的ID的,爲什麼不直接使用這些ID作爲用戶的帳號呢?因爲我不能信任帳戶服務器提供的ID,以Yahoo的BBAuth爲例,我發現我有一次重新申請了一次新的BBAuth應用之後,發現居然每個登錄用戶的ID也都跟着變化了,雖然對於每一個應用,登錄用戶的ID還是唯一的,不過誰也不能保證不會出現需要更換域名、重新添加權限等需要重新申請應用的時候,假如重新申請權限造成每個用戶的資料丟失,問題就大了,因此,帳戶服務器提供的號稱唯一的ID是不可信的,還是獲取用戶的登錄帳號比較可靠。

3.用戶可能存在多種不同的賬戶類型,當用戶以不同的帳戶類型登錄到系統的時候,系統肯定會認爲是不同的賬戶;而且,Web應用判斷用戶是不是同一個,除了要判斷用戶的ID之外,還要加上帳戶類型,因爲不同的網站的同一個賬戶的類型肯定不能認爲是同一個用戶;

下面是本系統的兩套源碼:分別是AccountClient和AccountServer的源碼:

下載Step1.AccountServer源碼地址: http://file.ddvip.com/2008_12/1228721201_ddvip_7418.zip

下載Step1.AccountClient源碼地址: http://file.ddvip.com/2008_12/1228721231_ddvip_9225.zip

這次系列的結束只是代表第一階段的研究完成,我後面還會繼續優化此係統,並且會繼續發文探討,不過暫時不會這麼密集的發表了。

發佈了159 篇原創文章 · 獲贊 1 · 訪問量 79萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章