java web SSO單點登陸

第一篇:javascript

Web應用系統的演化老是從簡單到複雜,從單功能到多功能模塊再到多子系統方向發展。css

.當前的大中型Web互聯網應用基本都是多系統組成的應用羣,由多個web系統協同爲用戶提供服務。html

多系統應用羣,必然意味着各系統間既相對獨立,又保持着某種聯繫。前端

獨立,意味着給用戶提供一個相對完整的功能服務,好比C2C商城,好比B2C商城。聯繫,意味着從用戶角度看,無論企業提供的服務如何多樣化、系列化,在用戶看來,仍舊是一個總體,用戶體驗不能受到影響。java

譬如用戶的帳號管理,用戶應該有一個統一帳號,不該該讓用戶在每一個子系統分別註冊、分別登陸、再分別登出。系統的複雜性不該該讓用戶承擔。mysql

登陸用戶使用系統服務,能夠看作是一次用戶會話過程。在單Web應用中,用戶登陸、登陸狀態判斷、用戶登出等操做,已有很常規的解決方案實現。web

在多系統應用羣中,這個問題就變得有些複雜,之前本不是問題的問題,如今可能就變成了一個重大技術問題。咱們要用技術手段,屏蔽系統底層自己的技術複雜性,給用戶提供天然超爽的用戶體驗。ajax

這就是咱們所說的單點登陸問題,即SSO(Single Sign On)。固然,咱們這裏主要討論的是Web系統,確切地講,應該叫Web SSO。

下面咱們來看一個現實中SSO的例子,例如阿里系統:redis

阿里目前給用戶提供的服務很龐大,種類也很繁多,咱們看幾個典型系統應用:www.taobao.com 淘寶應用、www.tmall.com 天貓應用、
www.alitrip.com 阿里旅遊。這些應用,當用戶訪問時,都須要登陸

顯然,對用戶來講,他不但願每一個子應用分別登陸,由於這都是阿里服務,在用戶看來,就至關於一個大系統。spring

當我在一個應用如淘寶上登陸後,再訪問阿里旅遊、天貓等其它系統,咱們發現,系統都顯示已登陸狀態。

當在任意一系統退出登陸後,再刷新訪問其它系統,均已顯示登出狀態。

能夠看出,阿里實現了SSO。實際上,幾乎全部提供複雜服務的互聯網公司,都實現了SSO,如阿里、百度、新浪、網易、騰訊、58...

SSO問題,是大中型Web應用常常碰到的問題,是Java架構師須要掌握的必備技能之一,中高級以上Web工程師都應對它有個瞭解。

SSO有啥技術難點?爲何咱們不能像解決單Web應用系統登陸那樣天然解決?爲說清楚這一問題,咱們得先了解下單應用系統下,用戶登陸的解決方案。

咱們討論的應用是Web應用,你們知道,對於Web應用,系統是Browser/Server架構,Browser和Server之間的通訊協議是HTTP協議。

HTTP是一個無狀態協議。即對服務器來講,每次收到的瀏覽器HTTP請求都是單一獨立的,服務器並不考慮兩次HTTP請求是否來自同一會話,即HTTP協議是非鏈接會話狀態協議。

對於Web應用登陸,意味着登陸成功後的後續訪問,能夠看作是登陸用戶和服務端的一次會話交互過程,直到用戶登出結束會話。

如何在非鏈接會話協議之上,實現這種會話的管理? 咱們須要額外的手段。

一般有兩種作法,一種是經過使用HTTP請求參數傳遞,這種方式對應用侵入性較大,通常不使用。

另外一種方式就是經過cookie。

cookie是HTTP提供的一種機制,cookie表明一小撮數據。服務端經過HTTP響應建立好cookie後,瀏覽器會接收下來,下次請求會自動攜帶上返回給服務端。

利用這個機制,咱們能夠實現應用層的登陸會話狀態管理。例如咱們能夠把登陸狀態信息保存在cookie中,這是客戶端保存方式。

因爲會話信息在客戶端,須要維護其安全性、須要加密保存、攜帶量會變大,這樣會影響http的處理效率,同時cookie的數據攜帶量也有必定的限制。

比較好的方式是服務端保存,cookie只保存會話信息的句柄。即在登陸成功後,服務端能夠建立一個惟一登陸會話,並把會話標識ID經過cookie返回給瀏覽器,瀏覽器下次訪問時會自動帶上這個ID,服務端根據ID便可判斷是此會話中的請求,從而判斷出是該用戶,這種操做直到登出銷燬會話爲止。

使人高興的是,咱們使用的Web應用服務器通常都會提供這種會話基礎服務,如Tomcat的Session機制。也就是說,應用開發人員沒必要利用Cookie親自代碼實現會話的建立、維護和銷燬等整個生命週期管理,這些內容服務器Session已經提供好了,咱們只需正確使用便可。

固然,爲了靈活性和效率,開發人員也可直接使用cookie實現本身的這種會話管理。

對於Cookie,處於安全性考慮,它有一個做用域問題,這個做用域由屬性Domain和Path共同決定的。也就是說,若是瀏覽器發送的請求不在此Cookie的做用域範圍內,請求是不會帶上此Cookie的。

Path是訪問路徑,咱們能夠定義/根路徑讓其做用全部路徑,Domain就不同了。咱們不能定義頂級域名如.com,讓此Cookie對於全部的com網站都起做用,最大範圍咱們只能定義到二級域名如.taobao.com,而一般,企業的應用羣可能包含有多個二級域名,如taobao.com、tmail.com、alitrip.com等等。

這時,解決單系統會話問題的Cookie機制不起做用了,多系統不能共享同一會話,這就是問題的所在!
固然,有的同窗會說:我把全部的應用統一用三級域名來表示,如a.taobao.com、b.taobao.com、c.taobao.com或乾脆用路徑來區分不一樣的應用如www.taobao.com\a、www.taobao.com\b、www.taobao.com\c,這樣cookie不就能夠共享了麼?

事實是成立的,但現實應用中,多域名策略是廣泛存在的,也有商業角度的考慮,這些咱們必需要面對。

退一步講,即便cookie能夠共享了,服務端如何識別處理這個會話?這時,咱們是不能直接使用服務器所提供的Session機制的,Session是在單一應用範圍內,共享Session須要特殊處理。

更復雜的狀況是,一般這些子系統多是異構的,session實現機制並不相同,若有的是Java系統,有的是PHP系統。共享Session對原系統入侵性很大。

至此,SSO技術問題這裏講清楚了。那咱們有沒有更好的通用解決方案?答案確定是有的,但比較複雜,這也是咱們專題討論的理由。整體來講,咱們須要一箇中央認證服務器,來統一集中處理各子系統的登陸請求。這是入門,後續會有系列文章深層次探討。

圖片描述

 

 

第二篇:

上篇咱們引入了SSO這個話題《15分鐘瞭解SSO是個什麼鬼!》。本篇咱們一步步深刻分析SSO實現機理,並親自動手實現一個線上可用的SSO認證服務器!

首先,咱們來分析下單Web應用系統登陸登出的實現機理。Web系統登陸登出功能,一般屬於系統安全管理模塊的一部分。如上篇所說,登陸,意味着用戶與系統之間的一次會話開始,登出,意味着本次會話的結束。

下圖列出整個登陸登出會話過程當中,用戶與系統之間的HTTP交互過程:

圖片描述

如圖,服務器內部又作了哪些工做呢?

一般服務端在用戶登陸請求到來時(圈1),會先作認證(Authentication)操做,就是證實這個瀏覽器請求用戶是合法系統用戶,通常狀況就是驗證用戶名和密碼。

認證經過後,系統緊接着給這個合法用戶受權(Authorization),就是根據該用戶在此係統中的權限定義,綁定正確的權限信息,爲用戶後續正確使用系統功能提供安全保障。

最後創建會話,這個能夠基於服務器容器提供的Session機制或本身基於Cookie開發的相似功能,創建起本次會話。

登陸成功後,當瀏覽器後續請求來時(圈2),服務器需進行登陸狀態判斷,即判別是否處於會話狀態,從而識別操做是不是本次登陸用戶的操做。

登出時,服務端取消會話,本次登陸用戶會話結束。下次請求時,系統即判斷是非登陸用戶。

上面分析了單Web應用登陸登出實現機理。那對於多系統的SSO,該如何實現呢?咱們先分析下基本實現思路。

有上面分析可知,單Web應用登陸,主要涉及到認證、受權、會話創建、取消會話等幾個關鍵環節。推廣到多系統,每一個系統也會涉及到認證、受權、會話創建取消等工做。那咱們能不能把每一個系統的認證工做抽象出來,放到單獨的服務應用中取處理,是否是就能解決單點登陸問題?

