Java实现SSO

摘要 :单点登陆( SSO )的技术被愈来愈普遍地运用到各个领域的软件系统当中。本文从业务的角度分析了单点登陆的需求和应用领域;从技术自己的角度分析了单点登陆技术的内部机制和实现手段,而且给出 Web-SSO 和桌面 SSO 的实现、源代码和详细讲解;还从安全和性能的角度对现有的实现技术进行进一步分析,指出相应的风险和须要改进的方面。本文除了从多个方面和角度给出了对单点登陆( SSO )的全面分析,还而且讨论了如何将现有的应用和 SSO 服务结合起来,可以帮助应用架构师和系统分析人员从本质上认识单点登陆,从而更好地设计出符合须要的安全架构。
关键字 SSO, Java, J2EE, JAAS
什么是单点登录
单点登陆( Single Sign On ),简称为  SSO ,是目前比较流行的企业业务整合的解决方案之一。 SSO 的定义是在多个应用系统中,用户只须要登陆一次就能够访问全部相互信任的应用系统。
较大的企业内部,通常都有不少的业务支持系统为其提供相应的管理和 IT 服务。例如财务系统为财务人员提供财务的管理、计算和报表服务;人事系统为人事部门提供全公司人员的维护服务;各类业务系统为公司内部不一样的业务提供不一样的服务等等。这些系统的目的都是让计算机来进行复杂繁琐的计算工做,来替代人力的手工劳动,提升工做效率和质量。这些不一样的系统每每是在不一样的时期建设起来的,运行在不一样的平台上;也许是由不一样厂商开发,使用了各类不一样的技术和标准。若是举例说国内一著名的 IT 公司(名字隐去),内部共有 60 多个业务系统,这些系统包括两个不一样版本的 SAP ERP 系统, 12 个不一样类型和版本的数据库系统, 8 个不一样类型和版本的操做系统,以及使用了 3 种不一样的防火墙技术,还有数十种互相不能兼容的协议和标准,你相信吗?不要怀疑,这种状况其实很是广泛。每个应用系统在运行了数年之后,都会成为不可替换的企业 IT 架构的一部分,以下图所示。
随着企业的发展,业务系统的数量在不断的增长,老的系统却不能轻易的替换,这会带来不少的开销。其一是管理上的开销,须要维护的系统愈来愈多。不少系统的数据是相互冗余和重复的,数据的不一致性会给管理工做带来很大的压力。业务和业务之间的相关性也愈来愈大,例如公司的计费系统和财务系统,财务系统和人事系统之间都不可避免的有着密切的关系。
为了下降管理的消耗,最大限度的重用已有投资的系统,不少企业都在进行着企业应用集成( EAI )。企业应用集成能够在不一样层面上进行:例如在数据存储层面上的“数据大集中”,在传输层面上的“通用数据交换平台”,在应用层面上的“业务流程整合”,和用户界面上的“通用企业门户”等等。事实上,还用一个层面上的集成变得愈来愈重要,那就是“身份认证”的整合,也就是“单点登陆”。
一般来讲,每一个单独的系统都会有本身的安全体系和身份认证系统。整合之前,进入每一个系统都须要进行登陆,这样的局面不只给管理上带来了很大的困难,在安全方面也埋下了重大的隐患。下面是一些著名的调查公司显示的统计数据:
  • 用户天天平均 16 分钟花在身份验证任务上 资料来源: IDS
  • 频繁的 IT 用户平均有 21 个密码 资料来源: NTA Monitor Password Survey
  • 49% 的人写下了其密码,而 67% 的人不多改变它们
  • 每 79 秒出现一块儿身份被窃事件 资料来源:National Small Business Travel Assoc
  • 全球欺骗损失每一年约 12B - 资料来源:Comm Fraud Control Assoc
  • 到 2007 年,身份管理市场将成倍增加至 $4.5B - 资料来源:IDS
 
使用“单点登陆”整合后,只须要登陆一次就能够进入多个系统,而不须要从新登陆,这不只仅带来了更好的用户体验,更重要的是下降了安全的风险和管理的消耗。请看下面的统计数据:
  • 提升 IT 效率:对于每 1000 个受管用户,每用户可节省$70K
  • 帮助台呼叫减小至少1/3,对于 10K 员工的公司,每一年能够节省每用户 $75,或者合计 $648K
  • 生产力提升:每一个新员工可节省 $1K,每一个老员工可节省 $350 资料来源:Giga
  • ROI 回报:7.5 到 13 个月 资料来源:Gartner
 
