编写自己的单点登录(SSO)服务

编写自己的单点登录(SSO)服务

摘要 :单点登录( SSO )的技术被越来越广泛地运用到各个领域的软件系统当中。本文从业务的角度分析了单点登录的需求和应用领域;从技术本身的角度分析了单点登录技术的内部机制和实现手段,并且给出 Web-SSO 和桌面 SSO 的实现、源代码和详细讲解;还从安全和性能的角度对现有的实现技术进行进一步分析,指出相应的风险和需要改进的方面。本文除了从多个方面和角度给出了对单点登录( SSO )的全面分析,还并且讨论了如何将现有的应用和 SSO 服务结合起来,能够帮助应用架构师和系统分析人员从本质上认识单点登录,从而更好地设计出符合需要的安全架构。
关键字 SSO, Java, J2EE, JAAS
1 什么是单点登陆
单点登录( 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 的安全问题,提高服务之间的合作效率。
2 单点登陆的技术实现机制
随着 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/ 中下载。其中 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 )。
             
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 应用源代码解析