轻量级单点登录系统最佳实践(一)——目录 目录 第1章 前言 第2章 单点登录简介 第3章 WEB-SSO通常实现方式 第4章 轻量级单点登陆系统简介 第5章 轻量级单点登陆系统实现 5.1. 公共组件SSOLAB.SSOSERVER.COMPONENTS 5.2. 单点登录系统SSOLAB.SSOSERVER.WEBAPP 5.3. 企业门户系统系统演示SSOLAB.PORTAL.WEBAPP 5.4. 人力资源管理系统演示SSOLAB. APP1. WEBAPP 5.5.财务管理系统演示SSOLAB. APP2. WEBAPP 5.6. 网上办公系统演示APP3 (JAVA) 第6章 后记 轻量级单点登录系统最佳实践(二)——第1章 前言 要实现企业应用集成,就不能不解决单点登录问题。单点登录(SSO,Single Sign On) 也可称统一认证服务,就是用户只登录一次就可以访问多个应用系统而不需要重新登录。怎么解决单点登录问题,用任何一个搜索引擎,都可以找到各种解决方法,可谓是八仙过海、各显神通。本文的目的不是提供一个功能齐全、安全可靠的解决方法,而是提供一个只使用原始Web技术、与软件平台无关、与用户验证形式无关、只有用户验证功能、可以在安全性不过强求的情况下使用的方法——轻量级单点登录系统。虽然本文是使用.NET框架、C#实现的,但完全可以按照同样方法使用其它平台、其它语言来实现;虽然本文是使用用户名和密码写在代码中这种最烂的用户验证形式,但完全可以使用数据库、Active Directory等形式来验证用户。 很多单点登录解决方法,不论是技术、还是思想,都非常复杂,不管是商业的、有代码的,方方面面都考虑到了。想必很多程序员和我一样,学习Web程序开发的第一步,就是解决用户登录问题,在互联网不发达的情况下,都颇费周折,实现单点登录也和这差不多,不过绕了个圈子罢了。复杂的单点登录方法,往往都附加了很多与单点登录无关的职责,刨去这些无关的职责,就只是在一个地方进行用户登录的很小的应用系统。 轻量级单点登录系统的特性: l 轻量级; l 基于Web-SSO方案; l 只是通过代码来说明方法,与开发平台、语言无关,与用户验证形式无关; l 不使用客户端Cookies,支持跨域认证; l 有基本的安全性; 轻量级单点登录系统最佳实践(三)——第2章 单点登录简介 目前的企业应用环境中,往往有很多的应用系统,如人力资源管理系统、办公自动化系统、财务管理系统、档案管理系统等等。这些应用系统服务于企业的信息化建设,为企业带来了很好的效益。但是,用户在使用这些应用系统时,并不方便。用户每次使用系统,都必须输入用户名称和用户密码,进行身份验证;而且,应用系统不同,用户账号就不同,用户必须同时牢记多套用户名称和用户密码。特别是对于应用系统数目较多,用户数目也很多的企业,这个问题尤为突出。问题的原因并不是系统开发出现失误,而是缺少整体规划,缺乏统一的用户登录平台。 SSO(Single Sign On,单点登录)可以解决上述问题。所谓单点登录,就是是指访问同一服务器不同应用中的受保护资源的同一用户,只需要登录一次,即通过一个应用中的安全验证后,再访问其他应用中的受保护资源时,不再需要重新登录验证。 使用SSO的好处主要有: 方便用户。用户使用应用系统时,能够一次登录,多次使用。用户不再需要每次输入用户名称和用户密码,也不需要牢记多套用户名称和用户密码。单点登录平台能够改善用户使用应用系统的体验。 方便管理员。系统管理员只需要维护一套统一的用户账号,方便、简单。相比之下,系统管理员以前需要管理很多套的用户账号。每一个应用系统就有一套用户账号,不仅给管理上带来不方便,而且,也容易出现管理漏洞。 简化应用系统开发。开发新的应用系统时,可以直接使用单点登录平台的用户认证服务,简化开发流程。单点登录平台通过提供统一的认证平台,实现单点登录。因此,应用系统并不需要开发用户认证程序。 单点登录实现机制比较简单,单点登录的实质就是安全上下文(Security Context)或凭证(Credential)在多个应用系统之间的传递或共享。 ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_1.gif)
如上图所示,当用户第一次访问应用系统1的时候,因为还没有登,会被引导到认证系统中进行登录(1);根据用户提供的登录信息,认证系统进行身份效验,如果通过效验,应该返回给用户一个认证的凭据--ticket(2);用户再访问别的应用的时候(3,5)就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行效验,检查ticket的合法性(4,6)。如果通过效验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。 要实现SSO,需要以下主要的功能: 所有应用系统共享一个身份认证系统。统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行效验,判断其有效性。 所有应用系统能够识别和提取ticket信息。要实现SSO的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能。 轻量级单点登录系统最佳实践(四)——第3章 Web-SSO通常实现方式 随着互联网的高速发展,WEB应用几乎统治了绝大部分的软件应用系统,因此WEB-SSO是SSO应用当中最为流行。WEB-SSO有其自身的特点和优势,实现起来比较简单易用。 众所周知,Web协议(也就是HTTP)是一个无状态的协议。一个Web应用由很多个Web页面组成,每个页面都有唯一的URL来定义。用户在浏览器的地址栏输入页面的URL,浏览器就会向Web Server去发送请求。如下图,浏览器向Web服务器发送了两个请求,申请了两个页面。这两个页面的请求是分别使用了两个单独的HTTP连接。所谓无状态的协议也就是表现在这里,浏览器和Web服务器会在第一个请求完成以后关闭连接通道,在第二个请求的时候重新建立连接。Web服务器并不区分哪个请求来自哪个客户端,对所有的请求都一视同仁,都是单独的连接。这样的方式大大区别于传统的(Client/Server)C/S结构,在那样的应用中,客户端和服务器端会建立一个长时间的专用的连接通道。正是因为有了无状态的特性,每个连接资源能够很快被其他客户端所重用,一台Web服务器才能够同时服务于成千上万的客户端。 ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_2.gif)
但是我们通常的应用是有状态的。先不用提不同应用之间的SSO,在同一个应用中也需要保存用户的登录身份信息。例如用户在访问页面1的时候进行了登录,但是刚才也提到,客户端的每个请求都是单独的连接,当客户再次访问页面2的时候,如何才能告诉Web服务器,客户刚才已经登录过了呢?浏览器和服务器之间有约定:通过使用cookie技术来维护应用的状态。Cookie是可以被Web服务器设置的字符串,并且可以保存在浏览器中。如下图所示,当浏览器访问了页面1时,web服务器设置了一个cookie,并将这个cookie和页面1一起返回给浏览器,浏览器接到cookie之后,就会保存起来,在它访问页面2的时候会把这个cookie也带上,Web服务器接到请求时也能读出cookie的值,根据cookie值的内容就可以判断和恢复一些用户的信息状态。 ![](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_3.gif)
Web-SSO完全可以利用Cookie结束来完成用户登录信息的保存,将浏览器中的Cookie和上文中的Ticket结合起来,完成SSO的功能。 为了完成一个简单的SSO的功能,需要两个部分的合作: l 统一的身份认证服务。 l 修改Web应用,使得每个应用都通过这个统一的认证服务来进行身份效验。 轻量级单点登录系统最佳实践(五)——第4章 轻量级单点登陆系统简介 轻量级单点登录系统解决方案包括以下项目: l 公共组件SSOLab.SSOServer.Components l 单点登录系统SSOLab.SSOServer.WebApp l 企业门户系统系统演示SSOLab.Portal.WebApp l 人力资源管理系统演示SSOLab. APP1. WebApp l 财务管理系统演示SSOLab. APP2. WebApp l 网上办公系统演示App3 (Java) Visual Studio 2008解决方案图 ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_4.jpeg)
Eclipse项目图 ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_5.jpeg)
整个解决方案运行过程如下: 1、访问企业门户系统http://localhost:7772/Portal/Default.aspx。 由于用户还没有在单点登录系统上登录过,所以跳转到单点登录系统用户登录页面http://localhost:7771/SSOSite/SignIn.aspx ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_6.jpeg)
2、输入正确的用户名和密码,跳转到企业门户系统首页面http://localhost:7772/Portal/Default.aspx,显示当前登陆用户的用户名和应用系统地址 ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_7.jpeg)
3.选择人力资源管理系统,打开人力资源管理系统首页面http://localhost:7773/App1/Default.aspx,显示当前登陆用户的用户名。 ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_8.jpeg)
4.选择财务管理系统,打开财务管理系统首页面http://localhost:7774/App2/Default.aspx,显示当前登陆用户的用户名。 ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_9.jpeg)
4.选择网上办公系统,打开网上办公系统首页面http://localhost:8080/App3/default.jsp,显示当前登陆用户的用户名。 ![轻量级单点登录系统最佳实践(上)](http://image21.360doc.com/DownloadImg/2010/12/2323/7914589_10.jpeg)
轻量级单点登录系统最佳实践(六)——5.1. 公共组件SSOLab.SSOServer.Components l Application.cs应用系统类。属性:ID、名称、单点登录秘钥。 l ApplicationService.cs应用系统服务类。方法:根据名称获取应用系统。为了叙述简便,直接把应用系统的信息写入代码中。 l User.cs用户类。属性:ID、用户名、密码。 l UserService.cs用户服务类。方法:验证用户、根据ID获取用户、根据用户名获取用户。为了叙述简便,直接把用户的信息写入代码中。 l SSOUtil.cs单点登录工具类。静态方法:获取随机字符串、DES加密、DES解密、获取网站地址 Application.cs
using 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 = "门户系统"; application.SSOKey = "Xj1wD4DT7UicRVxOBJdjAg2AjErHkoEDlB9GqMJtYMwbfbnc9slagStcVt0Y3lY0XVKDnn6nO9cnCPDwM0tJU6iCBlWEoomDfjAjhobLurOxHR8ua8a25NGNQXQ1Q34X";
return application; } else if (name == "app1") { Application application = new Application(); application.ID = "1466B140-D840-430a-9598-619D6888E9DB"; application.Name = "人力资源管理系统"; application.SSOKey = "XD0cEmXD0IcmYD0gBmYE0OdnYE1jHnZE1USnZF3y3GYpm93Gjp2s2GSog32FfoDm2FZoaG2FZnmhpkVBlJXkWB1eGkWCqA2lWC7k2lXCw1dlXDMqolZr805InrQk4Ixq";
return application; } else if (name == "app2") { Application application = new Application(); application.ID = "2A5F9A65-7490-480e-836F-DF18608D617B"; application.Name = "财务管理系统"; application.SSOKey = "XBtyndN8yHpZCiM1eO9XtE1qii9Oey17CYosH8cM7nRnXBIBjdN811pZrtw1PfhcBDyq7S9OeHcGmWAR7ycM7aloXBCsXQhe10FgrBEwPfSndDZpGwxbL55ymWAmhycM";
return application; } else if (name == "app3") { Application application = new Application(); application.ID = "F788FED3-32EE-470a-8DDF-0E6AD8A2FCEC"; application.Name = "网上办公系统"; application.SSOKey = "XJbbaaAnnQC67829OLkEKwgwiZL30oegpTbptQG0SLQG97665k4O32bb5CQdnffggufXJmBW16nZesssc2AOJl6bO0wiZLiu7k7FTbq27d0CdUG9110ykINvggh5CRjn";
return application; } else { return null; } } } } User.cs 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); } } } 轻量级单点登录系统最佳实践(七)——5.2. 单点登录系统SSOLab.SSOServer.WebApp l SignIn.aspx单点登录系统登录页面。 l SignIn.aspx.cs单点登录系统登录页面后台代码。用户登录成功后,返回应用系统相应页面。 l SignOut.aspx单点登录系统注销页面。 l SignOut.aspx.cs单点登录系统注销页面后台代码。 l SSOContext.aspx单点登录系统上下文页面。 l SSOContext.aspx.cs单点登录系统上下文页面后台代码。根据应用系统请求返回相应信息,其中用户信息为加密形式,每个应用系统采用不同的秘钥。 SignIn.aspx <%@ 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./TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www./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> SignIn.aspx.cs 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("登录失败!"); } } } }
SignOut.aspx <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SignOut.aspx.cs" Inherits="SSOLab.SSOServer.WebApp.SignOut" %>
SignOut.aspx.cs 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); } } }
SSOContext.aspx <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SSOContext.aspx.cs" Inherits="SSOLab.SSOServer.WebApp.SSOContext1" %>
|