編寫你本身的單點登陸(SSO)服務

王昱 yuwang881@gmail.com   博客地址 http://yuwang881.blog.sohu.com
摘要 :單點登陸( SSO )的技術被愈來愈普遍地運用到各個領域的軟件系統其中。本文從業務的角度分析了單點登陸的需求和應用領域;從技術自己的角度分析了單點登陸技術的內部機制和實現手段,並且給出 Web-SSO 和桌面 SSO 的實現、源碼和具體解說;還從安全和性能的角度對現有的實現技術進行進一步分析,指出對應的風險和需要改進的方面。本文除了從多個方面和角度給出了對單點登陸( SSO )的全面分析,還並且討論了怎樣將現有的應用和 SSO 服務結合起來,能夠幫助應用架構師和系統分析人員從本質上認識單點登陸,從而更好地設計出符合需要的安全架構。
keyword SSO, Java, J2EE, JAAS
1 什麼是單點登錄
單點登陸( Single Sign On ),簡稱爲 SSO ,是眼下比較流行的企業業務整合的解決方式之中的一個。 SSO 的定義是在多個應用系統中,用戶僅僅需要登陸一次就可以訪問所有相互信任的應用系統。
較大的企業內部,通常都有很是多的業務支持系統爲其提供對應的管理和 IT 服務。好比財務系統爲財務人員提供財務的管理、計算和報表服務;人事系統爲人事部門提供全公司人員的維護服務;各類業務系統爲公司內部不一樣的業務提供不一樣的服務等等。這些系統的目的都是讓計算機來進行復雜繁瑣的計算工做,來替代人力的手工勞動,提升工做效率和質量。這些不一樣的系統每每是在不一樣的時期建設起來的,執行在不一樣的平臺上;或許是由不一樣廠商開發,使用了各類不一樣的技術和標準。假設舉例說國內一著名的 IT 公司(名字隱去),內部共同擁有 60 多個業務系統,這些系統包含兩個不一樣版本號的 SAP ERP 系統, 12 個不一樣類型和版本號的數據庫系統, 8 個不一樣類型和版本號的操做系統,以及使用了 3 種不一樣的防火牆技術,還有數十種互相不能兼容的協議和標準,你相信嗎?不要懷疑,這樣的狀況事實上很是廣泛。每一個應用系統在執行了數年之後,都會成爲不可替換的企業 IT 架構的一部分,例如如下圖所看到的。
隨着企業的發展,業務系統的數量在不斷的添加�,老的系統卻不能輕易的替換,這會帶來很是多的開銷。其一是管理上的開銷,需要維護的系統愈來愈多。很是多系統的數據是相互冗餘和反覆的,數據的不一致性會給管理工做帶來很是大的壓力。業務和業務之間的相關性也愈來愈大,好比公司的計費系統和財務系統,財務系統和人事系統之間都不可避免的有着密切的關係。
爲了減小管理的消耗,最大限度的重用已有投資的系統,很是多企業都在進行着企業應用集成( EAI )。企業應用集成可以在不一樣層面上進行:好比在數據存儲層面上的「數據大集中」,在傳輸層面上的「通用數據交換平臺」,在應用層面上的「業務流程整合」,和用戶界面上的「通用企業門戶」等等。其實,還用一個層面上的集成變得愈來愈重要,那就是「身份認證」的整合,也就是「單點登陸」。
一般來講,每個單獨的系統都會有本身的安全體系和身份認證系統。整合曾經,進入每個系統都需要進行登陸,這種局面不只給管理上帶來了很是大的困難,在安全方面也埋下了重大的隱患。如下是一些著名的調查公司顯示的統計數據:
  • 用戶天天平均 16 分鐘花在身份驗證任務上 - 資料來源: IDS
  • 頻繁的 IT 用戶平均有 21 個password - 資料來源: NTA Monitor Password Survey
  • 49% 的人寫下了其password,而 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 )。