另外,使用“单点登陆”仍是 SOA 时代的需求之一。在面向服务的架构中,服务和服务之间,程序和程序之间的通信大量存在,服务之间的安全认证是 SOA 应用的难点之一,应此创建“单点登陆”的系统体系可以大大简化 SOA 的安全问题,提升服务之间的合做效率。
单点登录的技术实现机制
随着 SSO 技术的流行, SSO 的产品也是满天飞扬。全部著名的软件厂商都提供了相应的解决方案。在这里我并不想介绍本身公司( Sun Microsystems )的产品,而是对 SSO 技术自己进行解析,而且提供本身开发这一类产品的方法和简单演示。有关我写这篇文章的目的,请参考个人博客( http://yuwang881.blog.sohu.com/3184816.html )。
单点登陆的机制实际上是比较简单的,用一个现实中的例子作比较。颐和园是北京著名的旅游景点,也是我常去的地方。在颐和园内部有许多独立的景点,例如“苏州街”、“佛香阁”和“德和园”,均可以在各个景点门口单独买票。不少游客须要游玩全部德景点,这种买票方式很不方便,须要在每一个景点门口排队买票,钱包拿进拿出的,容易丢失,很不安全。因而绝大多数游客选择在大门口买一张通票(也叫套票),就能够玩遍全部的景点而不须要从新再买票。他们只须要在每一个景点门口出示一下刚才买的套票就可以被容许进入每一个独立的景点。
单点登陆的机制也同样,以下图所示,当用户第一次访问应用系统 1 的时候,由于尚未登陆,会被引导到认证系统中进行登陆( 1 );根据用户提供的登陆信息,认证系统进行身份效验,若是经过效验,应该返回给用户一个认证的凭据-- ticket 2 );用户再访问别的应用的时候( 3 5 )就会将这个 ticket 带上,做为本身认证的凭据,应用系统接受到请求以后会把 ticket 送到认证系统进行效验,检查 ticket 的合法性( 4 6 )。若是经过效验,用户就能够在不用再次登陆的状况下访问应用系统 2 和应用系统 3 了。
从上面的视图能够看出,要实现 SSO ,须要如下主要的功能:
  • 全部应用系统共享一个身份认证系统。统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登陆信息和用户信息库相比较,对用户进行登陆认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行效验,判断其有效性。
  • 全部应用系统可以识别和提取ticket信息要实现SSO的功能,让用户只登陆一次,就必须让应用系统可以识别已经登陆过的用户。应用系统应该能对ticket进行识别和提取,经过与认证系统的通信,能自动判断当前用户是否登陆过,从而完成单点登陆的功能。
 
上面的功能只是一个很是简单的 SSO 架构,在现实状况下的 SSO 有着更加复杂的结构。有两点须要指出的是:
  • 单一的用户信息数据库并非必须的,有许多系统不能将全部的用户信息都集中存储,应该容许用户信息放置在不一样的存储中,以下图所示。事实上,只要统一认证系统,统一ticket的产生和效验,不管用户信息存储在什么地方,都能实现单点登陆。
 
  • 统一的认证系统并非说只有单个的认证服务器,以下图所示,整个系统能够存在两个以上的认证服务器,这些服务器甚至能够是不一样的产品。认证服务器之间要经过标准的通信协议,互相交换认证信息,就能完成更高级别的单点登陆。以下图,当用户在访问应用系统1时,由第一个认证服务器进行认证后,获得由此服务器产生的ticket。当他访问应用系统4的时候,认证服务器2可以识别此ticket是由第一个服务器产生的,经过认证服务器之间标准的通信协议(例如SAML)来交换认证信息,仍然可以完成SSO的功能。
 
