??xml version="1.0" encoding="utf-8" standalone="yes"?>
1 什么是单点登陆
单点dQSingle Sign OnQ,UCؓ(f) SSOQ是目前比较行的企业业务整合的解决Ҏ(gu)之一。SSO的定义是在多个应用系l中Q用户只需要登录一ơ就可以讉K所有相互信ȝ应用pȝ?br />较大的企业内部,一般都有很多的业务支持pȝ为其提供相应的管理和IT服务。例如胦(ch)务系lؓ(f)财务人员提供财务的管理、计和报表服务Qh事系lؓ(f)Z部门提供全公思h员的l护服务Q各U业务系lؓ(f)公司内部不同的业务提供不同的服务{等。这些系l的目的都是让计机来进行复杂繁琐的计算工作Q来替代人力的手工劳动,提高工作效率和质量。这些不同的pȝ往往是在不同的时期徏设v来的Q运行在不同的^CQ也许是׃同厂商开发,使用?jin)各U不同的技术和标准。如果D例说国内一著名的IT公司Q名字隐去)(j)Q内部共?0多个业务pȝQ这些系l包括两个不同版本的SAP的ERPpȝQ?2个不同类型和版本的数据库pȝQ?个不同类型和版本的操作系l,以及(qing)使用?U不同的防火墙技术,q有数十U互怸能兼容的协议和标准,你相信吗Q不要怀疑,q种情况其实非常普遍。每一个应用系l在q行?jin)数q以后,都会(x)成ؓ(f)不可替换的企业IT架构的一部分Q如下图所C?/font>
随着企业的发展,业务pȝ的数量在不断的增加,老的pȝ却不能轻易的替换Q这?x)带来很多的开销。其一是管理上的开销Q需要维护的pȝ来多。很多系l的数据是相互冗余和重复的,数据的不一致性会(x)l管理工作带来很大的压力。业务和业务之间的相x(chng)也来大Q例如公司的计费pȝ和胦(ch)务系l,财务pȝ和h事系l之间都不可避免的有着密切的关pR?br />Z(jin)降低理的消耗,最大限度的重用已有投资的系l,很多企业都在q行着企业应用集成QEAIQ。企业应用集成可以在不同层面上进行:(x)例如在数据存储层面上的“数据大集中”,在传输层面上的“通用数据交换q_”,在应用层面上的“业务流E整合”,和用L(fng)面上的“通用企业门户”等{。事实上Q还用一个层面上的集成变得越来越重要Q那是“n份认证”的整合Q也是“单点登录”?br />通常来说Q每个单独的pȝ都会(x)有自q安全体系和n份认证系l。整合以前,q入每个pȝ都需要进行登录,q样的局面不仅给理上带来了(jin)很大的困难,在安全方面也埋下?jin)重大的隐(zhn)。下面是一些著名的调查公司昄的统计数据:(x)
用户每天q_ 16 分钟花在w䆾验证d?- 资料来源Q?IDS
频繁?IT 用户q_?21 个密?- 资料来源Q?NTA Monitor Password Survey
49% 的h写下?jin)其密码Q?67% 的h很少改变它们
?79 U出C赯n份被H事?- 资料来源QNational Small Business Travel Assoc
全球ƺ骗损失每年U?12B - 资料来源QComm Fraud Control Assoc
?2007 q_(d)w䆾理?jng)场成倍增长至 $4.5B - 资料来源QIDS
(tng)
使用“单点登录”整合后Q只需要登录一ơ就可以q入多个pȝQ而不需要重新登录,q不仅仅带来?jin)更好的用户体验Q更重要的是降低?jin)安全的风险和管理的消耗。请看下面的l计数据Q?br />提高 IT 效率Q对于每 1000 个受用P每用户可节省$70K
帮助台呼叫减至?/3Q对?10K 员工的公司,每年可以节省每用?$75Q或者合?$648K
生力提高:(x)每个新员工可节省 $1KQ每个老员工可节省 $350 K资料来源:(x)Giga
ROI 回报Q?.5 ?13 个月 K资料来源:(x)Gartner
(tng)
另外Q用“单点登录”还是SOA时代的需求之一。在面向服务的架构中Q服务和服务之间Q程序和E序之间的通讯大量存在Q服务之间的安全认证是SOA应用的难点之一Q应此徏立“单点登录”的pȝ体系能够大大化SOA的安全问题,提高服务之间的合作效率?br />2 单点登陆的技术实现机?br />随着SSO技术的行QSSO的品也是满天飞扬。所有著名的软g厂商都提供了(jin)相应的解x(chng)案。在q里我ƈ不想介绍自己公司QSun MicrosystemsQ的产品Q而是对SSO技术本w进行解析,q且提供自己开发这一cM品的Ҏ(gu)和简单演C。有x(chng)写这文章的目的Q请参考我的博客(http://yuwang881.blog.sohu.com/3184816.htmlQ?br />单点d的机制其实是比较单的Q用一个现实中的例子做比较。颐和园是北京著名的旅游景点Q也是我常去的地斏V在颐和园内部有许多独立的景点,例如“苏州街”、“佛(jng)香阁”和“d和园”,都可以在各个景点门口单独买票。很多游客需要游玩所有d景点Q这U买方式很不方便,需要在每个景点门口排队买票Q钱包拿q拿出的Q容易丢失,很不安全。于是绝大多数游客选择在大门口C张通票Q也叫套)(j)Q就可以玩遍所有的景点而不需要重新再买票。他们只需要在每个景点门口出示一下刚才买的套就能够被允许进入每个独立的景点?br />单点d的机制也一P如下图所C,当用L(fng)一ơ访问应用系l?的时候,因ؓ(f)q没有登录,?x)被引导到认证系l中q行dQ?Q;Ҏ(gu)用户提供的登录信息,认证pȝq行w䆾效验Q如果通过效验Q应该返回给用户一个认证的凭据Q-ticketQ?Q;用户再访问别的应用的时候(3Q?Q就?x)将q个ticket带上Q作p证的凭据Q应用系l接受到h之后?x)把ticket送到认证pȝq行效验Q检查ticket的合法性(4Q?Q。如果通过效验Q用户就可以在不用再ơ登录的情况下访问应用系l?和应用系l??jin)?/font>
从上面的视图可以看出Q要实现SSOQ需要以下主要的功能Q?br />所有应用系l共享一个n份认证系l?br />l一的认证系l是SSO的前提之一。认证系l的主要功能是将用户的登录信息和用户信息库相比较Q对用户q行d认证Q认证成功后Q认证系l应该生成统一的认证标志(ticketQ,q还l用戗另外,认证pȝq应该对ticketq行效验Q判断其有效性?
所有应用系l能够识别和提取ticket信息
要实现SSO的功能,让用户只d一ơ,必让应用pȝ能够识别已经dq的用户。应用系l应该能对ticketq行识别和提取,通过与认证系l的通讯Q能自动判断当前用户是否dq,从而完成单点登录的功能?
(tng)
上面的功能只是一个非常简单的SSO架构Q在现实情况下的SSO有着更加复杂的结构。有两点需要指出的是:(x)
单一的用户信息数据库q不是必ȝQ有许多pȝ不能所有的用户信息都集中存储,应该允许用户信息攄在不同的存储中,如下图所C。事实上Q只要统一认证pȝQ统一ticket的生和效验Q无论用户信息存储在什么地方,都能实现单点d?
(tng)
l一的认证系lƈ不是说只有单个的认证服务器,如下图所C,整个pȝ可以存在两个以上的认证服务器Q这些服务器甚至可以是不同的产品。认证服务器之间要通过标准的通讯协议Q互怺换认证信息,p完成更高U别的单点登录。如下图Q当用户在访问应用系l?Ӟq一个认证服务器q行认证后,得到由此服务器生的ticket。当他访问应用系l?的时候,认证服务?能够识别此ticket是由W一个服务器产生的,通过认证服务器之间标准的通讯协议Q例如SAMLQ来交换认证信息Q仍然能够完成SSO的功能?
(tng)
3 WEB-SSO的实?br />随着互联|的高速发展,W(xu)EB应用几乎l治?jin)绝大部分的软g应用pȝQ因此WEB-SSO是SSO应用当中最为流行。WEB-SSO有其自n的特点和优势Q实现v来比较简单易用。很多商业Y件和开源Y仉有对WEB-SSO的实现。其中值得一提的是OpenSSO Q?a >https://opensso.dev.java.netQ,为用Java实现WEB-SSO提供架构指南和服务指南,为用戯己来实现WEB-SSO提供?jin)理论的依据和实现的?gu)?br />Z么说WEB-SSO比较Ҏ(gu)实现呢?q是有WEB应用自n的特点决定的?br />众所周知QW(xu)eb协议Q也是HTTPQ是一个无状态的协议。一个Web应用由很多个Web面l成Q每个页面都有唯一的URL来定义。用户在览器的地址栏输入页面的URLQ浏览器׃(x)向Web Serverd送请求。如下图Q浏览器向Web服务器发送了(jin)两个hQ申请了(jin)两个面。这两个面的请求是分别使用?jin)两个单独的HTTPq接。所谓无状态的协议也就是表现在q里Q浏览器和W(xu)eb服务器会(x)在第一个请求完成以后关闭连接通道Q在W二个请求的时候重新徏立连接。Web服务器ƈ不区分哪个请求来自哪个客L(fng)Q对所有的h都一视同仁,都是单独的连接。这L(fng)方式大大区别于传l的QClient/ServerQC/Sl构,在那L(fng)应用中,客户端和服务器端?x)徏立一个长旉的专用的q接通道。正是因为有?jin)无状态的Ҏ(gu),每个q接资源能够很快被其他客L(fng)所重用Q一台Web服务器才能够同时服务于成千上万的客户端?/font>
但是我们通常的应用是有状态的。先不用提不同应用之间的SSOQ在同一个应用中也需要保存用L(fng)dw䆾信息。例如用户在讉K面1的时候进行了(jin)dQ但是刚才也提到Q客L(fng)的每个请求都是单独的q接Q当客户再次讉K面2的时候,如何才能告诉Web服务器,客户刚才已经dq了(jin)呢?览器和服务器之间有U定Q通过使用cookie技术来l护应用的状态。Cookie是可以被Web服务器设|的字符Ԍq且可以保存在浏览器中。如下图所C,当浏览器讉K?jin)页?Ӟweb服务器设|了(jin)一个cookieQƈ这个cookie和页?一赯回给览器,览器接到cookie之后Q就?x)保存v来,在它讉K面2的时候会(x)把这个cookie也带上,W(xu)eb服务器接到请求时也能dcookie的|Ҏ(gu)cookie值的内容可以判断和恢复一些用L(fng)信息状态?/font>
Web-SSO完全可以利用Cookiel束来完成用L(fng)录信息的保存Q将览器中的Cookie和上文中的Ticketl合hQ完成SSO的功能?br /> (tng)
Z(jin)完成一个简单的SSO的功能,需要两个部分的合作Q?br />l一的n份认证服务?
修改Web应用Q得每个应用都通过q个l一的认证服务来q行w䆾效验?
3.1 Web SSO 的样?br />Ҏ(gu)上面的原理,我用J2EE的技术(JSP和ServletQ完成了(jin)一个具有Web-SSO的简单样例。样例包含一个n份认证的服务器和两个单的Web应用Q得这两个 Web应用通过l一的n份认证服务来完成Web-SSO的功能。此样例所有的源代码和二进制代码都可以从网站地址http://gceclub.sun.com.cn/wangyu/ 下蝲?br /> (tng)
样例下蝲、安装部|和q行指南Q?br />Web-SSO的样例是׃个标准Web应用l成Q压~成三个zip文gQ从http://gceclub.sun.com.cn/wangyu/web-sso/中下载。其中SSOAuthQ?a >http://gceclub.sun.com.cn/wangyu/web-sso/SSOAuth.zipQ是w䆾认证服务QSSOWebDemo1Q?a >http://gceclub.sun.com.cn/wangyu/web-sso/SSOWebDemo1.zipQ和SSOWebDemo2Q?a >http://gceclub.sun.com.cn/wangyu/web-sso/SSOWebDemo2.zipQ是两个用来演示单点d的Web应用。这三个Web应用之所以没有打成war包,是因为它们不能直接部|ԌҎ(gu)读者的部v环境需要作出小的修改。样例部|和q行的环境有一定的要求Q需要符合Servlet2.3以上标准的J2EE容器才能q行Q例如Tomcat5,Sun Application Server 8, Jboss 4{)(j)。另外,w䆾认证服务需要JDK1.5的运行环境。之所以要用JDK1.5是因为笔者用了(jin)一个线E安全的高性能的Java集合cZConcurrentMap”,只有在JDK1.5中才有?
q三个Web应用完全可以单独部vQ它们可以分别部|在不同的机器,不同的操作系l和不同的J2EE的品上Q它们完全是标准的和q_无关的应用。但是有一个限Ӟ那两台部|应用(demo1、demo2Q的机器的域名需要相同,q在后面的章节中?x)解释到cookie和domain的关pM?qing)如何制作跨域的WEB-SSO
解压~SSOAuth.zip文gQ在/WEB-INF/下的web.xml中请修改“domainname”的属性以反映实际的应用部|情况,domainname需要设|ؓ(f)两个单点d的应用(demo1和demo2Q所属的域名。这个domainname和当前SSOAuth服务部v的机器的域名没有关系。我~省讄的是?sun.com”。如果你部vdemo1和demo2的机器没有域名,误入IP地址或主机名Q如localhostQ,但是如果使用IP地址或主机名也就意味着demo1和demo2需要部|到一台机器上?jin)。设|完后,Ҏ(gu)你所选择的J2EE容器Q可能需要将SSOAuthq个目录压羃打包成war文g。用“jar -cvf SSOAuth.war SSOAuth/”就可以完成q个功能?
解压~SSOWebDemo1和SSOWebDemo2文gQ分别在它们/WEB-INF/下找到web.xml文gQ请修改其中的几个初始化参数
<init-param>
<param-name>SSOServiceURL</param-name>
<param-value>http://wangyu.prc.sun.com:8080/SSOAuth/SSOAuth</param-value>
</init-param>
<init-param>
<param-name>SSOLoginPage</param-name>
<param-value>http://wangyu.prc.sun.com:8080/SSOAuth/login.jsp</param-value>
</init-param>
其中的SSOServiceURL和SSOLoginPage修改成部|SSOAuth应用的机器名、端口号以及(qing)根\径(~省是SSOAuthQ以反映实际的部|情c(din)设|完后,Ҏ(gu)你所选择的J2EE容器Q可能需要将SSOWebDemo1和SSOWebDemo2q两个目录压~打包成两个war文g。用“jar -cvf SSOWebDemo1.war SSOWebDemo1/”就可以完成q个功能?
误入第一个web应用的测试URLQtest.jspQ?例如http://wangyu.prc.sun.com:8080/ SSOWebDemo1/test.jspQ如果是W一ơ访问,便会(x)自动跌{到登录界面,如下?/font>
(tng)
使用pȝ自带的三个帐号之一dQ例如,用户名:(x)wangyu,密码QwangyuQ,便能成功的看到test.jsp的内容:(x)昄当前用户名和Ƣ迎信息?/font>
h着在同一个浏览器中输入第二个web应用的测试URLQtest.jspQ?例如http://wangyu.prc.sun.com:8080/ SSOWebDemo2/test.jsp。你?x)发玎ͼ不需要再ơ登录就能看到test.jsp的内容,同样是显C当前用户名和欢q信息,而且Ƣ迎信息中明的昄当前的应用名Uͼdemo2Q?
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng)
3.2 WEB-SSO代码讲解
3.2.1w䆾认证服务代码解析
Web-SSO的源代码可以从网站地址http://gceclub.sun.com.cn/wangyu/web-sso/websso_src.zip下蝲。n份认证服务是一个标准的web应用Q包括一个名为SSOAuth的ServletQ一个login.jsp文g和一个failed.html。n份认证的所有服务几乎都由SSOAuth的Servlet来实C(jin)Qlogin.jsp用来昄d的页面(如果发现用户q没有登录过Q;failed.html是用来显C登录失败的信息Q如果用L(fng)用户名和密码与信息数据库中的不一P(j)?br />SSOAuth的代码如下面的列表显C,l构非常单,先看看这个Servlet的主体部分:(x)
package DesktopSSO;
(tng)
import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
(tng)
import javax.servlet.*;
import javax.servlet.http.*;
(tng)
(tng)
public class SSOAuth extends HttpServlet {
(tng) (tng) (tng)
(tng) (tng) (tng) static private ConcurrentMap accounts;
(tng) (tng) (tng) static private ConcurrentMap SSOIDs;
(tng) (tng) (tng) String cookiename="WangYuDesktopSSOID";
(tng) (tng) (tng) String domainname;
(tng) (tng) (tng)
(tng) (tng) (tng) public void init(ServletConfig config) throws ServletException {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) super.init(config);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) domainname= config.getInitParameter("domainname");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) cookiename = config.getInitParameter("cookiename");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) SSOIDs = new ConcurrentHashMap();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) accounts=new ConcurrentHashMap();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) accounts.put("wangyu", "wangyu");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) accounts.put("paul", "paul");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) accounts.put("carol", "carol");
(tng) (tng) (tng) }
(tng)
(tng) (tng) (tng) protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) PrintWriter out = response.getWriter();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String action = request.getParameter("action");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String result="failed";
(tng) (tng) (tng) (tng) (tng) (tng) (tng) if (action==null) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) handlerFromLogin(request,response);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } else if (action.equals("authcookie")){
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) String myCookie = request.getParameter("cookiename");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (myCookie != null) result = authCookie(myCookie);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) out.print(result);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) out.close();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } else if (action.equals("authuser")) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) result=authNameAndPasswd(request,response);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) out.print(result);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) out.close();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } else if (action.equals("logout")) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) String myCookie = request.getParameter("cookiename");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) logout(myCookie);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) out.close();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) }
(tng)
.....
(tng)
}
(tng)
从代码很Ҏ(gu)看出QSSOAuth是一个简单的Servlet。其中有两个?rn)态成员变量:(x)accounts和SSOIDsQ这两个成员变量都用了(jin)JDK1.5中线E安全的MAPc:(x) ConcurrentMapQ所以这个样例一定要JDK1.5才能q行。Accounts用来存放用户的用户名和密码,在init()的方法中可以看到我给pȝd?jin)三个合法的用户。在实际应用中,accounts应该是去数据库中或LDAP中获得,Z(jin)单v见,在本样例中我使用?jin)ConcurrentMap在内存(sh)用程序创Z(jin)三个用户。而SSOIDs保存?sh)(jin)在用户成功的登录后所产生的cookie和用户名的对应关pR它的功能显而易见:(x)当用h功登录以后,再次讉K别的pȝQؓ(f)?jin)鉴别这个用戯求所带的cookie的有效性,需要到SSOIDs中检查这L(fng)映射关系是否存在?br /> (tng)
在主要的h处理Ҏ(gu)processRequest()中,可以很清楚的看到SSOAuth的所有功?br />如果用户q没有登录过Q是W一ơ登录本pȝQ会(x)被蟩转到l(f)ogin.jsp面Q在后面?x)解释如何蟩转?j)。用户在提供?jin)用户名和密码以后,׃?x)用handlerFromLogin()q个Ҏ(gu)来验证?
如果用户已经dq本pȝQ再讉K别的应用的时候,是不需要再ơ登录的。因为浏览器?x)将W一ơ登录时产生的cookie和请求一起发送。效验cookie的有效性是SSOAuth的主要功能之一?
SSOAuthq能直接效验非login.jsp面q来的用户名和密码的效验h。这个功能是用于非web应用的SSOQ这在后面的桌面SSO中会(x)用到?
SSOAuthq提供logout服务?
(tng)
下面看看几个主要的功能函敎ͼ(x)
(tng)private void handlerFromLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String username = request.getParameter("username");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String password = request.getParameter("password");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String pass = (String)accounts.get(username);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) if ((pass==null)||(!pass.equals(password)))
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) getServletContext().getRequestDispatcher("/failed.html").forward(request, response);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) else {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) String gotoURL = request.getParameter("goto");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) String newID = createUID();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) SSOIDs.put(newID, username);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) Cookie wangyu = new Cookie(cookiename, newID);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) wangyu.setDomain(domainname);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) wangyu.setMaxAge(60000);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) wangyu.setValue(newID);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) wangyu.setPath("/");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) response.addCookie(wangyu);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) System.out.println("login success, goto back url:" + gotoURL);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (gotoURL != null) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) PrintWriter out = response.getWriter();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) response.sendRedirect(gotoURL);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) out.close();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } (tng) (tng)
(tng) (tng) (tng) }
handlerFromLogin()q个Ҏ(gu)是用来处理来自login.jsp的登录请求。它的逻辑很简单:(x)用戯入的用户名和密码与预先设定好的用户集合(存放在accounts中)(j)相比较,如果用户名或密码不匹配的话,则返回登录失败的面Qfailed.htmlQ,如果d成功的话Q需要ؓ(f)用户当前的session创徏一个新的IDQƈ这个ID和用户名的映关pd攑ֈSSOIDs中,最后还要将q个ID讄为浏览器能够保存的cookie倹{?br />d成功后,览器会(x)到哪个页面呢Q那我们回顾一下我们是如何使用w䆾认证服务的。一般来说我们不?x)直接访问n份服务的MURLQ包括login.jsp。n份服务是用来保护其他应用服务的,用户一般在讉K一个受SSOAuth保护的Web应用的某个URLӞ当前q个应用?x)发现当前的用户q没有登录,便强制将也页面{向SSOAuth的login.jspQ让用户d。如果登录成功后Q应该自动的用L(fng)览器指向用h初想讉K的那个URL。在handlerFromLogin()q个Ҏ(gu)中,我们通过接收“goto”这个参数来保存用户最初访问的URLQ成功后侉K新定向到q个面中?br />另外一个要说明的是Q在讄cookie的时候,我用了(jin)一个setMaxAge(6000)的方法。这个方法是用来讄cookie的有效期Q单位是U。如果不使用q个Ҏ(gu)或者参Cؓ(f)负数的话Q当览器关闭的时候,q个cookie失效了(jin)。在q里我给?jin)很大的|1000分钟Q,D的行为是Q当你关闭浏览器Q或者关机)(j)Q下ơ再打开览器访问刚才的应用Q只要在1000分钟之内Q就不需要再d?jin)。我q样做是下面要介l的桌面SSO中所需要的功能?br />其他的方法更加简单,q里׃多解释了(jin)?br /> (tng)
3.2.2hSSO功能的web应用源代码解?br />要实现WEB-SSO的功能,只有w䆾认证服务是不够的。这点很昄Q要想多个应用h单点d的功能,q需要每个应用本w的配合Q将自己的n份认证的服务交给一个统一的n份认证服务-SSOAuth。SSOAuth服务中提供的各个Ҏ(gu)是供每个加入SSO的Web应用来调用的?br />一般来_(d)W(xu)eb应用需要SSO的功能,应该通过以下的交互过E来调用w䆾认证服务的提供的认证服务Q?br />Web应用中每一个需要安全保护的URL在访问以前,都需要进行安全检查,如果发现没有dQ没有发现认证之后所带的cookieQ,重新定向到SSOAuth中的login.jspq行d?
d成功后,pȝ?x)自动给你的览器设|cookieQ证明你已经dq了(jin)?
当你再访问这个应用的需要保护的URL的时候,pȝq是要进行安全检查的Q但是这ơ系l能够发现相应的cookie?
有了(jin)q个cookieQ还?sh)能证明你就一定有权限讉K。因为有可能你已llogout,或者cookie已经q期?jin),或者n份认证服务重赯Q这些情况下Q你的cookie都可能无效。应用系l拿到这个cookieQ还需要调用n份认证的服务Q来判断cookie时候真的有效,以及(qing)当前的cookie对应的用h谁?
如果cookie效验成功Q就允许用户讉K当前h的资源?
以上q些功能Q可以用很多Ҏ(gu)来实玎ͼ(x)
在每个被讉K的资源中QJSP或ServletQ中都加入n份认证的服务Q来获得cookieQƈ且判断当前用h否登录过。不q这个笨Ҏ(gu)没有Z(x)?-)?
可以通过一个controllerQ将所有的功能都写C个servlet中,然后在URL映射的时候,映射到所有需要保护的URL集合中(例如*.jspQ?security/*{)(j)。这个方法可以用,不过Q它的缺Ҏ(gu)不能重用。在每个应用中都要部|一个相同的servlet?
Filter是比较好的方法。符合Servlet2.3以上的J2EE容器具有部|filter的功能。(F(tun)ilter的用可以参考JavaWolrd的文?a >http://www.javaworld.com/javaworld/jw-06-2001/jw-0622-filters.htmlQFilter是一个具有很好的模块化,可重用的~程APIQ用在SSO正合适不q。本样例是使用一个filter来完成以上的功能?
(tng)
package SSO;
(tng)
import java.io.*;
import java.net.*;
import java.util.*;
import java.text.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.*;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
(tng)
public class SSOFilter implements Filter {
(tng) (tng) (tng) private FilterConfig filterConfig = null;
(tng) (tng) (tng) private String cookieName="WangYuDesktopSSOID";
(tng) (tng) (tng) private String SSOServiceURL= " (tng) (tng) (tng) private String SSOLoginPage= " (tng) (tng) (tng)
(tng) (tng) (tng) public void init(FilterConfig filterConfig) {
(tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) this.filterConfig = filterConfig;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) if (filterConfig != null) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (debug) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) log("SSOFilter:Initializing filter");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } (tng) (tng) (tng) (tng) (tng) (tng) (tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) cookieName = filterConfig.getInitParameter("cookieName");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) SSOServiceURL = filterConfig.getInitParameter("SSOServiceURL");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) SSOLoginPage = filterConfig.getInitParameter("SSOLoginPage");
(tng) (tng) (tng) }
.....
.....
(tng)
}
以上的初始化的源代码有两炚w要说明:(x)一是有两个需要配|的参数SSOServiceURL和SSOLoginPage。因为当前的Web应用很可能和w䆾认证服务QSSOAuthQ不在同一台机器上Q所以需要让q个filter知道w䆾认证服务部v的URLQ这h能去调用它的服务。另外一点就是由于n份认证的服务调用是要通过http协议来调用的Q在本样例中是这栯计的Q读者完全可以设计自qw䆾服务Q用别的调用协议,如RMI或SOAP{等Q,所有笔者引用了(jin)apache的commons工具包(详细信息情访问a(chn)pache 的网?a >http://jakarta.apache.org/commons/index.htmlQ,其中的“httpclient”可以大大简化http调用的编E?br />下面看看filter的主体方法doFilter():
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) if (debug) log("SSOFilter:doFilter()");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) HttpServletRequest request = (HttpServletRequest) req;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) HttpServletResponse response = (HttpServletResponse) res;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String result="failed";
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String url = request.getRequestURL().toString();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String qstring = request.getQueryString();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) if (qstring == null) qstring ="";
(tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) //(g)查httph的head是否有需要的cookie
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String cookieValue ="";
(tng) (tng) (tng) (tng) (tng) (tng) (tng) javax.servlet.http.Cookie[] diskCookies = request.getCookies();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) if (diskCookies != null) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) for (int i = 0; i < diskCookies.length; i++) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if(diskCookies[i].getName().equals(cookieName)){
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) cookieValue = diskCookies[i].getValue();
(tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) //如果扑ֈ?jin)相应的cookie则效验其有效?br /> (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) result = SSOService(cookieValue);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (debug) log("found cookies!");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) if (result.equals("failed")) { //效验p|或没有找到cookieQ则需要登?br /> (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) response.sendRedirect(SSOLoginPage+"?goto="+url);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } else if (qstring.indexOf("logout") > 1) {//logout服务
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (debug) log("logout action!");
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) logoutService(cookieValue);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) response.sendRedirect(SSOLoginPage+"?goto="+url);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } else {//效验成功
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) request.setAttribute("SSOUser",result);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) Throwable problem = null;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) chain.doFilter(req, res);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) } catch(Throwable t) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) problem = t;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) t.printStackTrace();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) } (tng) (tng) (tng) (tng) (tng) (tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (problem != null) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (problem instanceof ServletException) throw (ServletException)problem;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (problem instanceof IOException) throw (IOException)problem;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) sendProcessingError(problem, res);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } (tng) (tng)
(tng) (tng) (tng) }
doFilter()Ҏ(gu)的逻辑也是非常单的Q在接收到请求的时候,先去查找是否存在期望的cookie|如果扑ֈ?jin),׃?x)调用SSOService(cookieValue)L验这个cookie的有效性。如果cookie效验不成功或者cookieҎ(gu)不存在,׃(x)直接转到d界面让用L(fng)录;如果cookie效验成功Q就不会(x)做Q何阻拦,让此hq行下去。在配置文g中,有下面的一个节点表CZ(jin)此filter的URL映射关系Q只拦截所有的jsph?br /><filter-mapping>
<filter-name>SSOFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
(tng)
下面q有几个主要的函数需要说明:(x)
(tng) (tng) (tng) private String SSOService(String cookievalue) throws IOException {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String authAction = "?action=authcookie&cookiename=";
(tng) (tng) (tng) (tng) (tng) (tng) (tng) HttpClient httpclient = new HttpClient();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) GetMethod httpget = new GetMethod(SSOServiceURL+authAction+cookievalue);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpclient.executeMethod(httpget);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) String result = httpget.getResponseBodyAsString();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) return result;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } finally {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpget.releaseConnection();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) }
(tng) (tng) (tng)
(tng) (tng) (tng) private void logoutService(String cookievalue) throws IOException {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String authAction = "?action=logout&cookiename=";
(tng) (tng) (tng) (tng) (tng) (tng) (tng) HttpClient httpclient = new HttpClient();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) GetMethod httpget = new GetMethod(SSOServiceURL+authAction+cookievalue);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpclient.executeMethod(httpget);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpget.getResponseBodyAsString();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } finally {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpget.releaseConnection();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) }
q两个函C要是利用apache中的httpclient讉KSSOAuth提供的认证服务来完成效验cookie和logout的功能?br />其他的函数都很简单,有很多都是我的IDEQNetBeansQ替我自动生成的?br />4 当前Ҏ(gu)的安全局限?br />当前q个WEB-SSO的方案是一个比较简单的雏ŞQ主要是用来演示SSO的概念和说明SSO技术的实现方式。有很多斚wq需要完善,其中安全性是非常重要的一个方面?br />我们说过Q采用SSO技术的主要目的之一是加强安全性,降低安全风险。因为采用了(jin)SSOQ在|络上传递密码的ơ数减少Q风险降低是昄的,但是当前的方案却有其他的安全风险。由于cookie是一个用L(fng)录的唯一凭据Q对cookie的保护措施是pȝ安全的重要环节:(x)
cookie的长度和复杂?br />在本Ҏ(gu)中,cookie是有一个固定的字符Ԍ我的姓名Q加上当前的旉戟뀂这L(fng)cookie很容易被伪造和猜测。怀有恶意的用户如果猜测到合法的cookie可以被当作已经d的用PL讉K权限范围内的资源
cookie的效验和保护
在本Ҏ(gu)中,虽然密码只要传输一ơ就够了(jin)Q可cookie在网l中是经怼来传厅R一些网l探工P如sniff, snoop,tcpdump{)(j)可以很容易捕获到cookie的数倹{在本方案中Qƈ没有考虑cookie在传输时候的保护。另外对cookie的效验也q于单,q不L查发送cookie的来源究竟是不是cookie最初的拥有者,也就是说无法区分正常的用户和仉Kcookie的用戗?
当其中一个应用的安全性不好,其他所有的应用都会(x)受到安全威胁
因ؓ(f)有SSOQ所以当某个处于 SSO的应用被黒客ȝQ那么很Ҏ(gu)ȝ其他处于同一个SSO保护的应用?
q些安全漏洞在商业的SSO解决Ҏ(gu)中都?x)有所考虑Q提供相关的安全措施和保护手D,例如Sun公司的Access ManagerQcookie的复杂读和对cookie的保护都做得非常好。另外在OpneSSO Q?a >https://opensso.dev.java.netQ的架构指南中也l出?jin)部分安全措施的解决?gu)?br />5 当前Ҏ(gu)的功能和性能局限?br />除了(jin)安全性,当前Ҏ(gu)在功能和性能上都需要很多的改进Q?br />当前所提供的登录认证模式只有一U:(x)用户名和密码Q而且Z(jin)单,用户名和密码放在内存当中。事实上Q用戯n份信息的来源应该是多U多L(fng)Q可以是来自数据库中QLDAP中,甚至于来自操作系l自w的用户列表。还有很多其他的认证模式都是商务应用不可~少的,因此SSO的解x(chng)案应该包括各U认证的模式Q包括数字证书,RadiusQ?SafeWord QMemberShipQSecurID{多U方式。最为灵zȝ方式应该允许可插入的JAAS框架来扩展n份认证的接口
我们~写的Filter只能用于J2EE的应用,而对于大量非Java的Web应用Q却无法提供SSO服务?
在将Filter应用到Web应用的时候,需要对容器上的每一个应用都要做相应的修改,重新部v。而更加流行的做法是Agent机制Qؓ(f)每一个应用服务器安装一个agentQ就可以SSO功能应用到这个应用服务器中的所有应用?
当前的方案不能支持分别位于不同domain的Web应用q行SSO。这是因为浏览器在访问Web服务器的时候,仅仅?x)带上和当前web服务器具有相同domain名称的那些cookie。要提供跨域的SSO的解x(chng)案有很多其他的方法,在这里就不多说了(jin)。Sun的Access Manager具有跨域的SSO的功能?
另外QFilter的性能问题?sh)是需要重视的斚w。因为Filter?x)截h一个符合URL映射规则的请求,获得cookieQ验证其有效性。这一pdd是比较消耗资源的Q特别是验证cookie有效性是一个远E的http的调用,来访问SSOAuth的认证服务,有一定的延时。因此在性能上需要做q一步的提高。例如在本样例中Q如果将URL映射从?jsp”改成?*”,也就是说filterҎ(gu)有的h都v作用Q整个应用会(x)变得非常慢。这是因为,面当中包含?jin)各U静(rn)态元素如gif囄Qcss样式文gQ和其他html?rn)态页面,q些面的访问都要通过filter去验证。而事实上Q这些静(rn)态元素没有什么安全上的需求,应该在filter中进行判断,不去效验q些hQ性能?x)好很多。另外,如果在filter中加上一定的cacheQ而不需要每一个cookie效验h都去q端的n份认证服务中执行Q性能也能大幅度提高?
另外pȝq需要很多其他的服务Q如在内存(sh)定时删除无用的cookie映射{等Q都是一个严肃的解决Ҏ(gu)需要考虑的问题?
6 桌面SSO的实?br />从WEB-SSO的概念g伸开Q我们可以把SSO的技术拓展到整个桌面的应用,不仅仅局限在览器。SSO的概念和原则都没有改变,只需要再做一点点的工作,可以完成桌?SSO 的应用?br />桌面SSO和W(xu)EB-SSO一P关键的技术也在于如何在用L(fng)录过后保存登录的凭据。在WEB-SSO中,d的凭据是靠浏览器的cookie机制来完成的Q在桌面应用中,可以登录的凭证保存CQ何地方,只要所有SSO的桌面应用都׃nq个凭证?br />从网站可以下载一个简单的桌面SSO的样?http://gceclub.sun.com.cn/wangyu/desktop-sso/desktopsso.zip)和全部源码(http://gceclub.sun.com.cn/wangyu/desktop-sso/desktopsso_src.zipQ,虽然单,但是它具有桌面SSO大多数的功能Q稍微加以扩充就可以成ؓ(f)自己的解x(chng)案?br /> (tng)
6.1桌面样例的部|?br />q行此桌面SSO需要三个前提条Ӟ(x)
a) WEB-SSO的n份认证应用应该正在运行,因ؓ(f)我们在桌面SSO当中需要用到统一的认证服?br />b) 当前桌面需要运行Mozilla或Netscape览器,因ؓ(f)我们ticket保存到mozilla的cookie文g?br />c) 必须在JDK1.4以上q行。(WEB-SSO需要JDK1.5以上Q?
解开desktopsso.zip文gQ里面有两个目录bin和lib?
bin目录下有一些脚本文件和配置文gQ其中config.properties包含?jin)三个需要配|的参数Q?br />a) SSOServiceURL要指向WebSSO部v的n份认证的URL
b) SSOLoginPage要指向WebSSO部v的n份认证的d面URL
c) cookiefilepath要指向当前用L(fng)mozilla所存放cookie的文?
在bin目录下还有一个login.conf是用来配|JAASd模块Q本样例提供?jin)两个,读者可以Q意选择其中一个(也可以都选)(j)Q再重新q行E序Q查看登录认证的变化
在bin下的q行脚本可能需要作相应的修?br />a) 如果是在unix下,各个jar文g需要用?”来隔开Q而不是??br />b) java q行E序需要放|在当前q行的\径下Q否则需要加上java的\径全名?
(tng)
6.2桌面样例的运?br />样例E序包含三个单的Java控制台程序,q三个程序单独运行都需要登录。如果运行第一个命叫“GameSystem”的E序Q提C需要输入用户名和密码:(x)
效验成功以后Q便?x)显C当前登录的用户的基本信息等{?/font>
(tng)q时候再q行W二个桌面Java应用QmailSystemQ的时候,׃需要再d?jin),直接显C出来刚才登录的用户?/font>
W三个应用是logoutQ运行它之后Q用户便退出系l。再讉K的时候,又需要重新登录了(jin)。请读者再制裁执行完logout之后Q重新验证一下前两个应用的SSOQ先q行W二个应用,再运行第一个,?x)看到相同的效果?br />我们的样例ƈ没有在这里停步,事实上,本样例不仅能够和在几个Java应用之间SSOQ还能和览器进行SSOQ也是浏览器也当成是桌面的一部分。这对一些行业有着不小的吸引力?br />q时候再打开Mozilla览器,讉K以前提到的那两个WEB应用Q会(x)发现只要桌面应用如果dq,W(xu)eb应用׃用再d?jin),而且能显C刚才登录的用户的信息。读者可以在几个桌面和W(xu)eb应用之间q行d和logout的试验,看看它们之间的SSO?/font>
6.3桌面样例的源码分?br />桌面SSO的样例用了(jin)JAASQ要?jin)解JAAS的详l的信息请参?a >http://java.sun.com/products/jaasQ。JAAS是对PAMQPluggable Authentication ModuleQ的Java实现Q来完成 Java应用可插拔的安全认证模块。用JAAS作ؓ(f)Java应用的安全认证模块有很多好处Q最主要的是不需要修Ҏ(gu)代码可以更换认证方式。例如原有的Java应用如果使用JAAS的认证,如果需要应用SSOQ只需要修改JAAS的配|文件就行了(jin)。现在在行的J2EE和其?Java的品中Q用L(fng)w䆾认证都是通过JAAS来完成的。在样例中,我们展CZ(jin)q个功能。请看配|文件login.conf
(tng) (tng) (tng) DesktopSSO {
(tng) (tng) desktopsso.share.PasswordLoginModule required;
(tng) (tng) desktopsso.share.DesktopSSOLoginModule required;
};
当我们注解掉W二个模块的时候,只有W一个模块v作用。在q个模块的作用下Q只有test用户Q密码是12345Q才能登录。当我们注解掉第一个模块的时候,只有W二个模块v作用Q桌面SSO才会(x)起作用?br /> (tng)
所有的Java桌面样例E序都是标准JAAS应用Q熟(zhn)JAAS的程序员?sh)(x)很快?jin)解。JAAS中主要的是登录模块(LoginModuleQ。下面是SSOd模块的源码:(x)
(tng)public class DesktopSSOLoginModule implements LoginModule {
(tng) (tng) ..........
(tng) (tng) private String SSOServiceURL = "";
(tng) (tng) private String SSOLoginPage = "";
(tng) (tng) private static String cookiefilepath = ""; (tng) (tng)
(tng) (tng) .........
(tng)
在config.properties的文件中Q我们配|了(jin)它们的|(x)
SSOServiceURL=http://wangyu.prc.sun.com:8080/SSOAuth/SSOAuth
SSOLoginPage=http://wangyu.prc.sun.com:8080/SSOAuth/login.jsp
cookiefilepath=C:\\Documents and Settings\\yw137672\\Application Data\\Mozilla\\Profiles\\default\\hog6z1ji.slt\\cookies.txt
SSOServiceURL和SSOLoginPage成员变量指向?jin)在Web-SSO中用q的w䆾认证模块QSSOAuthQ这p明在桌面pȝ中我们试囑֒W(xu)eb应用q一个认证服务。而cookiefilepath成员变量则泄露了(jin)一个“天机”:(x)我们使用?jin)Mozilla览器的cookie文g来保存登录的凭证。换句话_(d)和Mozillaq?jin)一个保存登录凭证的机制。之所以用Mozilla是应为它的Cookie文g格式单,很容易编E访问和修改L的Cookie倹{(我试图解析Internet Explorer的cookie文g但没有成功。)(j)
下面是登录模块DesktopSSOLoginModule的主体:(x)login()Ҏ(gu)。逻辑也是非常单:(x)先用Cookie来登陆,如果成功Q则直接p入系l,否则需要用戯入用户名和密码来dpȝ?br /> (tng) (tng) (tng) public boolean login() throws LoginException{
(tng) (tng) (tng) (tng) (tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) if (Cookielogin()) return true;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } catch (IOException ex) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) ex.printStackTrace();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) if (passwordlogin()) return true;
(tng) (tng) (tng) (tng) (tng) throw new FailedLoginException();
(tng)}
(tng)
下面是Cookielogin()Ҏ(gu)的实体,它的逻辑是:(x)先从Cookie文g中获得相应的Cookie|通过w䆾效验服务效验Cookie的有效性。如果cookie有效qd成功Q如果不成功或Cookie不存在,用cookiedqp|?br /> (tng) (tng) (tng) public boolean Cookielogin() throws LoginException,IOException {
(tng) (tng) (tng) (tng) (tng) String cookieValue="";
(tng) (tng) (tng) (tng) (tng) int cookieIndex =foundCookie();
(tng) (tng) (tng) (tng) (tng) if (cookieIndex<0)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) return false;
(tng) (tng) (tng) (tng) (tng) else
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) cookieValue = getCookieValue(cookieIndex);
(tng) (tng) (tng) (tng) username = cookieAuth(cookieValue);
(tng) (tng) (tng) (tng) if (! username.equals("failed")) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) loginSuccess = true;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) return true;
(tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) return false;
(tng)}
(tng)
(tng)
用用户名和密码登录的Ҏ(gu)要复杂一些,通过Callback的机制和屏幕输入输出q行信息交互Q完成用L(fng)录信息的获取Q获取信息以后通过userAuthҎ(gu)来调用远端SSOAuth的服务来判定当前d的有效性?br /> (tng) (tng) public boolean passwordlogin() throws LoginException {
(tng) (tng) (tng) //
(tng) (tng) (tng) // Since we need input from a user, we need a callback handler
(tng) (tng) (tng) if (callbackHandler == null) {
(tng) (tng) (tng) (tng) (tng) (tng) throw new LoginException("No CallbackHandler defined");
(tng) (tng) (tng) }
(tng) (tng) (tng) Callback[] callbacks = new Callback[2];
(tng) (tng) (tng) callbacks[0] = new NameCallback("Username");
(tng) (tng) (tng) callbacks[1] = new PasswordCallback("Password", false);
(tng) (tng) (tng) //
(tng) (tng) (tng) // Call the callback handler to get the username and password
(tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) callbackHandler.handle(callbacks);
(tng) (tng) (tng) (tng) (tng) username = ((NameCallback)callbacks[0]).getName();
(tng) (tng) (tng) (tng) (tng) char[] temp = ((PasswordCallback)callbacks[1]).getPassword();
(tng) (tng) (tng) (tng) (tng) password = new char[temp.length];
(tng) (tng) (tng) (tng) (tng) System.arraycopy(temp, 0, password, 0, temp.length);
(tng) (tng) (tng) (tng) (tng) ((PasswordCallback)callbacks[1]).clearPassword();
(tng) (tng) (tng) } catch (IOException ioe) {
(tng) (tng) (tng) (tng) (tng) throw new LoginException(ioe.toString());
(tng) (tng) (tng) } catch (UnsupportedCallbackException uce) {
(tng) (tng) (tng) (tng) (tng) throw new LoginException(uce.toString());
(tng) (tng) (tng) }
(tng) (tng) (tng)
(tng) (tng) (tng) System.out.println();
(tng) (tng) (tng) String authresult ="";
(tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) authresult = userAuth(username, password);
(tng) (tng) (tng) } catch (IOException ex) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) ex.printStackTrace();
(tng) (tng) (tng) }
(tng) (tng) (tng) if (! authresult.equals("failed")) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) loginSuccess= true;
(tng) (tng) (tng) (tng) (tng) (tng) (tng) clearPassword();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) updateCookie(authresult);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } catch (IOException ex) {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) ex.printStackTrace();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) return true;
(tng) (tng) (tng) }
(tng) (tng)
(tng)
(tng) (tng) (tng) loginSuccess = false;
(tng) (tng) (tng) username = null;
(tng) (tng) (tng) clearPassword();
(tng) (tng) (tng) System.out.println( "Login: PasswordLoginModule FAIL" );
(tng) (tng) (tng) throw new FailedLoginException();
(tng)}
(tng)
(tng)
CookieAuth和userAuthҎ(gu)都是利用apahce的httpclient工具包和q程的SSOAuthq行httpq接Q获取服务?br /> (tng) (tng) (tng) (tng) (tng) (tng) (tng) private String cookieAuth(String cookievalue) throws IOException{
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String result = "failed";
(tng) (tng) (tng) (tng) (tng) (tng) (tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) HttpClient httpclient = new HttpClient(); (tng) (tng) (tng) (tng) (tng) (tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) GetMethod httpget = new GetMethod(SSOServiceURL+Action1+cookievalue);
(tng) (tng) (tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpclient.executeMethod(httpget);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) result = httpget.getResponseBodyAsString();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } finally {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpget.releaseConnection();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) return result;
(tng) (tng) (tng) }
(tng)
private String userAuth(String username, char[] password) throws IOException{
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String result = "failed";
(tng) (tng) (tng) (tng) (tng) (tng) (tng) String passwd= new String(password);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) HttpClient httpclient = new HttpClient(); (tng) (tng) (tng) (tng) (tng) (tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) GetMethod httpget = new GetMethod(SSOServiceURL+Action2+username+"&password="+passwd);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) passwd = null;
(tng) (tng) (tng)
(tng) (tng) (tng) (tng) (tng) (tng) (tng) try {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpclient.executeMethod(httpget);
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) result = httpget.getResponseBodyAsString();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) } finally {
(tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) (tng) httpget.releaseConnection();
(tng) (tng) (tng) (tng) (tng) (tng) (tng) }
(tng) (tng) (tng) (tng) (tng) (tng) (tng) return result;
(tng) (tng) (tng) (tng) (tng) (tng) (tng)
(tng) (tng) (tng) }
(tng)
q有一个地斚w要补充说明的是,在本样例中,用户名和密码的输入都?x)在屏幕上显C明文。如果希望用掩码形式来显C密码,以提高安全性,请参考:(x)http://java.sun.com/developer/technicalArticles/Security/pwordmask/
7 真正安全的全方位SSO解决Ҏ(gu)QKerberos
我们的样例程序(桌面SSO和W(xu)EB-SSOQ都有一个共性:(x)要想一个应用集成到我们的SSO解决Ҏ(gu)中,或多或少的需要修改应用程序。Web应用需要配|一个我们预制的filterQ桌面应用需要加上我们桌面SSO的JAAS模块Q至要修改JAAS的配|文Ӟ(j)。可是有很多E序是没有源代码和无法修改的Q例如常用的q程通讯E序telnet和ftp{等一些操作系l自己带的常用的应用E序。这些程序是很难修改加入到我们的SSO的解x(chng)案中?br />事实上有一U全方位的SSO解决Ҏ(gu)能够解决q些问题Q这是Kerberos协议QRFC 1510Q。Kerberos是网l安全应用标?http://web.mit.edu/kerberos/)Q由MIT学校发明Q被L的操作系l所采用。在采用kerberos的^CQ登录和认证是由操作pȝ本n来维护,认证的凭证也由操作系l来保存Q这h个桌面都可以处于同一个SSO的系l保护中。操作系l中的各个应用(如ftp,telnetQ只需要通过配置p加入到SSO中。另外用Kerberos最大的好处在于它的安全性。通过密钥法的保证和密钥中心(j)的徏立,可以做到用户的密码根本不需要在|络中传输,而传输的信息也会(x)十分的安全?br />目前支持Kerberos的操作系l包括Solaris, windows,Linux{等L的^台。只不过要搭Z个Kerberos的环境比较复杂,KDCQ密钥分发中?j)?j)的徏立也需要相当的步骤。Kerberos拥有非常成熟的APIQ包括Java的API。用Java Generic Security Services(GSS) APIq且使用JAAS中对Kerberos的支持(详细信息请参见Sun的Java&Kerberos教程http://java.sun.com/ j2se/1.5.0/docs/guide/security/jgss/tutorials/index.htmlQ,要将我们q个样例攚w成对Kerberos的支持也是不隄?值得一提的是在JDK6.0 Q?a >http://www.java.net/download/jdk6Q当中直接就包含?jin)对GSS的支持,不需要单独下载GSS的包?br /> (tng)
8 ȝ
本文的主要目的是阐述SSO的基本原理,q提供了(jin)一U实现的方式。通过Ҏ(gu)代码的分析来掌握开发SSO服务的技术要点和充分理解SSO的应用范围。但是,本文仅仅说明?jin)n份认证的服务Q而另外一个和w䆾认证密不可分的服?---权限效验Q却没有提到。要开发出真正的SSO的品,在功能上、性能上和安全上都必须有更加完备的考虑?br />作者简?br />王昱是Sun中国工程研究院的Java工程师,现在的主要负责全球合作伙伴的技术支持。作Z名Java资深工程师和架构师,王昱在Java 的很多领域都有多q的造诣Q特别是在Java虚拟机、J2EE技?包括EJB, JSP/Servlet, JMS和W(xu)eb services{技?、集技术和Java应用性能调优?sh)有着较ؓ(f)丰富的经验。曾l多ơ在重要的Java?x)议发表演讲Qƈ在国际著名的Java技术站 点发表文章?