java實現簡單的單點登入

snake_hand發表於2013-03-29
摘要 :單點登入( 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 的產品,在功能上、效能上和安全上都必須有更加完備的考慮。

相關文章