3 WEB-SSO 的实现
随着互联网的高速发展, WEB 应用几乎统治了绝大部分的软件应用系统,所以 WEB-SSO SSO 应用当中最为流行。 WEB-SSO 有其自身的特色和优点,实现起来比较简单易用。不少商业软件和开源软件都有对 WEB-SSO 的实现。其中值得一提的是 OpenSSO  https://opensso.dev.java.net ),为用 Java 实现 WEB-SSO 提供架构指南和服务指南,为用户本身来实现 WEB-SSO 提供了理论的依据和实现的方法。
为何说 WEB-SSO 比较容易实现呢?这是有 WEB 应用自身的特色决定的。
众所周知, Web 协议(也就是 HTTP )是一个无状态的协议。一个 Web 应用由不少个 Web 页面组成,每一个页面都有惟一的 URL 来定义。用户在浏览器的地址栏输入页面的 URL ,浏览器就会向 Web Server 去发送请求。以下图,浏览器向 Web 服务器发送了两个请求,申请了两个页面。这两个页面的请求是分别使用了两个单独的 HTTP 链接。所谓无状态的协议也就是表如今这里,浏览器和 Web 服务器会在第一个请求完成之后关闭链接通道,在第二个请求的时候从新创建链接。 Web 服务器并不区分哪一个请求来自哪一个客户端,对全部的请求都一视同仁,都是单独的链接。这样的方式大大区别于传统的( Client/Server C/S 结构 , 在那样的应用中,客户端和服务器端会创建一个长时间的专用的链接通道。正是由于有了无状态的特性,每一个链接资源可以很快被其余客户端所重用,一台 Web 服务器才可以同时服务于成千上万的客户端。
可是咱们一般的应用是有状态的。先不用提不一样应用之间的 SSO ,在同一个应用中也须要保存用户的登陆身份信息。例如用户在访问页面 1 的时候进行了登陆,可是刚才也提到,客户端的每一个请求都是单独的链接,当客户再次访问页面 2 的时候,如何才能告诉 Web 服务器,客户刚才已经登陆过了呢?浏览器和服务器之间有约定:经过使用 cookie 技术来维护应用的状态。 Cookie 是能够被 Web 服务器设置的字符串,而且能够保存在浏览器中。以下图所示,当浏览器访问了页面 1 时, web 服务器设置了一个 cookie ,并将这个 cookie 和页面 1 一块儿返回给浏览器,浏览器接到 cookie 以后,就会保存起来,在它访问页面 2 的时候会把这个 cookie 也带上, Web 服务器接到请求时也能读出 cookie 的值,根据 cookie 值的内容就能够判断和恢复一些用户的信息状态。
Web-SSO 彻底能够利用 Cookie 结束来完成用户登陆信息的保存,将浏览器中的 Cookie 和上文中的 Ticket 结合起来,完成 SSO 的功能。
 
为了完成一个简单的 SSO 的功能,须要两个部分的合做:
  1. 统一的身份认证服务。
  2. 修改Web应用,使得每一个应用都经过这个统一的认证服务来进行身份效验。
3.1 Web SSO  的样例
根据上面的原理,我用 J2EE 的技术( JSP Servlet )完成了一个具备 Web-SSO 的简单样例。样例包含一个身份认证的服务器和两个简单的 Web 应用,使得这两个  Web 应用经过统一的身份认证服务来完成 Web-SSO 的功能。此样例全部的源代码和二进制代码均可以从网站地址 http://gceclub.sun.com.cn/wangyu/  下载。
 
样例下载、安装部署和运行指南:
  • Web-SSO的样例是由三个标准Web应用组成,压缩成三个zip文件,从http://gceclub.sun.com.cn/wangyu/web-sso/中下载。其中SSOAuthhttp://gceclub.sun.com.cn/wangyu/web-sso/SSOAuth.zip)是身份认证服务;SSOWebDemo1http://gceclub.sun.com.cn/wangyu/web-sso/SSOWebDemo1.zip)和SSOWebDemo2http://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的产品上,它们彻底是标准的和平台无关的应用。可是有一个限制,那两台部署应用(demo1demo2)的机器的域名须要相同,这在后面的章节中会解释到cookiedomain的关系以及如何制做跨域的WEB-SSO
  • 解压缩SSOAuth.zip文件,在/WEB-INF/下的web.xml中请修改“domainname”的属性以反映实际的应用部署状况,domainname须要设置为两个单点登陆的应用(demo1demo2)所属的域名。这个domainname和当前SSOAuth服务部署的机器的域名没有关系。我缺省设置的是“.sun.com”。若是你部署demo1demo2的机器没有域名,请输入IP地址或主机名(如localhost),可是若是使用IP地址或主机名也就意味着demo1demo2须要部署到一台机器上了。设置完后,根据你所选择的J2EE容器,可能须要将SSOAuth这个目录压缩打包成war文件。用“jar -cvf SSOAuth.war SSOAuth/”就能够完成这个功能。
  • 解压缩SSOWebDemo1SSOWebDemo2文件,分别在它们/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> 将其中的SSOServiceURLSSOLoginPage修改为部署SSOAuth应用的机器名、端口号以及根路径(缺省是SSOAuth)以反映实际的部署状况。设置完后,根据你所选择的J2EE容器,可能须要将SSOWebDemo1SSOWebDemo2这两个目录压缩打包成两个war文件。用“jar -cvf SSOWebDemo1.war SSOWebDemo1/”就能够完成这个功能。
  • 请输入第一个web应用的测试URLtest.jsp,例如http://wangyu.prc.sun.com:8080/ SSOWebDemo1/test.jsp,若是是第一次访问,便会自动跳转到登陆界面,以下图
  • 使用系统自带的三个账号之一登陆(例如,用户名:wangyu,密码:wangyu),便能成功的看到test.jsp的内容:显示当前用户名和欢迎信息。
  • 请接着在同一个浏览器中输入第二个web应用的测试URLtest.jsp,例如http://wangyu.prc.sun.com:8080/ SSOWebDemo2/test.jsp。你会发现,不须要再次登陆就能看到test.jsp的内容,一样是显示当前用户名和欢迎信息,并且欢迎信息中明确的显示当前的应用名称(demo2)。
             
3.2 WEB-SSO 代码讲解
3.2.1 身份认证服务代码解析
Web-SSO 的源代码能够从网站地址 http://gceclub.sun.com.cn/wangyu/web-sso/websso_src.zip 下载。身份认证服务是一个标准的 web 应用,包括一个名为 SSOAuth Servlet ,一个 login.jsp 文件和一个 failed.html 。身份认证的全部服务几乎都由 SSOAuth Servlet 来实现了; login.jsp 用来显示登陆的页面(若是发现用户尚未登陆过); failed.html 是用来显示登陆失败的信息(若是用户的用户名和密码与信息数据库中的不同)。
SSOAuth 的代码以下面的列表显示,结构很是简单,先看看这个 Servlet 的主体部分
package DesktopSSO;
 
import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
 
import javax.servlet.*;
import javax.servlet.http.*;
 
 
public class SSOAuth extends HttpServlet {
   
     static private ConcurrentMap accounts;
     static private ConcurrentMap SSOIDs;
     String cookiename="WangYuDesktopSSOID";
     String domainname;
   
     public void init(ServletConfig config) throws ServletException {
         super.init(config);
         domainname= config.getInitParameter("domainname");
         cookiename = config.getInitParameter("cookiename");
         SSOIDs = new ConcurrentHashMap();
         accounts=new ConcurrentHashMap();
         accounts.put("wangyu", "wangyu");
         accounts.put("paul", "paul");
         accounts.put("carol", "carol");
     }
 
     protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
         PrintWriter out = response.getWriter();
         String action = request.getParameter("action");
         String result="failed";
         if (action==null) {
             handlerFromLogin(request,response);
         } else if (action.equals("authcookie")){
             String myCookie = request.getParameter("cookiename");
             if (myCookie != null) result = authCookie(myCookie);
             out.print(result);
             out.close();
         } else if (action.equals("authuser")) {
            result=authNameAndPasswd(request,response);
             out.print(result);
             out.close();
         } else if (action.equals("logout")) {
             String myCookie = request.getParameter("cookiename");
             logout(myCookie);
             out.close();
         }
     }
 
.....
 
}
 
