这里有一个j2ee样例, 包含一个身份认证的服务器和两个简单的Web应用,使得这两个 Web应用通过统一的身份认证服务来完成Web-SSO的功能。
此样例所有的源代码和二进制代码都可以从网站地址http://gceclub.sun.com.cn/wangyu/ 下载。
一、样例下载、安装部署和运行指南:
Web-SSO的样例是由三个标准Web应用组成,压缩成三个zip文件,从http://gceclub.sun.com.cn/wangyu/web-sso/中下载。其中SSOAuth(http://gceclub.sun.com.cn/wangyu/web-sso/SSOAuth.zip)是身份认证服务;SSOWebDemo1(http://gceclub.sun.com.cn/wangyu/web-sso/SSOWebDemo1.zip)和SSOWebDemo2(http://gceclub.sun.com.cn/wangyu/web-sso/SSOWebDemo2.zip)是两个用来演示单点登录的Web应用。
这三个Web应用之所以没有打成war包,是因为它们不能直接部署,根据读者的部署环境需要作出小小的修改。样例部署和运行的环境有一定的要求,需要符合Servlet2.3以上标准的J2EE容器才能运行(例如Tomcat5,Sun Application Server 8, Jboss 4等)。
另外,身份认证服务需要JDK1.5的运行环境。之所以要用JDK1.5是因为笔者使用了一个线程安全的高性能的Java集合类“ConcurrentMap”,只有在JDK1.5中才有。
这三个Web应用完全可以单独部署,它们可以分别部署在不同的机器,不同的操作系统和不同的J2EE的产品上,它们完全是标准的和平台无关的应用。
但是有一个限制,那两台部署应用(demo1、demo2)的机器的域名需要相同,这在后面的章节中会解释到cookie和domain的关系以及如何制作跨域的WEB-SSO
解压缩SSOAuth.zip文件,在/WEB-INF/下的web.xml中请修改“domainname”的属性以反映实际的应用部署情况,domainname需要设置为两个单点登录的应用(demo1和demo2)所属的域名。
这个domainname和当前SSOAuth服务部署的机器的域名没有关系。我缺省设置的是“.sun.com”。
如果你部署demo1和demo2的机器没有域名,请输入IP地址或主机名(如localhost),但是如果使用IP地址或主机名也就意味着demo1和demo2需要部署到一台机器上了。
设置完后,根据你所选择的J2EE容器,可能需要将SSOAuth这个目录压缩打包成war文件。用“jar -cvf SSOAuth.war SSOAuth/”就可以完成这个功能。
解压缩SSOWebDemo1和SSOWebDemo2文件,分别在它们/WEB-INF/下找到web.xml文件,请修改其中的几个初始化参数
<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应用的机器名、端口号以及根路径(缺省是SSOAuth)以反映实际的部署情况。
设置完后,根据你所选择的J2EE容器,可能需要将SSOWebDemo1和SSOWebDemo2这两个目录压缩打包成两个war文件。用“jar -cvf SSOWebDemo1.war SSOWebDemo1/”就可以完成这个功能。
请输入第一个web应用的测试URL(test.jsp),例如http://wangyu.prc.sun.com:8080/ SSOWebDemo1/test.jsp,如果是第一次访问,便会自动跳转到登录界面,如下图
使用系统自带的三个帐号之一登录(例如,用户名:wangyu,密码:wangyu),便能成功的看到test.jsp的内容:显示当前用户名和欢迎信息。
请接着在同一个浏览器中输入第二个web应用的测试URL(test.jsp),例如http://wangyu.prc.sun.com:8080/ SSOWebDemo2/test.jsp。你会发现,不需要再次登录就能看到test.jsp的内容,同样是显示当前用户名和欢迎信息,而且欢迎信息中明确的显示当前的应用名称(demo2)。
二、代码讲解
- 身份认证的所有服务几乎都由SSOAuth的Servlet来实现了;
- login.jsp用来显示登录的页面(如果发现用户还没有登录过);
- failed.html是用来显示登录失败的信息(如果用户的用户名和密码与信息数据库中的不一样)。
1、Servlet的主体部分
- 如果用户还没有登录过,是第一次登录本系统,会被跳转到login.jsp页面(在后面会解释如何跳转)。用户在提供了用户名和密码以后,就会用handlerFromLogin()这个方法来验证。
- 如果用户已经登录过本系统,再访问别的应用的时候,是不需要再次登录的。因为浏览器会将第一次登录时产生的cookie和请求一起发送。效验cookie的有效性是SSOAuth的主要功能之一。
- SSOAuth还能直接效验非login.jsp页面过来的用户名和密码的效验请求。这个功能是用于非web应用的SSO,这在后面的桌面SSO中会用到。
- SSOAuth还提供logout服务。
2、具有SSO功能的web应用
- 将自己的身份认证的服务交给一个统一的身份认证服务-SSOAuth。SSOAuth服务中提供的各个方法就是供每个加入SSO的Web应用来调用的。
- Web应用中每一个需要安全保护的URL在访问以前,都需要进行安全检查,如果发现没有登录(没有发现认证之后所带的cookie),就重新定向到SSOAuth中的login.jsp进行登录。
- 登录成功后,系统会自动给你的浏览器设置cookie,证明你已经登录过了。
- 当你再访问这个应用的需要保护的URL的时候,系统还是要进行安全检查的,但是这次系统能够发现相应的cookie。
- 有了这个cookie,还不能证明你就一定有权限访问。因为有可能你已经logout,或者cookie已经过期了,或者身份认证服务重起过,这些情况下,你的cookie都可能无效。应用系统拿到这个cookie,还需要调用身份认证的服务,来判断cookie时候真的有效,以及当前的cookie对应的用户是谁。
- 如果cookie效验成功,就允许用户访问当前请求的资源。
- 在每个被访问的资源中(JSP或Servlet)中都加入身份认证的服务,来获得cookie,并且判断当前用户是否登录过。不过这个笨方法没有人会用:-)。
- 可以通过一个controller,将所有的功能都写到一个servlet中,然后在URL映射的时候,映射到所有需要保护的URL集合中(例如*.jsp,/security/*等)。这个方法可以使用,不过,它的缺点是不能重用。在每个应用中都要部署一个相同的servlet。
- Filter是比较好的方法。符合Servlet2.3以上的J2EE容器就具有部署filter的功能。(Filter的使用可以参考JavaWolrd的文章http://www.javaworld.com/javaworld/jw-06-2001/jw-0622-filters.html)Filter是一个具有很好的模块化,可重用的编程API,用在SSO正合适不过。本样例就是使用一个filter来完成以上的功能。
<filter-name>SSOFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
这两个函数主要是利用apache中的httpclient访问SSOAuth提供的认证服务来完成效验cookie和logout的功能。
其他的函数都很简单,有很多都是我的IDE(NetBeans)替我自动生成的。
三、当前方案的局限性
1、当前方案的安全局限性
当前这个WEB-SSO的方案是一个比较简单的雏形,主要是用来演示SSO的概念和说明SSO技术的实现方式。有很多方面还需要完善,其中安全性是非常重要的一个方面。
我们说过,采用SSO技术的主要目的之一就是加强安全性,降低安全风险。因为采用了SSO,在网络上传递密码的次数减少,风险降低是显然的,但是当前的方案却有其他的安全风险。
由于cookie是一个用户登录的唯一凭据,对cookie的保护措施是系统安全的重要环节:
cookie的长度和复杂度
在本方案中,cookie是有一个固定的字符串(我的姓名)加上当前的时间戳。这样的cookie很容易被伪造和猜测。怀有恶意的用户如果猜测到合法的cookie就可以被当作已经登录的用户,任意访问权限范围内的资源
cookie的效验和保护
在本方案中,虽然密码只要传输一次就够了,可cookie在网络中是经常传来传去。一些网络探测工具(如sniff, snoop,tcpdump等)可以很容易捕获到cookie的数值。在本方案中,并没有考虑cookie在传输时候的保护。另外对cookie的效验也过于简单,并不去检查发送cookie的来源究竟是不是cookie最初的拥有者,也就是说无法区分正常的用户和仿造cookie的用户。
当其中一个应用的安全性不好,其他所有的应用都会受到安全威胁
因为有SSO,所以当某个处于 SSO的应用被黒客攻破,那么很容易攻破其他处于同一个SSO保护的应用。
这些安全漏洞在商业的SSO解决方案中都会有所考虑,提供相关的安全措施和保护手段,例如Sun公司的Access Manager,cookie的复杂读和对cookie的保护都做得非常好。另外在OpneSSO (https://opensso.dev.java.net)的架构指南中也给出了部分安全措施的解决方案。
2、当前方案的功能和性能局限性
除了安全性,当前方案在功能和性能上都需要很多的改进:
当前所提供的登录认证模式只有一种:用户名和密码,而且为了简单,将用户名和密码放在内存当中。事实上,用户身份信息的来源应该是多种多样的,可以是来自数据库中,LDAP中,甚至于来自操作系统自身的用户列表。还有很多其他的认证模式都是商务应用不可缺少的,因此SSO的解决方案应该包括各种认证的模式,包括数字证书,Radius, SafeWord ,MemberShip,SecurID等多种方式。最为灵活的方式应该允许可插入的JAAS框架来扩展身份认证的接口
我们编写的Filter只能用于J2EE的应用,而对于大量非Java的Web应用,却无法提供SSO服务。
在将Filter应用到Web应用的时候,需要对容器上的每一个应用都要做相应的修改,重新部署。而更加流行的做法是Agent机制:为每一个应用服务器安装一个agent,就可以将SSO功能应用到这个应用服务器中的所有应用。
当前的方案不能支持分别位于不同domain的Web应用进行SSO。这是因为浏览器在访问Web服务器的时候,仅仅会带上和当前web服务器具有相同domain名称的那些cookie。要提供跨域的SSO的解决方案有很多其他的方法,在这里就不多说了。Sun的Access Manager就具有跨域的SSO的功能。
另外,Filter的性能问题也是需要重视的方面。因为Filter会截获每一个符合URL映射规则的请求,获得cookie,验证其有效性。这一系列任务是比较消耗资源的,特别是验证cookie有效性是一个远程的http的调用,来访问SSOAuth的认证服务,有一定的延时。因此在性能上需要做进一步的提高。例如在本样例中,如果将URL映射从“.jsp”改成“/*”,也就是说filter对所有的请求都起作用,整个应用会变得非常慢。这是因为,页面当中包含了各种静态元素如gif图片,css样式文件,和其他html静态页面,这些页面的访问都要通过filter去验证。而事实上,这些静态元素没有什么安全上的需求,应该在filter中进行判断,不去效验这些请求,性能会好很多。另外,如果在filter中加上一定的cache,而不需要每一个cookie效验请求都去远端的身份认证服务中执行,性能也能大幅度提高。
另外系统还需要很多其他的服务,如在内存中定时删除无用的cookie映射等等,都是一个严肃的解决方案需要考虑的问题。