思考方向是正確的,咱們把這個統一處理認證服務的應用叫認證中心。當用戶訪問子系統須要登陸時,咱們把它引到認證中心,讓用戶到認證中心去登陸認證,認證經過後返回並告知系統用戶已登陸。當用戶再訪問另外一系統應用時,咱們一樣引導到認證中心,發現已經登陸過,即返回並告知該用戶已登陸

圖片描述

由上圖能夠看出,原先在各系統中的認證模塊,已經剝離出來放到獨立的認證中心中去執行。至於受權,每一個應用都不同,可維持原樣。會話部分,其實分解成了全局會話和局部會話,這個後面再詳細解釋。

思路是有了,但真正實現這一認證服務器咱們還得要具體解決幾個關鍵問題

1、登陸信息傳遞問題

應用系統將登陸請求轉給認證中心,這個很好解決,咱們一個HTTP重定向便可實現。如今的問題是,用戶在認證中心登陸後,認證中心如何將消息轉回給該系統?

這是在單web系統中不存在的問題。咱們知道HTTP協議傳遞消息只能經過請求參數方式或cookie方式,cookie跨域問題不能解決,咱們只能經過URL請求參數。

咱們能夠將認證經過消息作成一個令牌(token)再利用HTTP重定向傳遞給應用系統。但如今的關鍵是:該系統如何判斷這個令牌的真僞?若是判斷這個令牌確實是由認證中心發出的,且是有效的?

咱們還須要應用系統和認證中心之間再來個直接通訊,來驗證這個令牌確實是認證中心發出的,且是有效的。因爲應用系統和認證中心是屬於服務端之間的通訊,不通過用戶瀏覽器,相對是安全的。
圖片描述
用戶首次登陸時流程以下:

1.用戶瀏覽器訪問系統A需登陸受限資源。

2.系統A發現該請求須要登陸,將請求重定向到認證中心,進行登陸。

3.認證中心呈現登陸頁面,用戶登陸,登陸成功後,認證中心重定向請求到系統A,並附上認證經過令牌。

4.系統A與認證中心通訊,驗證令牌有效,證實用戶已登陸。

5.系統A將受限資源返給用戶。

圖片描述

已登陸用戶首次訪問應用羣中系統B時:

  1. 瀏覽器訪問另外一應用B需登陸受限資源。

  2. 系統B發現該請求須要登陸,將請求重定向到認證中心,進行登陸。

  3. 認證中心發現已經登陸,即重定向請求響應到系統B,附帶上認證令牌。

  4. 系統B與認證中心通訊,驗證令牌有效,證實用戶已登陸。

  5. 系統B將受限資源返回給客戶端。

2、登陸狀態判斷問題

用戶到認證中心登陸後,用戶和認證中心之間創建起了會話,咱們把這個會話稱爲全局會話。當用戶後續訪問系統應用時,咱們不可能每次應用請求都到認證中心去斷定是否登陸,這樣效率很是低下,這也是單Web應用不須要考慮的。

咱們能夠在系統應用和用戶瀏覽器之間創建起局部會話,局部會話保持了客戶端與該系統應用的登陸狀態,局部會話依附於全局會話存在,全局會話消失,局部會話必須消失。

用戶訪問應用時,首先判斷局部會話是否存在,如存在,即認爲是登陸狀態,無需再到認證中心去判斷。如不存在,就重定向到認證中心判斷全局會話是否存在,如存在,按1提到的方式通知該應用,該應用與客戶端就創建起它們之間局部會話,下次請求該應用,就不去認證中心驗證了。

3、登出問題

用戶在一個系統登出了,訪問其它子系統,也應該是登出狀態。要想作到這一點,應用除結束本地局部會話外,還應該通知認證中心該用戶登出。

認證中心接到登出通知,便可結束全局會話,同時須要通知全部已創建局部會話的子系統,將它們的局部會話銷燬。這樣,用戶訪問其它應用時,都顯示已登出狀態。

圖片描述

整個登出流程以下:

1.客戶端嚮應用A發送登出Logout請求。

2.應用A取消本地會話,同時通知認證中心,用戶已登出。

3.應用A返回客戶端登出請求。

4.認證中心通知全部用戶登陸訪問的應用,用戶已登出。

到此,咱們完整介紹了實現一個SSO認證服務器的基本思路方法,後面咱們就按照這個方法,本身動手利用Java一步步實現一個SSO認證服務器。

圖片描述

 

第三篇:

上篇《實現一個SSO認證服務器是這樣的》中,咱們詳細講述了實現SSO的基本思路,本篇咱們按照這個思路,親自動手實現一個輕量級的SSO認證中心。
除了認證中心,咱們還要改造系統應用的登陸登出部分,使之與認證中心交互,共同完成SSO。

所以咱們的實現分紅兩大部分,一個是SSO Server,表明認證中心,另外一個是SSO Client,表明使用SSO系統應用的登陸登出組件。咱們給咱們實現的這個SSO工程起個名字,叫Nebula。

咱們先討論下Nebula中幾個關鍵問題的實現:

1. 登陸令牌token的實現

前面咱們討論了,系統把用戶重定向導向認證中心並登陸後,認證中心要把登陸成功信息經過令牌方式告訴給應用系統。認證中心會記錄下來自某個應用系統的某個用戶本次經過了認證中心的認證所涉及的基本信息,並生成一個登陸令牌token,認證中心須要經過URL參數的形式把token傳遞迴應用系統,因爲通過客戶端瀏覽器,故令牌token的安全性很重要。

所以令牌token的實現要知足三個條件:

首先,token具備惟一性,它表明着來自某應用系統用戶的一次成功登陸。咱們能夠利用java util包工具直接生成一個32位惟一字符串來實現。

String token = UUID.randomUUID().toString();

同時,咱們定義一個javabean, TokenInfo 來承載token所表示的具體內容,即某個應用系統來的某個用戶本次經過了認證中心