从代码很容易看出, SSOAuth 就是一个简单的 Servlet 。其中有两个静态成员变量: accounts SSOIDs ,这两个成员变量都使用了 JDK1.5 中线程安全的 MAP 类: ConcurrentMap ,因此这个样例必定要 JDK1.5 才能运行。 Accounts 用来存放用户的用户名和密码,在 init() 的方法中能够看到我给系统添加了三个合法的用户。在实际应用中, accounts 应该是去数据库中或 LDAP 中得到,为了简单起见,在本样例中我使用了 ConcurrentMap 在内存中用程序建立了三个用户。而 SSOIDs 保存了在用户成功的登陆后所产生的 cookie 和用户名的对应关系。它的功能显而易见:当用户成功登陆之后,再次访问别的系统,为了鉴别这个用户请求所带的 cookie 的有效性,须要到 SSOIDs 中检查这样的映射关系是否存在。
 
在主要的请求处理方法 processRequest() 中,能够很清楚的看到 SSOAuth 的全部功能
  1. 若是用户尚未登陆过,是第一次登陆本系统,会被跳转到login.jsp页面(在后面会解释如何跳转)。用户在提供了用户名和密码之后,就会用handlerFromLogin()这个方法来验证。
  2. 若是用户已经登陆过本系统,再访问别的应用的时候,是不须要再次登陆的。由于浏览器会将第一次登陆时产生的cookie和请求一块儿发送。效验cookie的有效性是SSOAuth的主要功能之一。
  3. SSOAuth还能直接效验非login.jsp页面过来的用户名和密码的效验请求。这个功能是用于非web应用的SSO,这在后面的桌面SSO中会用到。
  4. SSOAuth还提供logout服务。
 
下面看看几个主要的功能函数:
  private void handlerFromLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
         String username = request.getParameter("username");
         String password = request.getParameter("password");
         String pass = (String)accounts.get(username);
         if ((pass==null)||(!pass.equals(password)))
             getServletContext().getRequestDispatcher("/failed.html").forward(request, response);
         else {
             String gotoURL = request.getParameter("goto");
             String newID = createUID();
             SSOIDs.put(newID, username);
             Cookie wangyu = new Cookie(cookiename, newID);
             wangyu.setDomain(domainname);
             wangyu.setMaxAge(60000);
             wangyu.setValue(newID);
             wangyu.setPath("/");
             response.addCookie(wangyu);
             System.out.println("login success, goto back url:" + gotoURL);
             if (gotoURL != null) {
                 PrintWriter out = response.getWriter();
                 response.sendRedirect(gotoURL);
                 out.close();
             }
         }  
     }
handlerFromLogin() 这个方法是用来处理来自 login.jsp 的登陆请求。它的逻辑很简单:将用户输入的用户名和密码与预先设定好的用户集合(存放在 accounts 中)相比较,若是用户名或密码不匹配的话,则返回登陆失败的页面( failed.html ),若是登陆成功的话,须要为用户当前的 session 建立一个新的 ID ,并将这个 ID 和用户名的映射关系存放到 SSOIDs 中,最后还要将这个 ID 设置为浏览器可以保存的 cookie 值。
登陆成功后,浏览器会到哪一个页面呢?那咱们回顾一下咱们是如何使用身份认证服务的。通常来讲咱们不会直接访问身份服务的任何 URL ,包括 login.jsp 。身份服务是用来保护其余应用服务的,用户通常在访问一个受 SSOAuth 保护的 Web 应用的某个 URL 时,当前这个应用会发现当前的用户尚未登陆,便强制将也页面转向 SSOAuth login.jsp ,让用户登陆。若是登陆成功后,应该自动的将用户的浏览器指向用户最初想访问的那个 URL 。在 handlerFromLogin() 这个方法中,咱们经过接收 goto” 这个参数来保存用户最初访问的 URL ,成功后便从新定向到这个页面中。
另一个要说明的是,在设置 cookie 的时候,我使用了一个setMaxAge(6000) 的方法。这个方法是用来设置 cookie 的有效期,单位是秒。若是不使用这个方法或者参数为负数的话,当浏览器关闭的时候,这个 cookie 就失效了。在这里我给了很大的值( 1000 分钟),致使的行为是:当你关闭浏览器(或者关机),下次再打开浏览器访问刚才的应用,只要在 1000 分钟以内,就不须要再登陆了。我这样作是下面要介绍的桌面 SSO 中所须要的功能。
其余的方法更加简单,这里就很少解释了。
 
