heimeiyingwang 2011-05-20
1 概述
对于单点登录的机制和原理就不在这里赘述了。本文仅是对于单点登录问题研究所得的心得进行一下总结。
想要实现单点登录可以采用的方式有很多种:
1、利用成熟的软件框架(CAS,OPENSSO等)
2、自己建设单点登录框架(像sohu的单点登录)
3 、还有就是最简单的使用URL模拟登录。但是各种方式都各有利弊。最主要的问题是大部分实现方式都需要对单点登录目标系统进行修改,或者在目标系统中放入单点登录代码。如果我们自己对目标系统没有控制能力,那么与目标系统的沟通就成为最大的阻碍。 如果可以不修改目标系统,或者可以不在目标系统放入代码就可以登录就是最理想的单点登录。基于这一问题,那么建立模仿登录请求的URL进行登录就成为相对较佳的方案。为什么说是“相对较佳”,因为利用URL进行登录,基本上都有页面的刷新或者是跳转。那么单点登录的设计将成为瓶颈,尤其是在想要设计成无刷新登录或者可以Session保持随时进入目标系统,将给系统带来更多的限制。
实际上,近期主要是针对这一问题进行了大量的研究,采用了各种方法进行尝试。但是并没有得到实质性的成果,主要存在的问题是:浏览器基于安全考虑所采用的“同源策略”(Same Origin Policy)。
下面我分别根据所采用的方法进行阐述
2 跨域解决对策
2.1Ajax
Ajax异步发送请求是最先采用的方法。原有的单点登录方式,是在目标系统放置代码。通过URL访问该代码,在由该代码发送Ajax请求进行登录。
因为该种方式,需要在目标系统放置代码,这样不能满足我们的要求,所以尝试将Ajax发送请求登录代码移到“源系统”(即具备单点登录功能的系统,就是我们的门户系统)。但是请求根本无法发出,浏览器直接就弹出提示框:“该页正在访问其控制范围之外的数据,这有些危险,是否继续"。Ajax本身实际上是通过XMLHttpRequest对象来进行数据的交互,而浏览器出于安全考虑,不允许js代码进行跨域操作,所以会警告。有一种方法,用服务器端的XmlHttpRequest代理实现跨域访问。 我们不能在浏览器端直接使用AJAX来跨域访问资源,但是在服务器端是没有这种跨域安全限制的。所以,只需要让服务器端帮我们完成“跨域访问”的工作,然后在浏览器端用AJAX获取服务器端“跨域访问”的结果就可以了。这就是所谓的在服务器端创建一个XmlHttpRequest代理,通过这个代理来访问其他域名下的资源。这里引用Yahoo! JavaScript Developer Center上的几张图来进一步说明这个方案:
使用XmlHttpRequest访问同一域名下的资源:
使用XmlHttpRequest跨域访问资源:
用服务器端的XmlHttpRequest代理来跨域访问资源:
编写服务器端XmlHttpRequest代理的具体过程就不赘述了,无非是创建一个自定义的HTTP请求。
实际上,该方法并没有从本质上解决Ajax的跨域问题。2.2 JavaScript
比较常用的一种解决跨域的方法是用动态script标签实现客户端的跨域访问。 很明显,上一个方案必须要在服务器端做相应的改动才能实现跨域访问。但是很多时候是不能改动服务器端的源代码,那么上一个方案就不能满足需求了。
我们应该能注意到,虽然浏览器有跨域访问的限制,但是我们是可以通过script标签远程引用其他域名下的脚本文件的。而且,script标签的src属性不一定必须是一个存在的js文件,也可以是一个http handler的url,只要这个http handler返回的是一个text/javascript类型的响应就可以了。
这样,我们的第二个方案就浮出水面了。这个和上个的区别就是请求是使用<script>标签来请求的,这个要求也是两个域都是由你来开发才行。原理就是JS文件注入,在本域内的a内生成一个JS标签,它的SRC指向请求的另外一个域的某个页面b,b返回数据即可,可以直接返回JS的代码。因为script的src属性是可以跨域的。具体看代码,这个也比较简单。
2.3 Iframe
Iframe具体使用情况有:
一、本域和子域的相互访问:www.aa.com和book.aa.com
二、本域和其他域的相互访问: www.aa.com和www.bb.com 用 iframe第一种情况如果想做到数据的交互,那么www.aa.com和book.aa.com必须由你来开发才可以。可以将book.aa.com用iframe添加到www.aa.com的某个页面下,在www.aa.com和iframe里面都加上document.domain = "aa.com",这样就可以统一域了,可以实现跨域访问。就和平时同一个域中镶嵌iframe一样,直接调用里面的JS就可以了。(这个办法我没有尝试,不过理论可行,而且下面的跨子域Cookie方法似乎更好)
第二种情况当两个域不同时,如果想相互调用,那么同样需要两个域都是由你来开发才可以。用iframe可以实现数据的互相调用。解决方案就是用window.location对象的hash属性。hash属性就是http://domian/web/a.htm#dshakjdhsjka里面的#dshakjdhsjka。利用JS改变hash值网页不会刷新,可以这样实现通过JS访问hash值来做到通信。不过除了IE之外其他大部分浏览器只要改变hash就会记录历史,你在前进和后退时就需要处理,非常麻烦。不过再做简单的处理时还是可以用的,具体的代码我再下面有下载。大体的过程是页面a和页面b在不同域下,b通过iframe添加到a里,a通过JS修改iframe的hash值,b里面做一个监听(因为JS只能修改hash,数据是否改变只能由b自己来判断),检测到b的hash值被修改了,得到修改的值,经过处理返回a需要的值,再来修改a的hash值(这个地方要注意,如果a本身是那种查询页面的话比如http://domian/web/a.aspx?id=3,在b中直接parent.window.location是无法取得数据的,同样报没有权限的错误,需要a把这个传过来,所以也比较麻烦),同样a里面也要做监听,如果hash变化的话就取得返回的数据,再做相应的处理。
2.4 HttpClient
下面所采用的办法是利用Apache的HttpClient进行登录。
通常,我们使用A系统中的URL进行单点登录的流程是这样的。首先创建模拟A系统中登录表单提交的URL进行登录(我们把这个URL叫做URL1)。如果登录成功的话,用A系统的正常访问URL访问该网站就可以看到已经是登录状态(我们把这个URL叫做URL2)。
其原理就是HTTP协议的原理,在我们利用URL1进行访问以后,服务器就会为该用户创建一个Session,并在响应中设置“Set-Cookie”头信息,信息中包含对应的SessionID。浏览器就会根据该信息在客户端生成Cookie。当我们再访问URL2时,浏览器就会判断该会话中是否已创建Cookie,如果已经创建就会在请求中添加Cookie头信息,信息中包含对应的SessionID。服务器在处理消息时判断SessionID是否相同,相同就认为是同一会话,同一个人。
这就是现在解决HTTP协议无状态的办法。但是这个前提是在使用同一个浏览器。就像现在的IE7,虽然可能会打开多个Tab页,但是还是同一个浏览器。利用HttpClient确实可以模拟发送请求,登录进入目标系统。但是HttpClient的原理是每建立一个链接,相当于新打开一个浏览器。那么按照上面所说流程分别访问URL1和URL2将会被模拟成打开两个浏览器,换句话说在访问URL2时,就不会带上Cookie的消息头,服务器就会认为不是同一个会话,也就不会模拟出成功的登录状态。
根据上面的描述,解决问题的症结就在于:在发送访问URL2的请求时可以带上URL1返回的Cookie信息即可。
想要在访问URL2的时候带上Cookie头信息,有以下几个办法
1、在访问URL1之后,在客户端创建Cookie
2、在访问URL2时,在消息头中加入Cookie头信息。
3、 在访问URL2时,进行URL重写,在其后加入Cookie头中的信息。通常情况下,以上三种操作都不需要我们自己做,完全可以由服务器和系统自动完成。但是我们所要处理的情况是在B系统中,通过访问A系统的URL1和URL2登录到A系统中,这样就会产生问题。
2.5 跨域Cookie
1、从B系统中访问A系统的URL1,出于同源策略的安全考虑,浏览器会禁止A系统生成Cookie。
2、很不幸的是J2EE并没有提供在请求头中加入Cookie头的方法。
3、该方法是唯一可行的,但是如果B系统引用A系统的类似于URL2的URL过多,URL重写是一块加到的工作了。
总结的来说,就是因为同源策略导致不能共享Cookie,也因此不能维持Session的联通。但是同源策略是支持跨子域Cookie的。2.6 跨子域Cookie 所谓跨子域登录,A,B站点和P站点位于同一个域下面,比如A站点为http://blog.yizhu2000.com,B站点为 http://forum.yizhu2000.com,他们和登录站点P的关系可以看到,都是属于同一个父域,yizhu2000.com,不同的是子域不同,一个为blog,一个为forum,一个是passport我们先看看最常用的非跨站点普通登录的情况,一般登录验证通过后,一般会将你的用户名和一些用户信息,通过某一密钥进行加密,写在本地,也就是一个加密的cookie,我们把这个cookie叫做--票(ticket)。
需要判断用户是否登录的页面,需要读取这个ticket,并从其中解密出用户信息,如果ticket不存在,或者无法解密,意味着用户没有登录,或者登录信息不正确,这时就要跳转到登录页面进行登录,在这里加密的作用有两个,一是防止用户信息被不怀好意者看到,二是保证ticket不会被伪造,后者其实更为重要,加密后,各个应用需要采用与加密同样的密钥进行解密,如果不知道密钥,就不能伪造出ticket,(注:加密和解密的密钥有可能不同,取决于采用什么加密算法,如果是对称加密,则为同一密钥,如果是非对称,就不同了,一般用私钥加密,公钥解密,但是无论怎样,密钥都只有内部知道,这样伪造者既无法伪造也无法解密ticket)
跨子域的单点登录,和上述普通登录的过程没有什么不同,唯一不同的是写cookie时,由于登录站点P和应用A处于不同的子域,P站写入的cookie的域为passport.yizhu2000.net,而A站点为forum.yizhu2000.net,A在判断用户登录时无法读到P站点的ticket
解决方法非常简单,当Login完成后P站点写ticket的时候,只需把cookie的域设为他们共同的父域,yizhu2000.net就可以了:cookie.domain="yizhu2000.net",A站点自然就可以读到这个ticket了
2.7 P3P 在网上查过资料,有一种方法是可以做到不同域的Cookie设置,其方法就是在响应头中加入P3P头信息。设置方法举例如下:
Step1:
首先在hosts文件中设置(其中的192.168.73.1为您本机的ip,写成127.0.0.1不行。)只是举例在真正服务器上不需要这样。
192.168.73.1www.a.com
192.168.73.1 www.b.comStep2:编写文件 b_setcookie.jsp
view plaincopy to clipboardprint?
<%@pagecontentType="text/html;charset=utf-8"%>
<%
response.addHeader("Cache-Control","no-cache");
response.addHeader("Expires","Thu,01Jan197000:00:01GMT");
Stringssocookie="www.sso12345678910.com";
%>
<mce:scriptsrc="http://www.a.com/mp/test/a_setcookie.jsp?id=<%=ssocookie%><!--
">
//--></mce:script>
Step3:编写文件a_setcookie.jsp
viewplaincopytoclipboardprint?
<%
response.setHeader("P3P","CP=\"CURaADMaDEVaPSAoPSDoOURBUSUNIPURINTDEMSTAPRECOMNAVOTCNOIDSPCOR\"");
StringdomainId=request.getParameter("id");
Cookie_cookie=newCookie("test",domainId);
_cookie.setMaxAge(30*60*100);
_cookie.setPath("/");
response.addCookie(_cookie);
%>Step4:编写文件 a_getcookie.jsp
viewplaincopytoclipboardprint?
<%@pagecontentType="text/html;charset=utf-8"%>
<%
Cookiecookies[]=request.getCookies();CookiesCookie=null;
Stringsname=null;
Stringname=null;
if(cookies==null)//如果没有任何cookie
out.print("noneanycookie");
else{
out.print(cookies.length+"<br>");
for(inti=0;i<cookies.length;i++){
sCookie=cookies[i];
sname=sCookie.getName();
name=sCookie.getValue();
out.println("comment==>>>"+sCookie.getComment()+"\n");
out.println("getDomain==>>>"+sCookie.getDomain()+"\n");
out.println("getSecure==>>"+sCookie.getSecure()+"\n");
out.println("getVersion==>>"+sCookie.getVersion()+"\n");
out.println("cookiename==>>"+sname+"->"+"cookievalue==>>>"+name+"<br>");
}
}
%>Step5:测试
依次请求
http://www.b.com/mp/test/b_setcookie.jsp
http://www.a.com/mp/test/a_getcookie.jsp
便可看到通过跨域设置的cookie的值!
这种方法虽然可以实现跨域设置Cookie,但还是需要在目标系统进行代码的注入。3 题外话
实际上水晶易表Flash不能跨域访问WebService获取数角也是因为类似于同源策略的安全沙盒问题引起。
对于安全沙盒问题,倒是也有相应的解决办法。
如果想要在Flash里面跨域获取数据,就必须在对方server上配置crossdomain.xml。具体来说,比如你的Flash在domainA下面,而你想要访问domainB暴露的webservice,那么domainB的server根目录下必须要有一个crossdomain.xml文件来配置说你有这个权限。这个是FlashPlayer的安全限制。
对于Flash Player 9之前的版本,这个crossdomain.xml文件大概如下:view plaincopy to clipboardprint?
<?xmlversion="1.0"encoding="UTF-8"?>
<!DOCTYPEcross-domain-policySYSTEM"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-fromdomain="*"secure="true"/>
</cross-domain-policy>
以上配置允许所有domain访问当前server所暴露的数据(比如webservice)。你可以在domain属性里面指定特殊的规则。secure属性用来设置你所暴露的数据是否走https协议。
但是对于FlashPlayer9而言,crossdomain.xml文件内容出现了较大的变化,原因是FlashPlayer9的security机制有所改变。所以当我用Flex3调用crossdomain的webservice时,还使用上面的crossdomain.xml文件,结果就报错说securityerror。于是稍微研究了一下,得到如下解决方案,其实就是要改变crossdomain.xml的内容:
viewplaincopytoclipboardprint?
<?xmlversion="1.0"encoding="UTF-8"?>
<!DOCTYPEcross-domain-policySYSTEM"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<site-controlpermitted-cross-domain-policies="all"/>
<allow-access-fromdomain="*"/>
<allow-http-request-headers-fromdomain="*"headers="*"/>
</cross-domain-policy>以上是Flash Player 9所要求的crossdomain.xml的内容。可以看到多了两个tag。其中site-control是可选的,但是allow-http-request-headers-from对于cross domain的web service确实必须的。如果没有允许header,就会像我之前一样报错。这些配置项的具体含义以及其他可选配置项,可以参考http://www.adobe.com/devnet/flashplayer/articles/flash_player_9_security.pdf。 当然在生成的Flash当中,需要有代码来调用相应的crossdomain.xml。但是水晶易表所导出的Flash当中,并不包含该代码。