輕量級單點登錄系統(tǒng)最佳實踐(一)——目錄 目錄 第1章 前言 第2章 單點登錄簡介 第3章 WEB-SSO通常實現(xiàn)方式 第4章 輕量級單點登陸系統(tǒng)簡介 第5章 輕量級單點登陸系統(tǒng)實現(xiàn) 5.1. 公共組件SSOLAB.SSOSERVER.COMPONENTS 5.2. 單點登錄系統(tǒng)SSOLAB.SSOSERVER.WEBAPP 5.3. 企業(yè)門戶系統(tǒng)系統(tǒng)演示SSOLAB.PORTAL.WEBAPP 5.4. 人力資源管理系統(tǒng)演示SSOLAB. APP1. WEBAPP 5.5.財務(wù)管理系統(tǒng)演示SSOLAB. APP2. WEBAPP 5.6. 網(wǎng)上辦公系統(tǒng)演示APP3 (JAVA) 第6章 后記 輕量級單點登錄系統(tǒng)最佳實踐(二)——第1章 前言 要實現(xiàn)企業(yè)應(yīng)用集成,就不能不解決單點登錄問題。單點登錄(SSO,Single Sign On) 也可稱統(tǒng)一認(rèn)證服務(wù),就是用戶只登錄一次就可以訪問多個應(yīng)用系統(tǒng)而不需要重新登錄。怎么解決單點登錄問題,用任何一個搜索引擎,都可以找到各種解決方法,可謂是八仙過海、各顯神通。本文的目的不是提供一個功能齊全、安全可靠的解決方法,而是提供一個只使用原始Web技術(shù)、與軟件平臺無關(guān)、與用戶驗證形式無關(guān)、只有用戶驗證功能、可以在安全性不過強(qiáng)求的情況下使用的方法——輕量級單點登錄系統(tǒng)。雖然本文是使用.NET框架、C#實現(xiàn)的,但完全可以按照同樣方法使用其它平臺、其它語言來實現(xiàn);雖然本文是使用用戶名和密碼寫在代碼中這種最爛的用戶驗證形式,但完全可以使用數(shù)據(jù)庫、Active Directory等形式來驗證用戶。 很多單點登錄解決方法,不論是技術(shù)、還是思想,都非常復(fù)雜,不管是商業(yè)的、有代碼的,方方面面都考慮到了。想必很多程序員和我一樣,學(xué)習(xí)Web程序開發(fā)的第一步,就是解決用戶登錄問題,在互聯(lián)網(wǎng)不發(fā)達(dá)的情況下,都頗費周折,實現(xiàn)單點登錄也和這差不多,不過繞了個圈子罷了。復(fù)雜的單點登錄方法,往往都附加了很多與單點登錄無關(guān)的職責(zé),刨去這些無關(guān)的職責(zé),就只是在一個地方進(jìn)行用戶登錄的很小的應(yīng)用系統(tǒng)。 輕量級單點登錄系統(tǒng)的特性: l 輕量級; l 基于Web-SSO方案; l 只是通過代碼來說明方法,與開發(fā)平臺、語言無關(guān),與用戶驗證形式無關(guān); l 不使用客戶端Cookies,支持跨域認(rèn)證; l 有基本的安全性; 輕量級單點登錄系統(tǒng)最佳實踐(三)——第2章 單點登錄簡介 目前的企業(yè)應(yīng)用環(huán)境中,往往有很多的應(yīng)用系統(tǒng),如人力資源管理系統(tǒng)、辦公自動化系統(tǒng)、財務(wù)管理系統(tǒng)、檔案管理系統(tǒng)等等。這些應(yīng)用系統(tǒng)服務(wù)于企業(yè)的信息化建設(shè),為企業(yè)帶來了很好的效益。但是,用戶在使用這些應(yīng)用系統(tǒng)時,并不方便。用戶每次使用系統(tǒng),都必須輸入用戶名稱和用戶密碼,進(jìn)行身份驗證;而且,應(yīng)用系統(tǒng)不同,用戶賬號就不同,用戶必須同時牢記多套用戶名稱和用戶密碼。特別是對于應(yīng)用系統(tǒng)數(shù)目較多,用戶數(shù)目也很多的企業(yè),這個問題尤為突出。問題的原因并不是系統(tǒng)開發(fā)出現(xiàn)失誤,而是缺少整體規(guī)劃,缺乏統(tǒng)一的用戶登錄平臺。 SSO(Single Sign On,單點登錄)可以解決上述問題。所謂單點登錄,就是是指訪問同一服務(wù)器不同應(yīng)用中的受保護(hù)資源的同一用戶,只需要登錄一次,即通過一個應(yīng)用中的安全驗證后,再訪問其他應(yīng)用中的受保護(hù)資源時,不再需要重新登錄驗證。 使用SSO的好處主要有: 方便用戶。用戶使用應(yīng)用系統(tǒng)時,能夠一次登錄,多次使用。用戶不再需要每次輸入用戶名稱和用戶密碼,也不需要牢記多套用戶名稱和用戶密碼。單點登錄平臺能夠改善用戶使用應(yīng)用系統(tǒng)的體驗。 方便管理員。系統(tǒng)管理員只需要維護(hù)一套統(tǒng)一的用戶賬號,方便、簡單。相比之下,系統(tǒng)管理員以前需要管理很多套的用戶賬號。每一個應(yīng)用系統(tǒng)就有一套用戶賬號,不僅給管理上帶來不方便,而且,也容易出現(xiàn)管理漏洞。 簡化應(yīng)用系統(tǒng)開發(fā)。開發(fā)新的應(yīng)用系統(tǒng)時,可以直接使用單點登錄平臺的用戶認(rèn)證服務(wù),簡化開發(fā)流程。單點登錄平臺通過提供統(tǒng)一的認(rèn)證平臺,實現(xiàn)單點登錄。因此,應(yīng)用系統(tǒng)并不需要開發(fā)用戶認(rèn)證程序。 單點登錄實現(xiàn)機(jī)制比較簡單,單點登錄的實質(zhì)就是安全上下文(Security Context)或憑證(Credential)在多個應(yīng)用系統(tǒng)之間的傳遞或共享。 如上圖所示,當(dāng)用戶第一次訪問應(yīng)用系統(tǒng)1的時候,因為還沒有登,會被引導(dǎo)到認(rèn)證系統(tǒng)中進(jìn)行登錄(1);根據(jù)用戶提供的登錄信息,認(rèn)證系統(tǒng)進(jìn)行身份效驗,如果通過效驗,應(yīng)該返回給用戶一個認(rèn)證的憑據(jù)--ticket(2);用戶再訪問別的應(yīng)用的時候(3,5)就會將這個ticket帶上,作為自己認(rèn)證的憑據(jù),應(yīng)用系統(tǒng)接受到請求之后會把ticket送到認(rèn)證系統(tǒng)進(jìn)行效驗,檢查ticket的合法性(4,6)。如果通過效驗,用戶就可以在不用再次登錄的情況下訪問應(yīng)用系統(tǒng)2和應(yīng)用系統(tǒng)3了。 要實現(xiàn)SSO,需要以下主要的功能: 所有應(yīng)用系統(tǒng)共享一個身份認(rèn)證系統(tǒng)。統(tǒng)一的認(rèn)證系統(tǒng)是SSO的前提之一。認(rèn)證系統(tǒng)的主要功能是將用戶的登錄信息和用戶信息庫相比較,對用戶進(jìn)行登錄認(rèn)證;認(rèn)證成功后,認(rèn)證系統(tǒng)應(yīng)該生成統(tǒng)一的認(rèn)證標(biāo)志(ticket),返還給用戶。另外,認(rèn)證系統(tǒng)還應(yīng)該對ticket進(jìn)行效驗,判斷其有效性。 所有應(yīng)用系統(tǒng)能夠識別和提取ticket信息。要實現(xiàn)SSO的功能,讓用戶只登錄一次,就必須讓應(yīng)用系統(tǒng)能夠識別已經(jīng)登錄過的用戶。應(yīng)用系統(tǒng)應(yīng)該能對ticket進(jìn)行識別和提取,通過與認(rèn)證系統(tǒng)的通訊,能自動判斷當(dāng)前用戶是否登錄過,從而完成單點登錄的功能。 輕量級單點登錄系統(tǒng)最佳實踐(四)——第3章 Web-SSO通常實現(xiàn)方式 隨著互聯(lián)網(wǎng)的高速發(fā)展,WEB應(yīng)用幾乎統(tǒng)治了絕大部分的軟件應(yīng)用系統(tǒng),因此WEB-SSO是SSO應(yīng)用當(dāng)中最為流行。WEB-SSO有其自身的特點和優(yōu)勢,實現(xiàn)起來比較簡單易用。 眾所周知,Web協(xié)議(也就是HTTP)是一個無狀態(tài)的協(xié)議。一個Web應(yīng)用由很多個Web頁面組成,每個頁面都有唯一的URL來定義。用戶在瀏覽器的地址欄輸入頁面的URL,瀏覽器就會向Web Server去發(fā)送請求。如下圖,瀏覽器向Web服務(wù)器發(fā)送了兩個請求,申請了兩個頁面。這兩個頁面的請求是分別使用了兩個單獨的HTTP連接。所謂無狀態(tài)的協(xié)議也就是表現(xiàn)在這里,瀏覽器和Web服務(wù)器會在第一個請求完成以后關(guān)閉連接通道,在第二個請求的時候重新建立連接。Web服務(wù)器并不區(qū)分哪個請求來自哪個客戶端,對所有的請求都一視同仁,都是單獨的連接。這樣的方式大大區(qū)別于傳統(tǒng)的(Client/Server)C/S結(jié)構(gòu),在那樣的應(yīng)用中,客戶端和服務(wù)器端會建立一個長時間的專用的連接通道。正是因為有了無狀態(tài)的特性,每個連接資源能夠很快被其他客戶端所重用,一臺Web服務(wù)器才能夠同時服務(wù)于成千上萬的客戶端。 但是我們通常的應(yīng)用是有狀態(tài)的。先不用提不同應(yīng)用之間的SSO,在同一個應(yīng)用中也需要保存用戶的登錄身份信息。例如用戶在訪問頁面1的時候進(jìn)行了登錄,但是剛才也提到,客戶端的每個請求都是單獨的連接,當(dāng)客戶再次訪問頁面2的時候,如何才能告訴Web服務(wù)器,客戶剛才已經(jīng)登錄過了呢?瀏覽器和服務(wù)器之間有約定:通過使用cookie技術(shù)來維護(hù)應(yīng)用的狀態(tài)。Cookie是可以被Web服務(wù)器設(shè)置的字符串,并且可以保存在瀏覽器中。如下圖所示,當(dāng)瀏覽器訪問了頁面1時,web服務(wù)器設(shè)置了一個cookie,并將這個cookie和頁面1一起返回給瀏覽器,瀏覽器接到cookie之后,就會保存起來,在它訪問頁面2的時候會把這個cookie也帶上,Web服務(wù)器接到請求時也能讀出cookie的值,根據(jù)cookie值的內(nèi)容就可以判斷和恢復(fù)一些用戶的信息狀態(tài)。 Web-SSO完全可以利用Cookie結(jié)束來完成用戶登錄信息的保存,將瀏覽器中的Cookie和上文中的Ticket結(jié)合起來,完成SSO的功能。 為了完成一個簡單的SSO的功能,需要兩個部分的合作: l 統(tǒng)一的身份認(rèn)證服務(wù)。 l 修改Web應(yīng)用,使得每個應(yīng)用都通過這個統(tǒng)一的認(rèn)證服務(wù)來進(jìn)行身份效驗。 輕量級單點登錄系統(tǒng)最佳實踐(五)——第4章 輕量級單點登陸系統(tǒng)簡介 輕量級單點登錄系統(tǒng)解決方案包括以下項目: l 公共組件SSOLab.SSOServer.Components l 單點登錄系統(tǒng)SSOLab.SSOServer.WebApp l 企業(yè)門戶系統(tǒng)系統(tǒng)演示SSOLab.Portal.WebApp l 人力資源管理系統(tǒng)演示SSOLab. APP1. WebApp l 財務(wù)管理系統(tǒng)演示SSOLab. APP2. WebApp l 網(wǎng)上辦公系統(tǒng)演示App3 (Java) Visual Studio 2008解決方案圖 Eclipse項目圖
整個解決方案運行過程如下:
1、訪問企業(yè)門戶系統(tǒng)http://localhost:7772/Portal/Default.aspx。
由于用戶還沒有在單點登錄系統(tǒng)上登錄過,所以跳轉(zhuǎn)到單點登錄系統(tǒng)用戶登錄頁面http://localhost:7771/SSOSite/SignIn.aspx
2、輸入正確的用戶名和密碼,跳轉(zhuǎn)到企業(yè)門戶系統(tǒng)首頁面http://localhost:7772/Portal/Default.aspx,顯示當(dāng)前登陸用戶的用戶名和應(yīng)用系統(tǒng)地址
3.選擇人力資源管理系統(tǒng),打開人力資源管理系統(tǒng)首頁面http://localhost:7773/App1/Default.aspx,顯示當(dāng)前登陸用戶的用戶名。
4.選擇財務(wù)管理系統(tǒng),打開財務(wù)管理系統(tǒng)首頁面http://localhost:7774/App2/Default.aspx,顯示當(dāng)前登陸用戶的用戶名。
4.選擇網(wǎng)上辦公系統(tǒng),打開網(wǎng)上辦公系統(tǒng)首頁面http://localhost:8080/App3/default.jsp,顯示當(dāng)前登陸用戶的用戶名。
輕量級單點登錄系統(tǒng)最佳實踐(六)——5.1. 公共組件SSOLab.SSOServer.Components
l Application.cs應(yīng)用系統(tǒng)類。屬性:ID、名稱、單點登錄秘鑰。 l ApplicationService.cs應(yīng)用系統(tǒng)服務(wù)類。方法:根據(jù)名稱獲取應(yīng)用系統(tǒng)。為了敘述簡便,直接把應(yīng)用系統(tǒng)的信息寫入代碼中。 l User.cs用戶類。屬性:ID、用戶名、密碼。 l UserService.cs用戶服務(wù)類。方法:驗證用戶、根據(jù)ID獲取用戶、根據(jù)用戶名獲取用戶。為了敘述簡便,直接把用戶的信息寫入代碼中。 l SSOUtil.cs單點登錄工具類。靜態(tài)方法:獲取隨機(jī)字符串、DES加密、DES解密、獲取網(wǎng)站地址 Application.cs User.cs 輕量級單點登錄系統(tǒng)最佳實踐(七)——5.2. 單點登錄系統(tǒng)SSOLab.SSOServer.WebApp l SignIn.aspx單點登錄系統(tǒng)登錄頁面。 l SignIn.aspx.cs單點登錄系統(tǒng)登錄頁面后臺代碼。用戶登錄成功后,返回應(yīng)用系統(tǒng)相應(yīng)頁面。 l SignOut.aspx單點登錄系統(tǒng)注銷頁面。 l SignOut.aspx.cs單點登錄系統(tǒng)注銷頁面后臺代碼。 l SSOContext.aspx單點登錄系統(tǒng)上下文頁面。 l SSOContext.aspx.cs單點登錄系統(tǒng)上下文頁面后臺代碼。根據(jù)應(yīng)用系統(tǒng)請求返回相應(yīng)信息,其中用戶信息為加密形式,每個應(yīng)用系統(tǒng)采用不同的秘鑰。 SignIn.aspx SignIn.aspx.cs SignOut.aspx SignOut.aspx.cs SSOContext.aspxusing System;
using System.Collections.Generic;
using System.Text;
namespace SSOLab.SSOServer.Components
{
public class Application
{
private string _id;
private string _name;
private string _ssoKey;
public string ID
{
get
{
return this._id;
}
set
{
this._id = value;
}
}
public string Name
{
get
{
return this._name;
}
set
{
this._name = value;
}
}
public string SSOKey
{
get
{
return this._ssoKey;
}
set
{
this._ssoKey = value;
}
}
public Application()
{
}
}using System;
using System.Collections.Generic;
using System.Text;
namespace SSOLab.SSOServer.Components
{
public class ApplicationService
{
public static readonly int SSO_KEY_LENGTH = 128;
public Application GetApplicationByName(string name)
{
if (name == "portal")
{
Application application = new Application();
application.ID = "C8288957-B6AA-4522-99FF-7D1E60509974";
application.Name = "門戶系統(tǒng)";
application.SSOKey = "Xj1wD4DT7UicRVxOBJdjAg2AjErHkoEDlB9GqMJtYMwbfbnc9slagStcVt0Y3lY0XVKDnn6nO9cnCPDwM0tJU6iCBlWEoomDfjAjhobLurOxHR8ua8a25NGNQXQ1Q34X";
return application;
}
else if (name == "app1")
{
Application application = new Application();
application.ID = "1466B140-D840-430a-9598-619D6888E9DB";
application.Name = "人力資源管理系統(tǒng)";
application.SSOKey = "XD0cEmXD0IcmYD0gBmYE0OdnYE1jHnZE1USnZF3y3GYpm93Gjp2s2GSog32FfoDm2FZoaG2FZnmhpkVBlJXkWB1eGkWCqA2lWC7k2lXCw1dlXDMqolZr805InrQk4Ixq";
return application;
}
else if (name == "app2")
{
Application application = new Application();
application.ID = "2A5F9A65-7490-480e-836F-DF18608D617B";
application.Name = "財務(wù)管理系統(tǒng)";
application.SSOKey = "XBtyndN8yHpZCiM1eO9XtE1qii9Oey17CYosH8cM7nRnXBIBjdN811pZrtw1PfhcBDyq7S9OeHcGmWAR7ycM7aloXBCsXQhe10FgrBEwPfSndDZpGwxbL55ymWAmhycM";
return application;
}
else if (name == "app3")
{
Application application = new Application();
application.ID = "F788FED3-32EE-470a-8DDF-0E6AD8A2FCEC";
application.Name = "網(wǎng)上辦公系統(tǒng)";
application.SSOKey = "XJbbaaAnnQC67829OLkEKwgwiZL30oegpTbptQG0SLQG97665k4O32bb5CQdnffggufXJmBW16nZesssc2AOJl6bO0wiZLiu7k7FTbq27d0CdUG9110ykINvggh5CRjn";
return application;
}
else
{
return null;
}
}
}
}using System;
using System.Collections.Generic;
using System.Text;
namespace SSOLab.SSOServer.Components
{
public class User
{
private string _id;
private string _username;
private string _password;
public string ID
{
get
{
return this._id;
}
set
{
this._id = value;
}
}
public string Username
{
get
{
return this._username;
}
set
{
this._username = value;
}
}
public string Password
{
get
{
return this._password;
}
set
{
this._password = value;
}
}
public User()
{
}
}
} UserService.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace SSOLab.SSOServer.Components
{
public class UserService
{
public bool AuthenticationUser(string username, string password, out string id)
{
User user = GetUserByName(username);
if (user != null && user.Password == password)
{
id = user.ID;
return true;
}
else
{
id = String.Empty;
return false;
}
}
public User GetUserByID(string userID)
{
if (userID == "464FA65A-0DFF-46a9-AC0B-3EF1E4CDFF94")
{
User user = new User();
user.ID = "464FA65A-0DFF-46a9-AC0B-3EF1E4CDFF94";
user.Username = "admin";
user.Password = "admin";
return user;
}
else
{
return null;
}
}
public User GetUserByName(string username)
{
if (username == "admin")
{
User user = new User();
user.ID = "464FA65A-0DFF-46a9-AC0B-3EF1E4CDFF94";
user.Username = "admin";
user.Password = "admin";
return user;
}
else
{
return null;
}
}
}SSOUtil.cs
using System;
using System.Collections.Generic;
using System.Web;
using System.Security.Cryptography;
using System.Text;
using System.IO;
namespace SSOLab.SSOServer.Components
{
public class SSOUtil
{
public static string GetRandomString(int length)
{
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++)
{
Random random = new Random(unchecked(i * (int)(DateTime.Now.Ticks)));
int ret = random.Next(122);
while (ret < 48 || (ret > 57 && ret < 65) || (ret > 90 && ret < 97))
{
ret = random.Next(122);
}
sb.Append((char)ret);
}
return sb.ToString();
}
public static string DESEncrypt(string text, string key)
{
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
des.Mode = System.Security.Cryptography.CipherMode.ECB;
des.Padding = PaddingMode.Zeros;
des.Key = ASCIIEncoding.ASCII.GetBytes(key);
byte[] inputBuffer = Encoding.GetEncoding("UTF-8").GetBytes(text);
byte[] outputBuffer = des.CreateEncryptor().TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);
return Convert.ToBase64String(outputBuffer);
}
public static string DESDecrypt(string text, string key)
{
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
des.Mode = System.Security.Cryptography.CipherMode.ECB;
des.Padding = PaddingMode.Zeros;
des.Key = ASCIIEncoding.ASCII.GetBytes(key);
byte[] inputBuffer = Convert.FromBase64String(text);
byte[] outputBuffer = des.CreateDecryptor().TransformFinalBlock(inputBuffer, 0, inputBuffer.Length);
return Encoding.GetEncoding("UTF-8").GetString(outputBuffer);
}
public static string GetSiteUrl()
{
string path = HttpContext.Current.Request.ApplicationPath;
if (path.EndsWith("/") && path.Length == 1)
{
return GetHostUrl();
}
else
{
return GetHostUrl() + path;
}
}
public static string GetHostUrl()
{
return string.Format("{0}://{1}:{2}",
HttpContext.Current.Request.Url.Scheme,
HttpContext.Current.Request.Url.Host,
HttpContext.Current.Request.Url.Port);
}
}
}<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SignIn.aspx.cs" Inherits="SSOLab.SSOServer.WebApp.SignIn" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<table width="300">
<tr>
<td>
用戶名
</td>
<td>
<asp:TextBox ID="txtUsername" runat="server" />
</td>
</tr>
<tr>
<td>
密碼
</td>
<td>
<asp:TextBox ID="txtPassword" runat="server" />
</td>
</tr>
<tr>
<td colspan="2">
<asp:Button ID="btnSignIn" runat="server" OnClick="btnSignIn_Click" Text="登錄" />
</td>
</tr>
</table>
</div>
</form>
</body>
</html>using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Security;
using SSOLab.SSOServer.Components;
namespace SSOLab.SSOServer.WebApp
{
public partial class SignIn : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void btnSignIn_Click(object sender, EventArgs e)
{
string userID;
bool passed = new UserService().AuthenticationUser(txtUsername.Text,
txtPassword.Text,
out userID);
if (passed && !String.IsNullOrEmpty(userID))
{
FormsAuthentication.SetAuthCookie(userID, true);
Session["USER_IS_LONGIN"] = true;
Session["USER_ID"] = userID;
string returnUrl = HttpUtility.UrlDecode(Request.Params["ReturnUrl"]);
Response.Redirect(returnUrl);
}
else
{
Session["USER_IS_LONGIN"] = false;
Session["USER_ID"] = String.Empty;
Response.Write("登錄失敗!");
}
}
}
}<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SignOut.aspx.cs" Inherits="SSOLab.SSOServer.WebApp.SignOut" %>
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Security;
namespace SSOLab.SSOServer.WebApp
{
public partial class SignOut : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
FormsAuthentication.SignOut();
Session.Remove("USER_IS_LONGIN");
Session.Remove("USER_ID");
string returnUrl = HttpUtility.UrlDecode(Request.Params["ReturnUrl"]);
Response.Redirect(returnUrl);
}
}
}<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SSOContext.aspx.cs" Inherits="SSOLab.SSOServer.WebApp.SSOContext1" %>