public class TokenInfo { private int userId; //用戶惟一標識ID private String username; //用戶登陸名 private String ssoClient; //來自登陸請求的某應用系統標識 private String globalId; //本次登陸成功的全局會話sessionId ... }

token和tokenInfo造成了一個<key,value>形式的鍵值對,後續應用系統向認證中心驗證token時還會用到。

其次,token存在的有效期間不能過長,這是出於安全的角度,例如token生存最大時長爲60秒。

咱們能夠直接利用redis特性來實現這一功能。redis本質就是<key,value>鍵值對形式的內存數據庫,而且這個鍵值對能夠設置有效時長。

第三,token只能使用一次,用完即做廢,不能重複使用。這也是保證系統安全性。

咱們能夠定義一個TokenUtil工具類,來實現<token,tokenInfo>鍵值對在redis中的操做,主要接口以下:

public class TokenUtil { ... // 存儲臨時令牌到redis中,存活期60秒 public static void setToken(String tokenId, TokenInfo tokenInfo){ ... } //根據token鍵取TokenInfo public static TokenInfo getToken(String tokenId){ ... } //刪除某個 token鍵值 public static void delToken(String tokenId){ ... } }

2. 全局會話和本地會話的實現
用戶登陸成功後,在瀏覽器用戶和認證中心之間會創建全局會話,瀏覽器用戶與訪問的應用系統之間,會創建本地局部會話。

爲簡便,可使用web應用服務器(如tomcat)提供的session功能來直接實現。

這裏須要注意的是,咱們須要根據會話ID(即sessionId)能訪問到這個session。由於根據前面登出流程說明,認證中心的登出請求不是直接來自鏈接的瀏覽器用戶,可能來自某應用系統。認證中心也會通知註冊的系統應用進行登出。

這些請求,都是系統之間的交互,不通過用戶瀏覽器。系統要有根據sessionId訪問session的能力。同時,在認證中心中,還須要維護全局會話ID和已登陸系統本地局部會話ID的關係,以便認證中心可以通知已登陸的系統進行登出處理。

爲了安全,目前的web應用服務器,如tomcat,是不提供根據sessionId訪問session的能力的,那是容器級範圍內的能力。咱們須要在本身的應用中,本身維護一個sessionId和session直接的對應關係,咱們把它放到一個Map中,方便須要時根據sessionId找到對應的session。同時,咱們藉助web容器提供的session事件監聽能力,程序來維護這種對應關係。

認證中心涉及到兩個類,GlobalSessions和GlobalSessionListener,相關代碼以下:

public class GlobalSessions { //存放全部全局會話 private static Map<String, HttpSession> sessions = new HashMap<String,HttpSession>(); public static void addSession(String sessionId, HttpSession session) { sessions.put(sessionId, session); } public static void delSession(String sessionId) { sessions.remove(sessionId); } //根據id獲得session public static HttpSession getSession(String sessionId) { return sessions.get(sessionId); } } public class GlobalSessionListener implements HttpSessionListener{ public void sessionCreated(HttpSessionEvent httpSessionEvent) { GlobalSessions.addSession( httpSessionEvent.getSession().getId(), httpSessionEvent.getSession()); } public void sessionDestroyed(HttpSessionEvent httpSessionEvent) { GlobalSessions.delSession(httpSessionEvent.getSession().getId()); } }

SSO Client對應的是LocalSessions和LocalSessionListener,實現方式同上。

3. 應用系統和認證中心之間的通訊
根據SSO實現流程,應用系統和認證中心之間須要直接通訊。如應用系統須要向認證中心驗證令牌token的真僞,應用系統通知認證中心登出,認證中心通知全部已註冊應用系統登出等。這是Server之間的通訊,如何實現呢?咱們可使用HTTP進行通訊,返回的消息應答格式可採用JSON格式。

Java的net包,提供了http訪問服務器的能力。這裏,咱們使用apache提供的一個更強大的開源框架,httpclient,來實現應用系統和認證中心之間的直接通訊。JSON和JavaBean之間的轉換,目前經常使用的有兩個工具包,一個是json-lib,還有一個是Jackson,Jackson效率較高,依賴包少,社區活躍度大,這裏咱們使用Jackson這個工具包。

如應用系統向認證中心發送token驗證請求的代碼片斷以下:

//向認證中心發送驗證token請求 String verifyURL = "http://" + server + PropertiesConfigUtil.getProperty("sso.server.verify"); HttpClient httpClient = new DefaultHttpClient(); //serverName做爲本應用標識 HttpGet httpGet = new HttpGet(verifyURL + "?token=" + token + "&localId=" + request.getSession().getId()); try{ HttpResponse httpResponse = httpClient.execute(httpGet); int statusCode = httpResponse.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_OK){ String result = EntityUtils.toString(httpResponse.getEntity(), "utf-8"); //解析json數據 ObjectMapper objectMapper = new ObjectMapper(); VerifyBean verifyResult = objectMapper.readValue(result, VerifyBean.class); //驗證經過,應用返回瀏覽器須要驗證的頁面 if(verifyResult.getRet().equals("0")){ Auth auth = new Auth(); auth.setUserId(verifyResult.getUserId()); auth.setUsername(verifyResult.getUsername()); auth.setGlobalId(verifyResult.getGlobalId()); request.getSession().setAttribute("auth", auth); //創建本地會話 return "redirect:http://" + returnURL; } } }catch(Exception e){ return "redirect:" + loginURL; }


核心實現細節討論清楚了,咱們就能夠根據上篇登陸登出操做流程,定義Nebula Server和Nebula Client所提供的接口。爲了解釋方便,咱們把上篇刻畫的登陸登出時系統之間調用的時序交互圖從新展現在這裏:

首次登陸時:
圖片描述
系統登出時:

圖片描述



Nebula Server認證中心包含四個重要相關接口,分別以下:

圖片描述

說明:此接口主要接受來自應用系統的認證請求,此時,returnURL參數需加上,用以向認證中心標識是哪一個應用系統,以及返回該應用的URL。如用戶沒有登陸,應用中心向瀏覽器用戶顯示登陸頁面。如已登陸,則產生臨時令牌token,並重定向回該系統。上面登陸時序交互圖中的2和此接口有關。

固然,該接口也同時接受用戶直接向認證中心登陸,此時沒有returnURL參數,認證中心直接返回登陸頁面。
圖片描述
說明: 處理瀏覽器用戶登陸認證請求。如帶有returnURL參數,認證經過後,將產生臨時認證令牌token,並攜帶此token重定向回系統。如沒有帶returnURL參數,說明用戶是直接從認證中心發起的登陸請求,認證經過後,返回認證中心首頁提示用戶已登陸。上面登陸時序交互圖中的3和此接口有關。

圖片描述

說明:認證應用系統來的token是否有效,若有效,應用系統向認證中心註冊,同時認證中心會返回該應用系統登陸用戶的相關信息,如ID,username等。上面登陸時序交互圖中的4和此接口有關。

圖片描述

說明:登出接口處理兩種狀況,一是直接從認證中心登出,一是來自應用重定向的登出請求。這個根據gId來區分,無gId參數說明直接從認證中心註銷,有,說明從應用中來。接口首先取消當前全局登陸會話,其次根據註冊的已登陸應用,通知它們進行登出操做。上面登出時序交互圖中的2和4與此接口有關。

Nebula Client鏈接組件包含兩個重要接口:

圖片描述

說明:接收來自認證中心攜帶臨時令牌token的重定向,向認證中心/auth/verify接口去驗證此token的有效性,若有效,即創建本地會話,根據returnURL返回瀏覽器用戶的實際請求。如驗證失敗,再重定向到認證中心登陸頁面。上面登陸時序交互圖中的4與此接口有關。

圖片描述

說明:處理兩種狀況,一種是瀏覽器向本應用接口發出的直接登出請求,應用會消除本地會話,調用認證服務器/auth/logout接口,通知認證中心刪除全局會話和其它已登陸應用的本地會話。 若是是從認證中心來的登出請求,此時帶有localId參數,接口實現會直接刪除本地會話,返回字符串"ok"。上面登出時序交互圖中的1和2與此接口有關。

至此,咱們把整個Nebula Server和Nebula Client實現細節都介紹清楚了。有了核心代碼片斷、有了詳細接口說明,我想你可以本身動手實現這個Nebula。固然,筆者後續會整理相關工程代碼,以某種適當形式開放給本社羣會員們!有什麼問題可直接給本公衆號發消息,筆者收集後會統一回答。
接下來,咱們介紹業界影響最大、使用最多的SSO開源解決方案,CAS。

圖片描述

 

第四篇:

上一篇《相遇篇》咱們做爲新手初步瞭解了CAS,安裝並進行了簡單體驗。這篇咱們進一步深刻認識CAS。
CAS原理和咱們前面本身開發的Nebula基本一致,全部的系統應用都會引導到CAS Server認證中心去登陸。登陸成功後,認證中心會產生一個票據叫TGT(Ticket Granting Ticket),TGT即表明了用戶與認證中心直接的全局會話。TGT存在,代表該用戶處於登陸狀態。

TGT並無放在Session中,也就是說,CAS全局會話的實現並無直接使用Session機制,而是利用了Cookie本身實現的,這個Cookie叫作TGC(Ticket Granting Cookie),它存放了TGT的id,認證中心服務端實現了TGT。

咱們利用上篇安裝的CAS,在認證中心登陸下,看下登陸先後cookie的變化。顯然,在登陸後,多出一個叫CASTGC的Cookie,它來維持全局會話。

登陸前:

q1

登陸後:

q2

若是是應用系統登陸,同理,會引導到認證中心進行登陸,登陸成功後再重定向迴應用系統,這時會帶上一個登陸令牌,告知系統應用登陸成功。

這個令牌,在CAS中叫作ST(Service Ticket)服務票據,它的做用和Nebula的token相似。固然,和Nebula同樣,應用系統收到ST後,會直接向CAS Server去驗證,驗證經過後,應用系統便可創建本地會話,返回用戶訪問的受限資源。
q3
下面咱們利用前面搭建的環境,看下從應用系統首次登陸時的狀況。首先訪問www.ssoclient.com:81/index.do,系統重定向到www.cas.com認證中心,輸入用戶名密碼後,攜帶ST重定向回www.ssoclient.com:81/index.do 下面是重要的三個HTTP走向。
q4
其中第一個,咱們看到,登陸成功後,系統會設置CASTGC Cookie,同時重定向迴應用時帶上了一個ticket變量,這個就是ST。

CAS官網給出了詳細的用戶登陸時序圖,很是詳細,這裏就不從新畫輪子了,直接引用以下:

q5

從流程圖能夠看出,和Nebula的時序流程圖幾乎如出一轍。固然,做爲成熟開源的CAS,考慮的應用場景更加豐富些。到目前爲止,其CAS認證協議已經持續發展了三個版本,v1實現了單點登陸,此版本跟Nebula實現的功能差很少。

v2版則重點增長了proxy模式,代理模式是一種更復雜形式的認證,即認證的Web應用(CAS Client)能夠做爲代理直接訪問須要認證的後端服務(如郵件服務器),瀏覽器用戶無需再和後端服務直接進行認證交互。這個比較複雜,在門戶中可能會用到,咱們這裏不作討論。

v3版本則更加豐富了協議,認證中心驗證票據後不只能夠返回身份ID,還可攜帶用戶暱稱、性別等其餘用戶基本信息。

CAS不只提供專有的CAS協議,還同時支持SAML 1.一、OpenID、OAuth等標準開放協議,具備更普遍的可用性。

CAS工程採用模塊化插件化設計思想,其核心子工程是cas-server-core,提供CAS最核心的功能。其它功能以插件形式提供,放在不一樣子工程中。

CAS雖然以Web應用的形式提供服務,但從理論上講,CAS提供的核心功能是基於票據令牌方式的認證,這種認證,和是不是Web應用方式運行無關。故實際上,cas-server-core提供的核心模塊只有兩部分,一是票據Ticket,包括票據的產生、查詢、刪除、存儲等各類操做。另外一個是認證,提供多種認證方式。

固然,CAS是支持Web應用的單點登陸,確切說是Web SSO解決方案。故cas-server-core還提供了web層的一些最基本邏輯框架,如登陸請求接收。

做爲獨立運行的Web應用,CAS還需提供與瀏覽器用戶的交互,與須要認證的應用系統交互,這些邏輯,絕大部分放在cas-server-webapp和cas-server-webapp-support兩個子工程中。CAS認證中心採用Spring MVC + WebFlow實現,cas-server-webapp提供了相關交互頁面和web工程配置,而cas-server-webapp-support提供了基於WebFlow的相關認證流程邏輯代碼,它將調用後端cas-server-core提供的票據和認證功能。

CAS應用的總體架構官方提供了一個比較清晰的架構圖,以下所示:

q6

對CAS工程內部有個大體認識後,咱們開始動手實踐,咱們一步步實踐如何在工程中實際應用CAS。

首先,咱們來看下如何修改登陸登出流程及相關頁面,知足實際應用需求。咱們讓CAS完成和咱們前面Nebula Server一致的登陸登出流程並提供一致的交互頁面。

CAS頁面採用主題模板方式,咱們的需求其實就是至關於定製一套本身的主題模板。

在WEB-INF/cas.properties 文件中找到:

cas.themeResolver.defaultThemeName=cas-theme-default cas.viewResolver.basename=default_views

這兩個參數指定了樣式定義文件和模板定義文件,他們在WEB-INF/cas-servlet.xml 中引用到。修改這兩個值,使用本身定義的文件:

cas.themeResolver.defaultThemeName=nebula-theme cas.viewResolver.basename=nebula_views

仿照resources/cas-theme-default.properties 建立nebula-theme.properties文件,其內容是定義了css文件和js文件的位置。因爲咱們沒用到js,只定義以下內容:

standard.custom.css.file=/css/nebula.css

建立/css/nebula.css文件,將nebula中的css文件內容copy過來。

同理仿照resources/default_views.properties 建立nebula_views.properties,CAS缺省定義了不少頁面模板,表明不一樣流程節點須要給用戶的接口頁面。

按照咱們前面開發Nebula所使用的登陸登出流程,只提供兩個頁面,一個是登陸頁面,用以處理用戶登陸問題,一個是首頁,登陸前和登陸後給用戶的接口頁面。所以,在nebula_views中,只定義兩個模板便可。

nebulaLoginView.(class)=org.springframework.web.servlet.view.JstlView nebulaLoginView.url=/WEB-INF/view/jsp/nebula/ui/login.jsp
nebulaIndexView.(class)=org.springframework.web.servlet.view.JstlView nebulaIndexView.url=/WEB-INF/view/jsp/nebula/ui/index.jsp

同時,咱們將nebula中兩個頁面,copy到/WEB-INF/view/jsp/nebula/ui 下並作相應調整。

按照CAS登陸邏輯,對login.jsp,除了username、password兩個登陸參數外,form中還須要增長三個隱藏參數,lt、execution、_eventId,lt主要是爲了增長頁面登陸安全性,防止重複提交,其它兩個確保正確走webflow登陸流程。

CAS的登陸登出過程由webflow定義,這樣能夠更好地實現登陸登出過程的定製化。登陸的定義流程說明是login-webflow.xml,登出時logout-webflow.xml。咱們分別copy這兩個文件爲nebula-login-webflow.xml和nebula-logout-webflow.xml做爲流程定義的基礎,並在cas-servlet.xml中修改登陸登出流程定義文件爲上述文件。

這裏特別提到cas-servlet.xml文件,cas-servlet.xml是CAS的spring mvc配置文件,文件中定義了服務接口和定義處理的Controller:

q8

咱們看到上面爲啥沒有/login 和 /logout 接口呢?其實他們已經配置成webflow流程入口,分別走nebula-login-webflow.xml和nebula-logout-webflow.xml流程。

q9

id即對應的path /login,logout同理,在文件下面可以找到。

根據要求,咱們修改nebula-login-webflow.xml和nebula-logout-webflow.xml中內容。對於nebula-login-webflow.xml,去掉沒必要要的流程節點,如warn和proxy等,同時將錯誤響應直接指向登陸頁面,故登陸流程中只提供兩個響應頁面即nebulaLoginView和nebulaIndexView。

對於nebula-logout-webflow.xml,登出正常結束後直接再走登陸流程。這樣和Nebula的登陸登出處理流程基本一致。

OK,咱們看下修改後的效果。先看下直接在認證中心登陸登出的狀況:

輸入www.cas.com即顯示登陸頁面:

q10

登陸成功後顯示首頁:

q11

點擊登出,又顯示登陸頁面,從應用系統登陸同理。至此,咱們改造了CAS認證中心登陸登出流程和相關界面模板,和Nebula認證中心給用戶的體驗效果基本一致。

q12

 

 

第五篇:

上一篇《相識篇》咱們進行了登陸登出流程定製和頁面定製實踐,本篇咱們繼續經過實踐深刻認識CAS。

記住密碼也是咱們登陸常提供的功能,CAS自己已經提供。下面咱們來看下如何配置來實現這個功能,咱們仍是繼續使用前面的例子做爲實踐。

1. 修改deployerConfigContext.xml,找到bean id="authenticationManager" 定義區,裏面添加以下內容:

<property name="authenticationMetaDataPopulators"> <util:list> <bean class="org.jasig.cas.authentication.principal.RememberMeAuthenticationMetaDataPopulator" /> </util:list> </property>

此處的修改主要是將頁面提交的rememberMe屬性傳遞到內部生成的authentication對象中。

2. 修改登陸流程文件內容,根據上篇實踐工程,咱們如今使用的是nebula-login-webflow.xml

修改credential爲:

<var name="credential" class="org.jasig.cas.authentication.RememberMeUsernamePasswordCredential" />

viewLoginForm中增長rememberMe綁定變量:

<binder> <binding property="username" /> <binding property="password" /> <binding property="rememberMe" /> </binder>

此處主要是接收登陸頁面的rememberMe屬性。

3.ticketExpirationPolicies.xml文件中,註釋掉原grantingTicketExpirationPolicy,啓用新的票據過時策略:

<bean id="grantingTicketExpirationPolicy" class="org.jasig.cas.ticket.support.RememberMeDelegatingExpirationPolicy"> <property name="sessionExpirationPolicy"> <bean class="org.jasig.cas.ticket.support.TimeoutExpirationPolicy"> <constructor-arg index="0" value="7200000" /> </bean> </property> <property name="rememberMeExpirationPolicy"> <bean class="org.jasig.cas.ticket.support.TimeoutExpirationPolicy"> <constructor-arg index="0" value="604800000" /> </bean> </property> </bean>

上述TGT票據過時策略定義的是若是前端頁面提交時「remember me」選項沒有選中,定義的是false,執行2小時用戶沒有操做應用TGT過時策略,若是選中了免登陸,7天過時。固然,能夠根據狀況修改上面的數字,注意單位是毫秒。

這是remember me這個功能的核心。咱們知道TGT表明的是全局會話,remember me意味着全局會話的有效期延長,上述就是定義TGT的過時策略。

4. 最後是修改登陸頁面,增長「記住我」remember me選項:

<div><input type="checkbox" name="rememberMe" id="rememberMe" checked="true" />記住我</div>

好了,作了如上修改,咱們啓動工程,直接在認證中心登陸,顯示的登陸頁面爲:

q2

登陸成功後,咱們關掉瀏覽器,再打開,輸入www.cas.com,則顯示

q3

此時,記住密碼功能生效。

remember me主要涉及的是TGT的有效期問題,這裏咱們再深刻討論一下。TGT的有效期控制採用策略模式實現。缺省是TicketGrantingTicketExpirationPolicy,即應用空閒超過2小時,TGT存在超過8小時即過時策略。CAS提供了幾種Policy,通常都能知足咱們的需求,固然咱們也能夠根據須要本身定義特殊的Policy策略。

TGT缺省採用虛擬機內存方式存儲,其生命週期由Policy控制。同時ticket的時效是被動後驗方式,在這種狀況下,咱們還須要一個清除器按期清除內存中過時的還未通過處理的ticket。這個清除器在ticketRegistry.xml中定義,叫ticketRegistryCleaner,定時任務採用spring集成的Quartz實現。

當咱們採用第三方Cache工具如redis、memcached等能控制數據存儲時效的其它存儲策略實現時,這時ticketRegistryCleaner就再也不須要了。但要注意,存儲數據時的有效時長要大於等於policy定義的有效時效。

咱們知道,TGT的ID在客戶端TGC Cookie中,所以保持全局會話,不只要服務端TGT這個票據對象存在,同時TGC這個Cookie也不能過時。在ticketGrantingTicketCookieGenerator.xml中,缺省狀況下p:cookieMaxAge設置爲-1,表示長期有效。這裏不須要修改,咱們只須要服務端用policy控制TGT的有效期就能夠了。

CAS給出的缺省例子是將帳戶信息(用戶名/密碼)放在配置文件中,實際運行系統,帳戶信息一般是在數據庫中保存。如今咱們就配置一下如何對數據庫中保存的帳戶進行認證。

1. 首先,在mysql數據庫中創建一張帳戶表account,並添加一些帳戶例子。
q5
注意,密碼咱們使用了MD5加密方式。

2. 在deployerConfigContext.xml文件中找到bean id="primaryAuthenticationHandler" 區並註釋掉,替換成以下內容:

<bean id="primaryAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> <property name="dataSource" ref="dataSource" /> <property name="sql" value="select password from account where username=? " /> <property name="passwordEncoder" ref="passwordEncoder"/> </bean>

也就是說,咱們替換了認證插件,採用SQL語句訪問數據庫獲取密碼認證方式。QueryDatabaseAuthenticationHandler在cas-server-support-jdbc子工程中。

CAS提供了多種認證方式,除JDBC訪問數據庫外,還可使用LDAP、x50九、spnego等方式,這些都以子工程插件的方式提供。固然,咱們一樣能夠開發本身須要的插件。

因爲咱們密碼使用了MD5加密方式,咱們引進了passwordEncoder,定義以下:

<bean id="passwordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" autowire="byName"> <constructor-arg value="MD5"/> <property name="characterEncoding"> <value>UTF-8</value> </property> </bean>

而後咱們定義數據源,這在spring工程中很常見:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName"> <value>com.mysql.jdbc.Driver</value> </property> <property name="url"> <value>jdbc:mysql://127.0.0.1:3306/jiweibu?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true</value> </property> <property name="username" value="root" /> <property name="password" value="123456" /> <property name="initialSize" value="10" /> <property name="maxActive" value="500" /> <property name="maxWait" value="60000" /> <property name="validationQuery"><value>select 1</value></property> </bean>

3. 在pom.xml中引入下面三個工程包,包括CAS JDBC認證插件、Mysql驅動還有dbcp鏈接池:

<dependency> <groupId>org.jasig.cas</groupId> <artifactId>cas-server-support-jdbc</artifactId> <version>${project.version}</version> <scope>compile</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.26</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency>

從新編譯build,運行修改的CAS認證中心,使用數據庫中的帳號嘗試登陸。

至此,咱們介紹瞭如何修改登陸登出流程,如何修改CAS Server相關界面,如何實現記住密碼免登陸功能,以及如何訪問數據庫中帳號去認證。經過這些實踐活動,咱們對CAS有了至關的理解,能夠應對通常的應用場景了。

後面,咱們將進一步深刻CAS,解決在大型互聯網應用中,基於CAS實現SSO須要關注的問題。

q6

 

 

第六篇:

前面咱們對CAS作了至關的瞭解,也基本可以將CAS應用於生產環境。本篇筆者將結合自身實際工做經驗,談談在大型互聯網應用中,如何架構CAS的問題。內容絕對乾貨,值得珍藏:)

對於大中型互聯網應用,網站性能問題提到了史無前例的高度,可以應對高併發、高可用、避免單點故障是系統架構設計的基本準則。

若是引入了SSO,那個這個認證中心就是整個應用架構中的一個及其重要的關鍵點,它必須知足兩個基本要求:

1.高可用,不容許發生故障。可想而知,若是認證中心發生故障,整個應用羣將沒法登錄,將會致使全部服務癱瘓。

2.高併發,由於全部用戶的登陸請求都須要通過它處理,其承擔的處理量經常是至關巨大的。

所以,在實際生產系統中,認證中心這個關鍵部件一般須要進行集羣,單個認證中心提供服務是很是危險的。

當咱們用CAS做爲SSO解決方案時,CAS Server做爲認證中心就會涉及到集羣問題。對CAS Server來講,缺省是單應用實例運行的,多實例集羣運行,咱們須要作特殊考慮。

考慮集羣,就要考慮應用中有哪些點和狀態相關,這些狀態相關的點和應用的運行環境密切相關。在多實例運行下,運行環境是分佈式的,這些狀態相關的點須要考慮,在分佈式環境下,如何保持狀態的一致性。

鑑於CAS實現方式,狀態相關點有兩個,一是CAS登陸登出流程,採用webflow實現,流程狀態存儲於session中。二是票據存儲,缺省是在JVM內存中。

那麼CAS集羣,咱們須要保證多個實例下,session中的狀態以及票據存儲狀態,是一致的。經常使用的解決方案是共享,也就是說,在多CAS實例下,他們的session和票據ticket是共享的,這樣就解決了一致性問題。

CAS在Tomcat下運行的話,官方提出的建議是利用tomcat集羣進行Session複製(Session Replication)。在高併發狀態下,這種session複製效率不是很高,節點數增多時更是如此,實戰中採用較少。

咱們能夠採用共享session的技術。但筆者實踐中,則採用了另一種更靈活的方案,那就是session sticky技術。

什麼是session sticky?即將某一用戶來的請求,經過前置機合理分配,始終定位在一臺tomcat上,這樣用戶的登陸登出webflow流程,始終發生在同一tomcat服務器上,保證了狀態的完整性。實際上,採用這種方式,咱們繞過了Session共享的需求。

另外一個問題咱們繞不過去了,那就是ticket共享問題。咱們知道,ticket缺省是存儲於虛擬機內存中的,多個CAS Server實例,意味着多個tomcat節點,多個JVM,TicketRegistry是各自獨立不共享的。

咱們是否也可以使用session sticky解決呢,不能夠!由於對於ticket來講,根據認證協議,訪問ticket不只來自瀏覽器用戶請求,並且還來自CAS Client應用系統,這是一個三方合做系統。來自應用系統的請求可能會訪問到另外一個CAS Server節點從而致使狀態不一致。

所以咱們要直面解決ticket共享問題。ticket的存儲由TicketRegistry定義,缺省是DefaultTicketRegistry,即JVM內存方式實現,咱們能夠定義外置存儲方式,讓多個實例共用這個存儲,以達到共享目的。

外置存儲實現方式有多種選擇,如存儲在數據庫中、存儲在Cache中、存儲在內存數據庫中等,CAS也提供了多種實現方式的插件,如利用memcached做爲ticket存儲方式的插件cas-server-integration-memcached、利用Cache的cas-server-integration-ehcache、cas-server-integration-jboss等。

這裏,使用另一種方式,即利用目前更流行的內存數據管理系統Redis來存儲Ticket。同時,爲了保證redis的高可用和高併發處理,咱們使用redis主從集羣,Sentinel控制,故認證中心具備很好的靈活性和水平可擴展性,整個架構圖以下:
q1
下面咱們就一步步進行配置搭建:

1.仿照cas-server-integration-memcached工程創建cas-server-integration-redis工程

q2

2.pom.xml中添加redis的java客戶端jar包,去掉memcached中須要的jar,最後依賴包以下:

<dependencies> <dependency> <groupId>org.jasig.cas</groupId> <artifactId>cas-server-core</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.7.2</version> </dependency> <!-- Test dependencies --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>1.9.0</version> <scope>test</scope> </dependency> </dependencies>
  1. 定義RedisTicketRegistry類,這個是核心,它實現了TicketRegistry接口,咱們使用Jedis客戶端:
public final class RedisTicketRegistry extends AbstractDistributedTicketRegistry implements DisposableBean { /** Redis client. */ private JedisSentinelPool jedisPool; private int st_time; //ST最大空閒時間 private int tgt_time; //TGT最大空閒時間 @Override protected void updateTicket(final Ticket ticket) { logger.debug("Updating ticket {}", ticket); Jedis jedis = jedisPool.getResource(); String ticketId = ticket.getId() ; try { jedis.expire(ticketId.getBytes(), getTimeout(ticket)); }catch (final Exception e) { logger.error("Failed updating {}", ticket, e); }finally{ jedis.close(); } } @Override public void addTicket(final Ticket ticket) { logger.debug("Adding ticket {}", ticket); Jedis jedis = jedisPool.getResource(); String ticketId = ticket.getId() ; ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = null; try{ oos = new ObjectOutputStream(bos); oos.writeObject(ticket); }catch(IOException e){ logger.error("adding ticket {} to redis error.", ticket); }finally{ try{ if(null!=oos) oos.close(); }catch(IOException e){ logger.error("oos closing error when adding ticket {} to redis.", ticket); } } jedis.setex(ticketId.getBytes(), getTimeout(ticket),bos.toByteArray()); jedis.close(); } @Override public boolean deleteTicket(final String ticketId) { logger.debug("Deleting ticket {}", ticketId); Jedis jedis = jedisPool.getResource(); try { jedis.del(ticketId.getBytes()); return true; } catch (final Exception e) { logger.error("Failed deleting {}", ticketId, e); return false; } finally{ jedis.close(); } } @Override public Ticket getTicket(final String ticketId) { Jedis jedis = jedisPool.getResource(); try { byte[] value = jedis.get(ticketId.getBytes()); if (null==value){ logger.error("Failed fetching {}, ticketId is null. ", ticketId); return null; } ByteArrayInputStream bais = new ByteArrayInputStream(value); ObjectInputStream ois = null; ois = new ObjectInputStream(bais); final Ticket t = (Ticket)ois.readObject(); if (t != null) { return getProxiedTicketInstance(t); } } catch (final Exception e) { logger.error("Failed fetching {}. ", ticketId, e); }finally{ jedis.close(); } return null; } /** * {@inheritDoc} * This operation is not supported. * * @throws UnsupportedOperationException if you try and call this operation. */ @Override public Collection<Ticket> getTickets() { throw new UnsupportedOperationException("GetTickets not supported."); } /** * Destroy the client and shut down. * * @throws Exception the exception */ public void destroy() throws Exception { jedisPool.destroy(); } @Override protected boolean needsCallback() { return true; } /** * Gets the timeout value for the ticket. * * @param t the t * @return the timeout */ private int getTimeout(final Ticket t) { if (t instanceof TicketGrantingTicket) { return this.tgt_time; } else if (t instanceof ServiceTicket) { return this.st_time; } throw new IllegalArgumentException("Invalid ticket type"); } public void setSt_time(int st_time) { this.st_time = st_time; } public void setTgt_time(int tgt_time) { this.tgt_time = tgt_time; } public void setJedisSentinelPool(JedisSentinelPool jedisPool) { this.jedisPool = jedisPool; } }

4.同理,仿照cas-server-integration-memcached編寫測試用例RedisTicketRegistryTests,核心代碼以下:

@Test public void testWriteGetDelete() throws Exception { //對ticket執行增查刪操做 final String id = "ST-1234567890ABCDEFGHIJKL-crud"; final ServiceTicket ticket = mock(ServiceTicket.class, withSettings().serializable()); when(ticket.getId()).thenReturn(id); registry.addTicket(ticket); final ServiceTicket ticketFromRegistry = (ServiceTicket) registry.getTicket(id); Assert.assertNotNull(ticketFromRegistry); Assert.assertEquals(id, ticketFromRegistry.getId()); registry.deleteTicket(id); Assert.assertNull(registry.getTicket(id)); }

相應的配置文件ticketRegistry-test.xml定義以下:

<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxTotal" value="4096"/> <property name="maxIdle" value="200"/> <property name="maxWaitMillis" value="3000"/> <property name="testOnBorrow" value="true" /> <property name="testOnReturn" value="true" /> </bean> <bean id="jedisSentinelPool" class="redis.clients.jedis.JedisSentinelPool"> <constructor-arg index="0" value="mymaster" /> <constructor-arg index="1"> <set> <value>192.168.1.111:26379</value> </set> </constructor-arg> <constructor-arg index="2" ref="poolConfig"/> </bean> <bean id="testCase1" class="org.jasig.cas.ticket.registry.RedisTicketRegistry" > <property name="jedisSentinelPool" ref="jedisSentinelPool" /> <property name="st_time" value="10" /> <property name="tgt_time" value="1200" /> </bean>

測試用例經過,至此,支持redis票據存儲的插件開發完畢。而後咱們利用mvn install把該插件安裝到本地倉儲。

q3

下面咱們開始在cas-server-webapp工程中使用該插件。

5.修改cas-server-webapp工程中ticketRegistry.xml文件,替換掉DefaultTicketRegistry,同時註釋掉ticketRegistryCleaner相關全部定義(爲何註釋掉前文有討論)。

<bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.RedisTicketRegistry" > <property name="jedisSentinelPool" ref="jedisSentinelPool" /> <property name="st_time" value="10" /> <property name="tgt_time" value="1200" /> </bean>

6.在POM.xml中添加cas-server-integration-redis模塊:

<dependency> <groupId>org.jasig.cas</groupId> <artifactId>cas-server-integration-redis</artifactId> <version>${project.version}</version> <scope>compile</scope> </dependency>

7.本地啓動redis,從新build工程,而後tomcat7:run運行CAS Server。直接登陸認證中心,觀察redis中數據變化。

q4

咱們看到TGT存到redis中了,作登出操做,會觀察到TGT已消失。從應用系統登陸,會發現ST也在redis中。

q5

 

第七篇:

前面咱們介紹的SSO,不管是CAS仍是咱們自主開發的Nebula,都有一個共同的特色,就是應用系統須要登陸時,都先重定向到認證服務器進行登陸。也就是說系統須要從一個應用先跳到另外一個應用,咱們看阿里的單點登陸就是這麼作的。

但有時候,咱們想進一步增長用戶體驗,並不但願用戶離開原應用頁面,在原有頁面基礎上進行登陸,讓用戶感覺不到認證中心的存在,能不能作到呢?回答是確定的,你們看下新浪的單點登陸方式,就是這麼作的。

在原有應用系統頁面進行登陸認證中心,如不發生跳轉,咱們須要使用Ajax方式。而最經常使用的XMLHttpRequest Ajax方式調用,存在一個跨域問題,即爲了安全,Ajax自己是不容許跨域調用的。這也就是爲何單點登陸常規作法是重定向到認證中心去登陸,而後再重定向回系統應用的緣由。(並且爲了安全,CAS自己也不提倡跨域遠程登陸)

在應用頁面中,如何達到遠程登陸CAS的效果?擺在咱們面前有兩道坎兒須要克服:

首先是遠程獲取lt和execution參數值問題。前面咱們介紹過,CAS登陸的form提交不只有username和password兩個參數,還包括lt和execution,lt防止重複提交,execution保證走的同一個webflow流程。在進行遠程提交時,咱們須要遠程獲得CAS動態產生的這兩個參數,從而保證可以向CAS進行正確form提交。

XMLHttpRequest Ajax不能使用,能夠採用另一種方式,即JSONP。JSONP使用了script標籤能夠跨域訪問其它網站資源的特性,巧妙地返回一段js回調方法代碼,經過執行這個回調方法,達到了傳遞跨域調用數據的目的。

第二個坎兒是如何在本頁面跨域提交form請求。咱們能不能也用JSONP方法呢?很遺憾,不行!JSONP提供的是get方式,而咱們提交的form是post方式。咱們可使用另一種ajax技術來解決,iframe。iframe能夠加載和操做其它域的資源,根據用戶提交的username和password,以及前面獲取的lt和execution,在iframe中提交登陸form參數,完成登陸。

主頁面如何獲取iframe提交返回的信息?能夠修改CAS的登陸流程,讓其在遠程登陸的狀況下,將出錯信息以參數的方式重定向迴應用系統服務端,應用系統再以調用父頁面js函數方法,將出錯信息經過參數傳遞給父頁面。

從上面思路能夠看出,咱們並無讓CAS增長遠程登陸的功能,CAS登陸,仍是須要在CAS所在域下登陸。咱們只是利用iframe方法,讓應用系統達到和遠程登陸同樣的用戶體驗效果。而實現這一效果的關鍵,是應用登陸頁對lt和execution動態參數以及CAS登陸反饋信息的捕獲。
下面咱們就按照上面思路介紹具體開發方法:

1.改造login-webflow.xml,增長支持跨域遠程登陸處理流程分支。

前面咱們已經瞭解,登陸流程的控制是在login-webflow.xml中,咱們對它進行改造。改造原則是不修改原代碼,在原有登陸處理流程的基礎上,增長一種新狀況的處理,即支持跨域遠程登陸處理。

在流程初始化處理完成後,咱們增長一個新的節點mode,它首先來檢查登陸請求中是否包含一個變量mode,而且變量的值爲rlogin。若是沒有,就繼續走原常規流程。若是有,說明是跨域遠程登陸狀況。<on-start> 後加入以下分支流程定義:

<action-state id="mode"> <evaluate expression="modeCheckAction.check(flowRequestContext)"/> <transition on="rlogin" to="serviceAuthorizationCheckR" /> <transition on="normal" to="ticketGrantingTicketCheck" /> </action-state> <action-state id="serviceAuthorizationCheckR"> <evaluate expression="serviceAuthorizationCheck"/> <transition to="generateLoginTicketR"/> </action-state> <action-state id="generateLoginTicketR"> <evaluate expression="generateLoginTicketAction.generate (flowRequestContext)" /> <transition on="generated" to="rLoginTicket" /> </action-state> <view-state id="rLoginTicket" view="rLoginTicket" model="credential"> <binder> <binding property="username" required="true" /> <binding property="password" required="true"/> </binder> <on-entry> <set name="viewScope.commandName" value="'credential'" /> </on-entry> <transition on="submit" bind="true" validate="true" to="realSubmitWithRLogin"> <evaluate expression="authenticationViaRFormAction.doBind (flowRequestContext, flowScope.credential)" /> </transition> </view-state> <action-state id="realSubmitWithRLogin"> <evaluate expression="authenticationViaRFormAction.submit(flowRequestContext, flowScope.credential, messageContext)" /> <transition on="success" to="sendTicketGrantingTicketR" /> </action-state> <action-state id="sendTicketGrantingTicketR"> <evaluate expression="sendTicketGrantingTicketAction" /> <transition on="success" to="rLoginRes" /> </action-state> <end-state id="rLoginRes" view="rLoginRes" />

2.增長rLoginTicket和rLoginRes新視圖

新增流程使用了兩個新view,rLoginTicket返回的是JSONP要求的js調用,將CAS產生的lt和execution數據傳遞給調用方。最後的rLoginRes是將出錯信息重定向迴應用系統。

前面咱們介紹了定義CAS頁面和修改頁面主題的方法,咱們基於前面的工做,在nebula_views.properties中添加(原始是default_views.properties):

rLoginTicket.(class)=org.springframework.web.servlet.view.JstlView rLoginTicket.url=/WEB-INF/view/jsp/nebula/ui/rLoginTicket.jsp rLoginRes.(class)=org.springframework.web.servlet.view.JstlView rLoginRes.url=/WEB-INF/view/jsp/nebula/ui/rLoginRes.jsp

同時在相應目錄下建立這兩個文件,文件內容以下:

rLoginTicket.jsp

<%@ page contentType="text/javascript; charset=UTF-8"%> <%out.print("jsonpcallback({'lt':'");%>${loginTicket}<%out.print ("','execution':'");%>${flowExecutionKey}<%out.print("'})");%>

rLoginRes.jsp

<%@ page contentType="text/html; charset=UTF-8"%> <html> <body> <script type="text/javascript"> location.replace("${service}?ticket=${ticket}&ret=${ret}&msg=${msg}"); </script> </body> </html>

3.定義新action節點

流程中,咱們定義了兩個新action,modeCheckAction和authenticationViaRFormAction,分別處理遠程登陸流程判斷和form提交處理。在cas-servlet.xml中定義:

<bean id="modeCheckAction" class="org.jasig.cas.web.flow.ModeCheckAction" /> <bean id="authenticationViaRFormAction" class="org.jasig.cas.web.flow.AuthenticationViaRFormAction" p:centralAuthenticationService-ref="centralAuthenticationService" p:ticketRegistry-ref="ticketRegistry"/>

按照CAS工程架構,這兩個新增的action定義在cas-server-webapp-support工程中。

ModeCheckAction定義以下:

package org.jasig.cas.web.flow; import javax.servlet.http.HttpServletRequest; import org.jasig.cas.web.support.WebUtils; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; public class ModeCheckAction{ public static final String NORMAL = "normal"; public static final String RLOGIN = "rlogin"; public RLoginCheckAction() { } public Event check(final RequestContext context) { final HttpServletRequest request = WebUtils.getHttpServletRequest(context); //根據mode判斷請求模式,如mode=rlogin,是AJAX登陸模式, //不存在是原模式,認證中心本地登陸 String mode = request.getParameter("mode"); if(mode!=null&&mode.equals("rlogin")){ context.getFlowScope().put("mode", mode); return new Event(this, RLOGIN); } return new Event(this, NORMAL); } }

AuthenticationViaRFormAction參照AuthenticationViaFormAction,對出錯輸出作了處理,核心代碼以下:

public final Event submit(final RequestContext context, final Credential credential, final MessageContext messageContext) throws Exception { // Validate login ticket final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context); final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context); if (!authoritativeLoginTicket.equals(providedLoginTicket)) { logger.warn("Invalid login ticket {}", providedLoginTicket); messageContext.addMessage(new MessageBuilder().code ("error.invalid.loginticket").build()); context.getFlowScope().put("ret", -1); context.getFlowScope().put("msg", "LT過時,請從新登陸!"); } try { final String tgtId = this.centralAuthenticationService.createTicketGrantingTicket(credential); WebUtils.putTicketGrantingTicketInFlowScope(context, tgtId); final Service service = WebUtils.getService(context); final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(tgtId,service); WebUtils.putServiceTicketInRequestScope(context,serviceTicketId); context.getFlowScope().put("ticket", serviceTicketId); return newEvent(SUCCESS); } catch (final AuthenticationException e) { context.getFlowScope().put("ret", -2); context.getFlowScope().put("msg", "用戶名密碼錯誤,請從新登陸!"); return newEvent(SUCCESS); } catch (final Exception e) { context.getFlowScope().put("ret", -3); context.getFlowScope().put("msg", "系統內部錯誤,請稍後登陸!"); return newEvent(SUCCESS); } }

支持跨域遠程登陸的CAS改造完成。應用系統方怎麼調用呢,咱們開發一個例子:

設置CAS認證中心的域名爲www.cas.com,應用系統的域名爲www.ssoclient.com:81

首先咱們按照前面方法把應用系統配置成SSO Client應用,這個前面介紹過,這裏不重複。開發一個應用登陸頁rlogin.html,代碼片斷以下:

咱們定義一個隱藏的iframe:

<iframe style="display:none;width:0;height:0" id="rlogin" name="rlogin"/>

登陸form部分:

<div id="sec-login"> <form id="login-form" name="login-form" action="http://www.cas.com/login" method="post" target="rlogin"> <div><input name="username" id="username" type="text" autocomplete="off" class="login-ipt" placeholder="郵箱/手機號" /></div> <div><input name="password" type="password" id="password" class="login-ipt" placeholder="密碼" /></div> <input type="hidden" name="lt" value="" id="lt" /> <input type="hidden" name="execution" value="" id="execution" /> <input type="hidden" name="_eventId" value="submit" /> <input type="button" value="登陸" class="login-bnt" onclick="javascript:login();" /> </form> </div>

關鍵是login js方法,JSON獲取lt和execution後,提交form到iframe定義以下:

var login = function(){ $.ajax({ url: 'http://www.cas.com/login? mode=rlogin&service=http://www.ssoclient.com:81/ssoresult.do', dataType: "jsonp", jsonpCallback: "jsonpcallback", success: function (data) { $('#lt').val(data.lt); $('#execution').val(data.execution); $('#login-form').submit(); }, error:function(){ alert('網絡訪問錯誤!'); } }); };

還須要定義一個logincallback方法,用於接收登陸後出錯信息:

var logincallback = function(result) { if (result.ret == 0){ location.href="\index.do"; } else { alert(result.msg); $('#login-form')[0].reset(); } };

系統應用定義的service是ssoresult.do,這是cas重定向返回的點(rLoginRes.jsp中定義),也是SSO Client系統應用登陸成功後返回的點。在這裏接收CAS傳來的登陸出錯數據並調用js的方式返回給父頁面。核心代碼以下:

@RequestMapping("/ssoresult.do") public void ssoResult(HttpServletRequest request, HttpServletResponse response) { String ret = request.getParameter("ret"); String msg = request.getParameter("msg"); if(ret==null){ ret = "0"; } String result = "<html><head><script language='javascript'>" + "parent.logincallback({'ret':" + ret + ",'msg':'" + msg + "'});" + "</script></head> </html>"; response.setContentType("text/html;charset=UTF-8"); try{ PrintWriter out = response.getWriter(); out.print(result); out.flush(); out.close(); }catch(Exception e){ } }

OK,運行效果以下:

應用登陸頁面前:

q1

用戶登陸信息輸入錯誤:

q2

登陸信息輸入正確:

q3

 

第八篇:

前面系列文章討論的CAS,確切地說是Web SSO解決方案,應用系統是Web系統,即B/S模式。

在移動互聯網應用大趨勢下,傳統互聯網應用都在向移動化方向延伸。

移動應用不只包括移動Web應用(觸屏版、H5應用),更多的是Native APP原生應用(安卓、蘋果等APP),即軟件架構是C/S模式。

對於CAS認證中心管控的Web應用羣,如何將這些原生APP應用歸入其中?

因爲沒有Web應用瀏覽器自然所具備的處理Cookie、處理HTTP重定向能力,原生APP的登陸會話管理通常採用自主開發。

在服務器端建立並保持會話,將會話句柄返給APP客戶端持有,後續須要登陸後訪問的API均需帶上這個會話句柄做爲請求的一個參數。這個會話句柄和咱們Java Web應用的jsessionid很相似。

上述是Native APP登陸管理的實現方式,那如何接入CAS認證中心呢?能夠有兩種方式:一種是APP直接訪問CAS認證中心,先獲得TGT,再獲得ST。APP拿到ST後,就能夠訪問配置成CAS Client的移動服務端應用,服務端和認證中心驗證過ST後,便可按上述方式創建起本地會話。

另外一種方式可採用服務端代理模式,即APP先向移動服務端應用提交登陸請求,服務端再向CAS認證中心登陸認證。這種方式將CAS認證中心的非瀏覽器登陸接口只暴露給移動服務端應用,起到很好的安全防禦功能。本文將採用第二種方式給你們示範。

CAS提供了一個支持RESTful風格API的插件,4.1.1新版是cas-server-support-rest,老版是cas-server-integration-restlet 能夠得到TGT和ST。

這裏咱們使用另一種方式,不用CAS插件,思路和《支持Web應用跨域登陸CAS》文章介紹相似,經過修改login-webflow流程返回JSON格式View。因爲是服務端代理模式,沒必要返回ST,認證成功便可創建本地會話了。

下面,咱們就一步步加以實現:

1.改造login-webflow.xml,增長Native APP登陸處理流程分支(在基於前面文章增長rlogin流程基礎上修改)

在流程初始化處理完成後,增長一新節點mode,它首先來檢查登陸請求中是否包含一個變量mode,而且mode的值爲app。若是沒有,就繼續走原常規流程。若是有,說明是Native APP登陸處理狀況。<on-start> 後加入以下分支流程定義:

<action-state id="mode"> <evaluate expression="modeCheckAction.check(flowRequestContext)"/> <transition on="rlogin" to="serviceAuthorizationCheckR" /> <transition on="app" to="serviceAuthorizationCheckR" /> <transition on="normal" to="ticketGrantingTicketCheck" /> </action-state>

產生lt後,咱們要作個判斷,看是app狀況仍是rlogin狀況,app走app處理流程。

<action-state id="generateLoginTicketR"> <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" /> <transition on="generated" to="modeCheckForLt" /> </action-state> <decision-state id="modeCheckForLt"> <if test="flowScope.mode != null && flowScope.mode == 'rlogin'" then="rLoginTicket" else="appLoginTicket" /> </decision-state>

增長appLoginTicket,注意它的輸出視圖是appLoginTicket。這和rlogin狀況的輸出視圖不一樣。

<view-state id="appLoginTicket" view="appLoginTicket" model="credential"> <binder> <binding property="username" required="true" /> <binding property="password" required="true"/> </binder> <on-entry> <set name="viewScope.commandName" value="'credential'" /> </on-entry> <transition on="submit" bind="true" validate="true" to="realSubmitWithRLogin"> <evaluate expression="authenticationViaRFormAction.doBind(flowRequestContext, flowScope.credential)" /> </transition> </view-state>

登陸認證信息提交後,須要根據mode返回不一樣的VIEW,app模式返回appRes,rlogin模式返回rLoginRes,故修改節點以下:

<action-state id="sendTicketGrantingTicketR"> <evaluate expression="sendTicketGrantingTicketAction" /> <transition on="success" to="modeCheck" /> </action-state> <decision-state id="modeCheck"> <if test="flowScope.mode != null && flowScope.mode == 'rlogin'" then="rLoginRes" else="appRes" /> </decision-state> <end-state id="rLoginRes" view="rLoginRes" /> <end-state id="appRes" view="appRes" />

2.增長appLoginTicket和appRes新視圖

在nebula_views.properties中添加(原始是default_views.properties):

appLoginTicket.(class)=org.springframework.web.servlet.view.JstlView appLoginTicket.url=/WEB-INF/view/jsp/nebula/ui/appLoginTicket.jsp appRes.(class)=org.springframework.web.servlet.view.JstlView appRes.url=/WEB-INF/view/jsp/nebula/ui/appRes.jsp

同時在相應目錄下建立這兩個文件,文件內容以下:

appLoginTicket.jsp

<%@ page contentType="text/html; charset=UTF-8"%> <%out.print("{\"lt\":\"");%>${loginTicket}<%out.print("\",\"execution\":\"");%>${flowExecutionKey}<%out.print("\"}");%>

appLoginRes.jsp

<%@ page contentType="text/html; charset=UTF-8"%> <%out.print("{\"ret\":\"");%>${ret}<%out.print("\",\"msg\":\"");%>${msg}<%out.print("\"}");%>

3.修改modeCheckAction內容,增長處理app狀況,核心代碼以下:

public class ModeCheckAction{ public static final String NORMAL = "normal"; public static final String APP = "app"; public static final String RLOGIN = "rlogin"; public ModeCheckAction() { } public Event check(final RequestContext context) { final HttpServletRequest request = WebUtils.getHttpServletRequest(context); //根據mode判斷請求模式,如mode=rlogin,是AJAX遠程登陸模式, //app是app登陸模式,不存在是原模式,認證中心本地登陸 String mode = request.getParameter("mode"); if(mode!=null&&mode.equals("rlogin")){ context.getFlowScope().put("mode", mode); return new Event(this, RLOGIN); } if(mode!=null&&mode.equals("app")){ context.getFlowScope().put("mode", mode); return new Event(this, APP); } return new Event(this, NORMAL); } }

至此,CAS認證中心改造完成!

4.開發支持APP登陸的移動服務端接口。接收APP登陸請求,採用HttpClient轉發至CAS認證中心登陸,返回json數據解析並最終返回給客戶端。本地會話採用redis維護,登陸成功,返回access_token。

接口定義:url: /login.json
入參: username string
password string
出參: ret string
msg string
access_token string

核心代碼以下:

@RequestMapping("/login.json") public @ResponseBody ResultBean login(HttpServletRequest request, HttpServletResponse response) { ResultBean resultBean = new ResultBean(); String username = request.getParameter("username"); String password = request.getParameter("password"); HttpClient httpClient = new DefaultHttpClient(); String url = SSO_SERVER_URL + "?mode=app&service=" + SSO_CLIENT_SERVICE; HttpGet httpGet = new HttpGet(url); try{ HttpResponse httpClientResponse = httpClient.execute(httpGet); int statusCode = httpClientResponse.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_OK){ String result = EntityUtils.toString(httpClientResponse.getEntity(), "utf-8").replace('\r', ' ').replace('\n', ' ').trim(); //解析json數據 ObjectMapper objectMapper = new ObjectMapper(); LtBean ltBean = objectMapper.readValue(result, LtBean.class); List<NameValuePair> formparams = new ArrayList<NameValuePair>(); formparams.add(new BasicNameValuePair("username", username)); formparams.add(new BasicNameValuePair("password", password)); formparams.add(new BasicNameValuePair("lt", ltBean.getLt())); formparams.add(new BasicNameValuePair("execution", ltBean.getExecution())); formparams.add(new BasicNameValuePair("_eventId", "submit")); UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, "UTF-8"); HttpPost httpPost = new HttpPost(SSO_SERVER_URL); httpPost.setEntity(entity); httpClientResponse = httpClient.execute(httpPost); statusCode = httpClientResponse.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_OK){ result = EntityUtils.toString(httpClientResponse.getEntity(), "utf-8") .replace('\r', ' ').replace('\n', ' ').trim(); objectMapper = new ObjectMapper(); resultBean = objectMapper.readValue(result, ResultBean.class); if(resultBean.getRet().equals("")){ String access_token = UUID.randomUUID().toString(); //會話句柄 TokenUtil.setAccess_token(access_token, username); //放入redis resultBean.setRet("0"); resultBean.setMsg("登陸成功"); resultBean.setAccess_token(access_token); } } } }catch(Exception e){ e.printStackTrace(); resultBean.setRet("-2"); resultBean.setMsg("系統服務錯誤,請稍後再試!"); return resultBean; }finally{ httpClient.getConnectionManager().shutdown(); } return resultBean; }
  1. 開發app客戶端登陸

APP開發不是本文重點,這裏略。

 

 

 

 

 

 

 

 



做者: 手插口袋_ 
連接:http://www.imooc.com/article/3558
來源:慕課網

相關文章
相關標籤/搜索