單點登陸的機制事實上是比較簡單的,用一個現實中的樣例作比較。頤和園是北京著名的旅遊景點,也是我常去的地方。在頤和園內部有不少獨立的景點,好比「蘇州街」、「佛香閣」和「德和園」,都可以在各個景點門口單獨買票。很是多遊客需要遊玩所有德景點,這樣的買票方式很是不方便,需要在每個景點門口排隊買票,錢包拿進拿出的,easy丟失,很是不安全。因而絕大多數遊客選擇在大門口買一張通票(也叫套票),就可以玩遍所有的景點而不需要又一次再買票。他們僅僅需要在每個景點門口出示一下剛纔買的套票就可以被贊成進入每個獨立的景點。
單點登陸的機制也同樣,例如如下圖所看到的,當用戶第一次訪問應用系統 1 的時候,因爲尚未登陸,會被引導到認證系統中進行登陸( 1 );依據用戶提供的登陸信息,認證系統進行身份效驗,假設經過效驗,應該返回給用戶一個認證的憑據-- ticket 2 );用戶再訪問別的應用的時候( 3 5 )就會將這個 ticket 帶上,做爲本身認證的憑據,應用系統接受到請求以後會把 ticket 送到認證系統進行效驗,檢查 ticket 的合法性( 4 6 )。假設經過效驗,用戶就可以在不用再次登陸的狀況下訪問應用系統 2 和應用系統 3 了。
從上面的視圖可以看出,要實現 SSO ,需要下面基本的功能:
  • 所有應用系統共享一個身份認證系統。
    統一的認證系統是SSO的前提之中的一個。認證系統的主要功能是將用戶的登陸信息和用戶信息庫相比較,對用戶進行登陸認證;認證成功後,認證系統應該生成統一的認證標誌(ticket),返還給用戶。另外,認證系統還應該對ticket進行效驗,推斷其有效性。
  • 所有應用系統能夠識別和提取ticket信息
    要實現SSO的功能,讓用戶僅僅登陸一次,就必須讓應用系統能夠識別已經登陸過的用戶。應用系統應該能對ticket進行識別和提取,經過與認證系統的通信,能本身主動推斷當前用戶是否登陸過,從而完畢單點登陸的功能。
 
上面的功能僅僅是一個很easy的 SSO 架構,在現實狀況下的 SSO 有着更加複雜的結構。有兩點需要指出的是:
  • 單一的用戶信息數據庫並不是必須的,有不少系統不能將所有的用戶信息都集中存儲,應該贊成用戶信息放置在不一樣的存儲中,例如如下圖所看到的。其實,僅僅要統一認證系統,統一ticket的產生和效驗,無論用戶信息存儲在什麼地方,都能實現單點登陸。
 
  • 統一的認證系統並不是說僅僅有單個的認證server,例如如下圖所看到的,整個系統能夠存在兩個以上的認證server,這些server甚至能夠是不一樣的產品。認證server之間要經過標準的通信協議,互相交換認證信息,就能完畢更高級別的單點登陸。例如如下圖,當用戶在訪問應用系統1時,由第一個認證server進行認證後,獲得由此server產生的ticket。當他訪問應用系統4的時候,認證server2能夠識別此ticket是由第一個server產生的,經過認證server之間標準的通信協議(好比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 比較easy實現呢?這是有 WEB 應用自身的特色決定的。
衆所周知, Web 協議(也就是 HTTP )是一個無狀態的協議。一個 Web 應用由很是多個 Web 頁面組成,每個頁面都有惟一的 URL 來定義。用戶在瀏覽器的地址欄輸入頁面的 URL ,瀏覽器就會向 Web Server 去發送請求。例如如下圖,瀏覽器向 Web server發送了兩個請求,申請了兩個頁面。這兩個頁面的請求是分別使用了兩個單獨的 HTTP 鏈接。所謂無狀態的協議也就是表現在這裏,瀏覽器和 Web server會在第一個請求完畢之後關閉鏈接通道,在第二個請求的時候又一次創建鏈接。 Web server並不區分哪一個請求來自哪一個client,對所有的請求都一視同仁,都是單獨的鏈接。這種方式大大差異於傳統的( Client/Server C/S 結構 , 在那樣的應用中,client和server端會創建一個長時間的專用的鏈接通道。正是因爲有了無狀態的特性,每個鏈接資源能夠很是快被其它client所重用,一臺 Web server才能夠同一時候服務於成千上萬的client。
但是咱們一般的應用是有狀態的。先不用提不一樣應用之間的 SSO ,在同一個應用中也需要保存用戶的登陸身份信息。好比用戶在訪問頁面 1 的時候進行了登陸,但是剛纔也提到,client的每個請求都是單獨的鏈接,當客戶再次訪問頁面 2 的時候,怎樣才幹告訴 Web server,客戶剛纔已經登陸過了呢?瀏覽器和server之間有約定:經過使用 cookie 技術來維護應用的狀態。 Cookie 是可以被 Web server設置的字符串,並且可以保存在瀏覽器中。例如如下圖所看到的,當瀏覽器訪問了頁面 1 時, web server設置了一個 cookie ,並將這個 cookie 和頁面 1 一塊兒返回給瀏覽器,瀏覽器接到 cookie 以後,就會保存起來,在它訪問頁面 2 的時候會把這個 cookie 也帶上, Web server接到請求時也能讀出 cookie 的值,依據 cookie 值的內容就可以推斷和恢復一些用戶的信息狀態。
Web-SSO 全然可以利用 Cookie 結束來完畢用戶登陸信息的保存,將瀏覽器中的 Cookie 和上文中的 Ticket 結合起來,完畢 SSO 的功能。
 
爲了完畢一個簡單的 SSO 的功能,需要兩個部分的合做:
  1. 統一的身份認證服務。
  2. 改動Web應用,使得每個應用都經過這個統一的認證服務來進行身份效驗。
3.1 Web SSO 的例子
依據上面的原理,我用 J2EE 的技術( JSP Servlet )完畢了一個具備 Web-SSO 的簡單例子。例子包括一個身份認證的server和兩個簡單的 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應用的機器名、port號以及根路徑(缺省是SSOAuth)以反映實際的部署狀況。設置完後,依據你所選擇的J2EE容器,可能需要將SSOWebDemo1SSOWebDemo2這兩個文件夾壓縮打包成兩個war文件。用「jar -cvf SSOWebDemo1.war SSOWebDemo1/」就可以完畢這個功能。
  • 請輸入第一個web應用的測試URLtest.jsp,好比http://wangyu.prc.sun.com:8080/ SSOWebDemo1/test.jsp,假設是第一次訪問,便會本身主動跳轉到登陸界面,例如如下圖

  • 使用系統自帶的三個賬號之中的一個登陸(好比,username:wangyu,password:wangyu),便能成功的看到test.jsp的內容:顯示當前username和歡迎信息。
  • 請接着在同一個瀏覽器中輸入第二個web應用的測試URLtest.jsp,好比http://wangyu.prc.sun.com:8080/ SSOWebDemo2/test.jsp。你會發現,不需要再次登陸就能看到test.jsp的內容,相同是顯示當前username和歡迎信息,而且歡迎信息中明白的顯示當前的應用名稱(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 是用來顯示登陸失敗的信息(假設用戶的username和password與信息數據庫中的不同)。
SSOAuth 的代碼如如下的列表顯示,結構很easy,先看看這個 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();
        }
    }
 