3.2.2 具备 SSO 功能的 web 应用源代码解析
要实现 WEB-SSO 的功能,只有身份认证服务是不够的。这点很显然,要想使多个应用具备单点登陆的功能,还须要每一个应用自己的配合:将本身的身份认证的服务交给一个统一的身份认证服务- SSOAuth SSOAuth 服务中提供的各个方法就是供每一个加入 SSO Web 应用来调用的。
通常来讲, Web 应用须要 SSO 的功能,应该经过如下的交互过程来调用身份认证服务的提供的认证服务:
  • Web应用中每个须要安全保护的URL在访问之前,都须要进行安全检查,若是发现没有登陆(没有发现认证以后所带的cookie),就从新定向到SSOAuth中的login.jsp进行登陆。
  • 登陆成功后,系统会自动给你的浏览器设置cookie,证实你已经登陆过了。
  • 当你再访问这个应用的须要保护的URL的时候,系统仍是要进行安全检查的,可是此次系统可以发现相应的cookie
  • 有了这个cookie,还不能证实你就必定有权限访问。由于有可能你已经logout,或者cookie已通过期了,或者身份认证服务重起过,这些状况下,你的cookie均可能无效。应用系统拿到这个cookie,还须要调用身份认证的服务,来判断cookie时候真的有效,以及当前的cookie对应的用户是谁。
  • 若是cookie效验成功,就容许用户访问当前请求的资源。
以上这些功能,能够用不少方法来实现:
  • 在每一个被访问的资源中(JSPServlet)中都加入身份认证的服务,来得到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.htmlFilter是一个具备很好的模块化,可重用的编程API,用在SSO正合适不过。本样例就是使用一个filter来完成以上的功能。
 
package SSO;
 
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;
 
public class SSOFilter implements Filter {
     private FilterConfig filterConfig = null;
     private String cookieName="WangYuDesktopSSOID";
     private String SSOServiceURL= "http://wangyu.prc.sun.com:8080/SSOAuth/SSOAuth";
     private String SSOLoginPage= "http://wangyu.prc.sun.com:8080/SSOAuth/login.jsp";
   
     public void init(FilterConfig filterConfig) {
 
         this.filterConfig = filterConfig;
         if (filterConfig != null) {
             if (debug) {
                 log("SSOFilter:Initializing filter");
             }
         }       
         cookieName = filterConfig.getInitParameter("cookieName");
         SSOServiceURL = filterConfig.getInitParameter("SSOServiceURL");
         SSOLoginPage = filterConfig.getInitParameter("SSOLoginPage");
    
.....
.....
 
}
以上的初始化的源代码有两点须要说明:一是有两个须要配置的参数 SSOServiceURL SSOLoginPage 。由于当前的 Web 应用极可能和身份认证服务( SSOAuth )不在同一台机器上,因此须要让这个 filter 知道身份认证服务部署的 URL ,这样才能去调用它的服务。另一点就是因为身份认证的服务调用是要经过 http 协议来调用的(在本样例中是这样设计的,读者彻底能够设计本身的身份服务,使用别的调用协议,如 RMI SOAP 等等),全部笔者引用了 apache commons 工具包(详细信息情访问 apache  的网站 http://jakarta.apache.org/commons/index.html ),其中的 httpclient” 能够大大简化 http 调用的编程。
下面看看 filter 的主体方法 doFilter():
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
         if (debug) log("SSOFilter:doFilter()");
         HttpServletRequest request = (HttpServletRequest) req;
         HttpServletResponse response = (HttpServletResponse) res;
         String result="failed";
         String url = request.getRequestURL().toString();
         String qstring = request.getQueryString();
         if (qstring == null) qstring ="";
 
         // 检查 http 请求的 head 是否有须要的 cookie
         String cookieValue ="";
         javax.servlet.http.Cookie[] diskCookies = request.getCookies();
         if (diskCookies != null) {
             for (int i = 0; i < diskCookies.length; i++) {
                 if(diskCookies[i].getName().equals(cookieName)){
                     cookieValue = diskCookies[i].getValue();
 
                     // 若是找到了相应的 cookie 则效验其有效性
                     result = SSOService(cookieValue);
                     if (debug) log("found cookies!");
                 }
             }
         }
         if (result.equals("failed")) { // 效验失败或没有找到 cookie ,则须要登陆
             response.sendRedirect(SSOLoginPage+"?goto="+url);
         } else if (qstring.indexOf("logout") > 1) {//logout 服务
             if (debug) log("logout action!");
             logoutService(cookieValue);
             response.sendRedirect(SSOLoginPage+"?goto="+url);
         } else {// 效验成功
             request.setAttribute("SSOUser",result);
             Throwable problem = null;
             try {
                 chain.doFilter(req, res);
             } catch(Throwable t) {
                 problem = t;
                 t.printStackTrace();
             }      
             if (problem != null) {
                 if (problem instanceof ServletException) throw (ServletException)problem;
                 if (problem instanceof IOException) throw (IOException)problem;
                 sendProcessingError(problem, res);
             }
         }  
     }
doFilter() 方法的逻辑也是很是简单的,在接收到请求的时候,先去查找是否存在指望的 cookie 值,若是找到了,就会调用 SSOService(cookieValue) 去效验这个 cookie 的有效性。若是 cookie 效验不成功或者 cookie 根本不存在,就会直接转到登陆界面让用户登陆;若是 cookie 效验成功,就不会作任何阻拦,让此请求进行下去。在配置文件中,有下面的一个节点表示了此 filter URL 映射关系:只拦截全部的 jsp 请求。
<filter-mapping> <filter-name>SSOFilter</filter-name> <url-pattern>*.jsp</url-pattern> </filter-mapping>
 
下面还有几个主要的函数须要说明:
     private String SSOService(String cookievalue) throws IOException {
         String authAction = "?action=authcookie&cookiename=";
         HttpClient httpclient = new HttpClient();
         GetMethod httpget = new GetMethod(SSOServiceURL+authAction+cookievalue);
         try { 
             httpclient.executeMethod(httpget);
             String result = httpget.getResponseBodyAsString();
             return result;
         } finally {
             httpget.releaseConnection();
         }
     }
   
