java實現簡單的單點登陸 (轉)

 

摘要 :單點登陸( 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 的產品,在功能上、性能上和安全上都必須有更加完備的考慮。
相關文章
相關標籤/搜索