.....
 
}
 
從代碼很是easy看出, SSOAuth 就是一個簡單的 Servlet 。當中有兩個靜態成員變量: accounts SSOIDs ,這兩個成員變量都使用了 JDK1.5 中線程安全的 MAP 類: ConcurrentMap ,因此這個例子必定要 JDK1.5 才幹執行。 Accounts 用來存放用戶的username和password,在 init() 的方法中可以看到我給系統加入�了三個合法的用戶。在實際應用中, accounts 應該是去數據庫中或 LDAP 中得到,爲了簡單起見,在本例子中我使用了 ConcurrentMap 在內存中用程序建立了三個用戶。而 SSOIDs 保存了在用戶成功的登陸後所產生的 cookie 和username的相應關係。它的功能顯而易見:當用戶成功登陸之後,再次訪問別的系統,爲了鑑別這個用戶請求所帶的 cookie 的有效性,需要到 SSOIDs 中檢查這種映射關係是否存在。
 
在基本的請求處理方法 processRequest() 中,可以很是清楚的看到 SSOAuth 的所有功能
  1. 假設用戶尚未登陸過,是第一次登陸本系統,會被跳轉到login.jsp頁面(在後面會解釋怎樣跳轉)。用戶在提供了username和password之後,就會用handlerFromLogin()這種方法來驗證。
  2. 假設用戶已經登陸過本系統,再訪問別的應用的時候,是不需要再次登陸的。因爲瀏覽器會將第一次登陸時產生的cookie和請求一塊兒發送。效驗cookie的有效性是SSOAuth的主要功能之中的一個。
  3. SSOAuth還能直接效驗非login.jsp頁面過來的username和password的效驗請求。這個功能是用於非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 的登陸請求。它的邏輯很是easy:將用戶輸入的username和password與預先設定好的用戶集合(存放在 accounts 中)相比較,假設username或password不匹配的話,則返回登陸失敗的頁面( failed.html ),假設登陸成功的話,需要爲用戶當前的 session 建立一個新的 ID ,並將這個 ID 和username的映射關係存放到 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() 方法的邏輯也是很easy的,在接收到請求的時候,先去查找是否存在指望的 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 的功能。