     private void logoutService(String cookievalue) throws IOException {
         String authAction = "?action=logout&cookiename=";
         HttpClient httpclient = new HttpClient();
         GetMethod httpget = new GetMethod(SSOServiceURL+authAction+cookievalue);
         try {
             httpclient.executeMethod(httpget);
             httpget.getResponseBodyAsString();
         } finally {
             httpget.releaseConnection();
         }
     }
这两个函数主要是利用 apache 中的 httpclient 访问 SSOAuth 提供的认证服务来完成效验 cookie logout 的功能。
其余的函数都很简单,有不少都是个人 IDE NetBeans )替我自动生成的。
当前方案的安全局限性
当前这个 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 )的架构指南中也给出了部分安全措施的解决方案。
当前方案的功能和性能局限性
除了安全性,当前方案在功能和性能上都须要不少的改进:
  • 当前所提供的登陆认证模式只有一种:用户名和密码,并且为了简单,将用户名和密码放在内存当中。事实上,用户身份信息的来源应该是多种多样的,能够是来自数据库中,LDAP中,甚至于来自操做系统自身的用户列表。还有不少其余的认证模式都是商务应用不可缺乏的,所以SSO的解决方案应该包括各类认证的模式,包括数字证书,Radius, SafeWord MemberShipSecurID等多种方式。最为灵活的方式应该容许可插入的JAAS框架来扩展身份认证的接口
  • 咱们编写的Filter只能用于J2EE的应用,而对于大量非JavaWeb应用,却没法提供SSO服务。
  • 在将Filter应用到Web应用的时候,须要对容器上的每个应用都要作相应的修改,从新部署。而更加流行的作法是Agent机制:为每个应用服务器安装一个agent,就能够将SSO功能应用到这个应用服务器中的全部应用。
  • 当前的方案不能支持分别位于不一样domainWeb应用进行SSO。这是由于浏览器在访问Web服务器的时候,仅仅会带上和当前web服务器具备相同domain名称的那些cookie。要提供跨域的SSO的解决方案有不少其余的方法,在这里就很少说了。SunAccess Manager就具备跨域的SSO的功能。
  • 另外,Filter的性能问题也是须要重视的方面。由于Filter会截获每个符合URL映射规则的请求,得到cookie,验证其有效性。这一系列任务是比较消耗资源的,特别是验证cookie有效性是一个远程的http的调用,来访问SSOAuth的认证服务,有必定的延时。所以在性能上须要作进一步的提升。例如在本样例中,若是将URL映射从“.jsp改为“/*,也就是说filter对全部的请求都起做用,整个应用会变得很是慢。这是由于,页面当中包含了各类静态元素如gif图片,css样式文件,和其余html静态页面,这些页面的访问都要经过filter去验证。而事实上,这些静态元素没有什么安全上的需求,应该在filter中进行判断,不去效验这些请求,性能会好不少。另外,若是在filter中加上必定的cache,而不须要每个cookie效验请求都去远端的身份认证服务中执行,性能也能大幅度提升。
  • 另外系统还须要不少其余的服务,如在内存中定时删除无用的cookie映射等等,都是一个严肃的解决方案须要考虑的问题。
桌面 SSO 的实现
WEB-SSO 的概念延伸开,咱们能够把 SSO 的技术拓展到整个桌面的应用,不只仅局限在浏览器。 SSO 的概念和原则都没有改变,只须要再作一点点的工做,就能够完成桌面  SSO  的应用。
桌面 SSO WEB-SSO 同样,关键的技术也在于如何在用户登陆事后保存登陆的凭据。在 WEB-SSO 中,登陆的凭据是靠浏览器的 cookie 机制来完成的;在桌面应用中,能够将登陆的凭证保存到任何地方,只要全部 SSO 的桌面应用都共享这个凭证。
从网站能够下载一个简单的桌面 SSO 的样例 (http://gceclub.sun.com.cn/wangyu/desktop-sso/desktopsso.zip) 和所有源码( http://gceclub.sun.com.cn/wangyu/desktop-sso/desktopsso_src.zip ),虽然简单,可是它具备桌面 SSO 大多数的功能,稍微加以扩充就能够成为本身的解决方案。
 
6.1 桌面样例的部署
  1. 运行此桌面SSO须要三个前提条件: a) WEB-SSO的身份认证应用应该正在运行,由于咱们在桌面SSO当中须要用到统一的认证服务 b) 当前桌面须要运行MozillaNetscape浏览器,由于咱们将ticket保存到mozillacookie文件中 c) 必须在JDK1.4以上运行。(WEB-SSO须要JDK1.5以上)
  2. 解开desktopsso.zip文件,里面有两个目录binlib
  3. bin目录下有一些脚本文件和配置文件,其中config.properties包含了三个须要配置的参数: a) SSOServiceURL要指向WebSSO部署的身份认证的URL b) SSOLoginPage要指向WebSSO部署的身份认证的登陆页面URL c) cookiefilepath要指向当前用户的mozilla所存放cookie的文件
  4. bin目录下还有一个login.conf是用来配置JAAS登陆模块,本样例提供了两个,读者能够任意选择其中一个(也能够都选),再从新运行程序,查看登陆认证的变化
  5. bin下的运行脚本可能须要做相应的修改 a) 若是是在unix下,各个jar文件须要用“:来隔开,而不是“; b) java 运行程序须要放置在当前运行的路径下,不然须要加上java的路径全名。
 
6.2 桌面样例的运行
样例程序包含三个简单的 Java 控制台程序,这三个程序单独运行都须要登陆。若是运行第一个命叫“ GameSystem 的程序,提示须要输入用户名和密码:
效验成功之后,便会显示当前登陆的用户的基本信息等等。
  这时候再运行第二个桌面 Java 应用( mailSystem )的时候,就不须要再登陆了,直接就显示出来刚才登陆的用户。
第三个应用是 logout ,运行它以后,用户便退出系统。再访问的时候,又须要从新登陆了。请读者再制裁执行完 logout 以后,从新验证一下前两个应用的 SSO :先运行第二个应用,再运行第一个,会看到相同的效果。
咱们的样例并无在这里停步,事实上,本样例不只可以和在几个 Java 应用之间 SSO ,还能和浏览器进行 SSO ,也就是将浏览器也当成是桌面的一部分。这对一些行业有着不小的吸引力。
这时候再打开 Mozilla 浏览器,访问之前提到的那两个 WEB 应用,会发现只要桌面应用若是登陆过, Web 应用就不用再登陆了,并且能显示刚才登陆的用户的信息。读者能够在几个桌面和 Web 应用之间进行登陆和 logout 的试验,看看它们之间的 SSO
6.3 桌面样例的源码分析
桌面 SSO 的样例使用了 JAAS (要了解 JAAS 的详细的信息请参考 http://java.sun.com/products/jaas )。 JAAS 是对 PAM Pluggable Authentication Module )的 Java 实现,来完成  Java 应用可插拔的安全认证模块。使用 JAAS 做为 Java 应用的安全认证模块有不少好处,最主要的是不须要修改源代码就能够更换认证方式。例如原有的 Java 应用若是使用 JAAS 的认证,若是须要应用 SSO ,只须要修改 JAAS 的配置文件就好了。如今在流行的 J2EE 和其余  Java 的产品中,用户的身份认证都是经过 JAAS 来完成的。在样例中,咱们就展现了这个功能。请看配置文件 login.conf
     DesktopSSO {
    desktopsso.share.PasswordLoginModule required;
    desktopsso.share.DesktopSSOLoginModule required;
};
当咱们注解掉第二个模块的时候,只有第一个模块起做用。在这个模块的做用下,只有 test 用户(密码是 12345 )才能登陆。当咱们注解掉第一个模块的时候,只有第二个模块起做用,桌面 SSO 才会起做用。
 
全部的 Java 桌面样例程序都是标准 JAAS 应用,熟悉 JAAS 的程序员会很快了解。 JAAS 中主要的是登陆模块( LoginModule )。下面是 SSO 登陆模块的源码:
  public class DesktopSSOLoginModule implements LoginModule {
    ..........
    private String SSOServiceURL = "";
    private String SSOLoginPage = "";
    private static String cookiefilepath = "";  
    .........
 
config.properties 的文件中,咱们配置了它们的值:
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 成员变量指向了在 Web-SSO 中用过的身份认证模块: SSOAuth ,这就说明在桌面系统中咱们试图和 Web 应用共用一个认证服务。而 cookiefilepath 成员变量则泄露了一个“天机”:咱们使用了 Mozilla 浏览器的 cookie 文件来保存登陆的凭证。换句话说,和 Mozilla 共用了一个保存登陆凭证的机制。之因此用 Mozilla 是应为它的 Cookie 文件格式简单,很容易编程访问和修改任意的 Cookie 值。(我试图解析 Internet Explorer cookie 文件但没有成功。)
下面是登陆模块DesktopSSOLoginModule的主体: login() 方法。逻辑也是很是简单:先用 Cookie 来登录,若是成功,则直接就进入系统,不然须要用户输入用户名和密码来登陆系统。
     public boolean login() throws LoginException{
         try {
             if (Cookielogin()) return true;
         } catch (IOException ex) {
             ex.printStackTrace();
         }
       if (passwordlogin()) return true;
       throw new FailedLoginException();
  }
 
下面是Cookielogin() 方法的实体,它的逻辑是: 先从 Cookie 文件中得到相应的 Cookie 值,经过身份效验服务效验 Cookie 的有效性。若是 cookie 有效 就算登陆成功;若是不成功或 Cookie 不存在,用 cookie 登陆就算失败。
     public boolean Cookielogin() throws LoginException,IOException {
       String cookieValue="";
       int cookieIndex =foundCookie();
       if (cookieIndex<0)
             return false;
       else
             cookieValue = getCookieValue(cookieIndex);
      username = cookieAuth(cookieValue);
      if (! username.equals("failed")) {
          loginSuccess = true;
          return true;
      }
      return false;
  }
 
 
用用户名和密码登陆的方法要复杂一些,经过 Callback 的机制和屏幕输入输出进行信息交互,完成用户登陆信息的获取;获取信息之后经过 userAuth 方法来调用远端 SSOAuth 的服务来断定当前登陆的有效性。
    public boolean passwordlogin() throws LoginException {
     //
     // Since we need input from a user, we need a callback handler
     if (callbackHandler == null) {
        throw new LoginException("No CallbackHandler defined");
     }
     Callback[] callbacks = new Callback[2];
     callbacks[0] = new NameCallback("Username");
     callbacks[1] = new PasswordCallback("Password", false);
     //
     // Call the callback handler to get the username and password
     try {
       callbackHandler.handle(callbacks);
       username = ((NameCallback)callbacks[0]).getName();
       char[] temp = ((PasswordCallback)callbacks[1]).getPassword();
       password = new char[temp.length];
       System.arraycopy(temp, 0, password, 0, temp.length);
       ((PasswordCallback)callbacks[1]).clearPassword();
     } catch (IOException ioe) {
       throw new LoginException(ioe.toString());
     } catch (UnsupportedCallbackException uce) {
       throw new LoginException(uce.toString());
     }
   
     System.out.println();
     String authresult ="";
     try {
         authresult = userAuth(username, password);
     } catch (IOException ex) {
         ex.printStackTrace();
     }
     if (! authresult.equals("failed")) {
         loginSuccess= true;
         clearPassword();
         try {
             updateCookie(authresult);
         } catch (IOException ex) {
             ex.printStackTrace();
         }
         return true;
     }
  
 
     loginSuccess = false;
     username = null;
     clearPassword();
     System.out.println( "Login: PasswordLoginModule FAIL" );
     throw new FailedLoginException();
  }
 
 
CookieAuth userAuth 方法都是利用 apahce httpclient 工具包和远程的 SSOAuth 进行 http 链接,获取服务。
         private String cookieAuth(String cookievalue) throws IOException{
         String result = "failed";
       
         HttpClient httpclient = new HttpClient();      
         GetMethod httpget = new GetMethod(SSOServiceURL+Action1+cookievalue);
   
         try {
             httpclient.executeMethod(httpget);
             result = httpget.getResponseBodyAsString();
         } finally {
             httpget.releaseConnection();
         }
         return result;
     }
 
private String userAuth(String username, char[] password) throws IOException{
         String result = "failed";
         String passwd= new String(password);
         HttpClient httpclient = new HttpClient();      
         GetMethod httpget = new GetMethod(SSOServiceURL+Action2+username+"&password="+passwd);
         passwd = null;
   
         try {
             httpclient.executeMethod(httpget);
             result = httpget.getResponseBodyAsString();
         } finally {
             httpget.releaseConnection();
         }
         return result;
       
     }
 
还有一个地方须要补充说明的是,在本样例中,用户名和密码的输入都会在屏幕上显示明文。若是但愿用掩码形式来显示密码,以提升安全性,请参考: http://java.sun.com/developer/technicalArticles/Security/pwordmask/
真正安全的全方位 SSO 解决方案: Kerberos
咱们的样例程序(桌面 SSO WEB-SSO )都有一个共性:要想将一个应用集成到咱们的 SSO 解决方案中,或多或少的须要修改应用程序。 Web 应用须要配置一个咱们预制的 filter ;桌面应用须要加上咱们桌面 SSO JAAS 模块(至少要修改 JAAS 的配置文件)。但是有不少程序是没有源代码和没法修改的,例如经常使用的远程通信程序 telnet ftp 等等一些操做系统本身带的经常使用的应用程序。这些程序是很难修改加入到咱们的 SSO 的解决方案中。
事实上有一种全方位的 SSO 解决方案可以解决这些问题,这就是 Kerberos 协议( RFC 1510 )。 Kerberos 是网络安全应用标准 (http://web.mit.edu/kerberos/) ,由 MIT 学校发明,被主流的操做系统所采用。在采用 kerberos 的平台中,登陆和认证是由操做系统自己来维护,认证的凭证也由操做系统来保存,这样整个桌面均可以处于同一个 SSO 的系统保护中。操做系统中的各个应用(如 ftp,telnet )只须要经过配置就能加入到 SSO 中。另外使用 Kerberos 最大的好处在于它的安全性。经过密钥算法的保证和密钥中心的创建,能够作到用户的密码根本不须要在网络中传输,而传输的信息也会十分的安全。
目前支持 Kerberos 的操做系统包括 Solaris, windows,Linux 等等主流的平台。只不过要搭建一个 Kerberos 的环境比较复杂, KDC (密钥分发中心)的创建也须要至关的步骤。 Kerberos 拥有很是成熟的 API ,包括 Java API 。使用 Java Generic Security Services(GSS) API 而且使用 JAAS 中对 Kerberos 的支持(详细信息请参见 Sun Java&Kerberos 教程 http://java.sun.com/ j2se/1.5.0/docs/guide/security/jgss/tutorials/index.html ),要将咱们这个样例改形成对 Kerberos 的支持也是不难的。 值得一提的是在 JDK6.0  http://www.java.net/download/jdk6 )当中直接就包含了对 GSS 的支持,不须要单独下载 GSS 的包。
 
总结
本文的主要目的是阐述 SSO 的基本原理,并提供了一种实现的方式。经过对源代码的分析来掌握开发 SSO 服务的技术要点和充分理解 SSO 的应用范围。可是,本文仅仅说明了身份认证的服务,而另一个和身份认证密不可分的服务 ---- 权限效验,却没有提到。要开发出真正的 SSO 的产品,在功能上、性能上和安全上都必须有更加完备的考虑。
相关文章
相关标签/搜索