其它的函數都很是easy,有很是多都是個人 IDE NetBeans )替我本身主動生成的。
4 當前方案的安全侷限性
當前這個 WEB-SSO 的方案是一個比較簡單的雛形,主要是用來演示 SSO 的概念和說明 SSO 技術的實現方式。有很是多方面還需要無缺,當中安全性是很是重要的一個方面。
咱們說過,採用 SSO 技術的主要目的之中的一個就是增強安全性,減小安全風險。因爲採用了 SSO ,在網絡上傳遞password的次數減小,風險減小是顯然的,但是當前的方案卻有其它的安全風險。因爲 cookie 是一個用戶登陸的惟一憑據,對 cookie 的保護措施是系統安全的重要環節:
  • cookie的長度和複雜度
    在本方案中,cookie是有一個固定的字符串(個人姓名)加上當前的時間戳。這種cookie很是easy被僞造和推測。懷有惡意的用戶假設推測到合法的cookie就可以被看成已經登陸的用戶,隨意訪問權限範圍內的資源
  • cookie的效驗和保護
    在本方案中,儘管password僅僅要傳輸一次就夠了,可cookie在網絡中是經常傳來傳去。一些網絡探測工具(如sniff, snoop,tcpdump等)可以很是easy捕獲到cookie的數值。在本方案中,並無考慮cookie在傳輸時候的保護。另外對cookie的效驗也過於簡單,並不去檢查發送cookie的來源究竟是不是cookie最初的擁有者,也就是說沒法區分正常的用戶和仿造cookie的用戶。
  • 噹噹中一個應用的安全性很差,其它所有的應用都會受到安全威脅
    因爲有SSO,因此當某個處於 SSO的應用被黒客攻破,那麼很是easy攻破其它處於同一個SSO保護的應用。
這些安全漏洞在商業的 SSO 解決方式中都會有所考慮,提供相關的安全措施和保護手段,好比 Sun 公司的 Access Manager cookie 的複雜讀和對 cookie 的保護都作得很好。另外在 OpneSSO https://opensso.dev.java.net )的架構指南中也給出了部分安全措施的解決方式。
5 當前方案的功能和性能侷限性
除了安全性,當前方案在功能和性能上都需要很是多的改進:
  • 當前所提供的登陸認證模式僅僅有一種:username和password,而且爲了簡單,將username和password放在內存其中。其實,用戶身份信息的來源應該是多種多樣的,可以是來自數據庫中,LDAP中,甚至於來自操做系統自身的用戶列表。還有很是多其它的認證模式都是商務應用必不可少的,所以SSO的解決方式應該包含各類認證的模式,包含數字證書,RadiusSafeWord MemberShipSecurID等多種方式。最爲靈活的方式應該贊成可插入的JAAS框架來擴展身份認證的接口
  • 咱們編寫的Filter僅僅能用於J2EE的應用,而對於大量非JavaWeb應用,卻沒法提供SSO服務。
  • 在將Filter應用到Web應用的時候,需要對容器上的每一個應用都要作對應的改動,又一次部署。而更加流行的作法是Agent機制:爲每一個應用server安裝一個agent,就可以將SSO功能應用到這個應用server中的所有應用。
  • 當前的方案不能支持分別位於不一樣domainWeb應用進行SSO。這是因爲瀏覽器在訪問Webserver的時候,只會帶上和當前webserver具備一樣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映射等等,都是一個嚴肅的解決方式需要考慮的問題。
6 桌面 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 的程序,提示需要輸入username和password:
效驗成功之後,便會顯示當前登陸的用戶的基本信息等等。
  這時候再執行第二個桌面 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 用戶(password是 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 文件格式簡單,很是easy編程訪問和改動隨意的 Cookie 值。(我試圖解析 Internet Explorer cookie 文件但沒有成功。)
如下是登陸模塊DesktopSSOLoginModule的主體: login() 方法。邏輯也是很easy:先用 Cookie 來登錄,假設成功,則直接就進入系統,不然需要用戶輸入username和password來登陸系統。
    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;
  }
 
 
用username和password登陸的方法要複雜一些,經過 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;
       
    }
 
另外一個地方需要補充說明的是,在本例子中,username和password的輸入都會在屏幕上顯示明文。假設但願用掩碼形式來顯示password,以提升安全性,請參考: http://java.sun.com/developer/technicalArticles/Security/pwordmask/
7 真正安全的全方位 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 最大的優勢在於它的安全性。經過密鑰算法的保證和密鑰中心的創建,可以作到用戶的password根本不需要在網絡中傳輸,而傳輸的信息也會十分的安全。
眼下支持 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 的包。
 
8 總結
本文的主要目的是闡述 SSO 的基本原理,並提供了一種實現的方式。經過對源碼的分析來掌握開發 SSO 服務的技術要點和充分理解 SSO 的應用範圍。但是,本文只說明瞭身份認證的服務,而另一個和身份認證密不可分的服務 ---- 權限效驗,卻沒有提到。要開發出真正的 SSO 的產品,在功能上、性能上和安全上都必須有更加完備的考慮。
做者簡單介紹
王昱是 Sun 中國project研究院的 Java project師,現在的主要負責全球合做夥伴的技術支持。做爲一名 Java 資深project師和架構師,王昱在 Java 的很是多領域都有多年的造詣,特別是在 Java 虛擬機、 J2EE 技術 ( 包含 EJB, JSP/Servlet, JMS Web services 等技術 ) 、集羣技術和 Java 應用性能調優上有着較爲豐富的經驗。之前屢次在重要的 Java 會議發表演講,並在國際著名的 Java 技術站 點發表文章。
 
資源連接
相關文章
相關標籤/搜索