雙網隔離環境下CAS單點登錄的解決方案

前言


  在單位內架設的Web系統,如果使用CAS作爲單點登錄方案,往往會遇到從單位的不同網絡(例如雙網隔離下的外網和內網)訪問時,系統無法正常登錄使用的問題。基於本人實踐,本文介紹一些解決方案。
  本文爲原創,首發簡書並移植到CSDN,未經許可不得轉載。

技術背景


  對CAS很熟悉的朋友可以跳過本章。
  用Java開發一個支持單點登錄SSO的Web應用,一般都需要部署兩個服務:CAS服務和Web應用服務。CAS的相關資料網上很多,例如:https://www.jianshu.com/p/d443cfc10646。這裏只簡單敘述一下其工作方式,爲解決方案做鋪墊。

  1. 開發和部署CAS服務和Web應用服務。假設:
CAS服務地址:http://192.168.1.2:8080/sso
Web應用地址:http://192.168.1.3:8080/web
  1. 在Web應用服務的配置文件中配置CAS服務器的地址。
  2. 用戶通過瀏覽器打開Web應用地址http://192.168.1.3:8080時,Web應用發現用戶未登錄過(沒有session和ticket),於是從配置文件中讀取到CAS服務地址,然後把用戶當前請求地址附加在這個地址後面,告訴用戶的瀏覽器去跳轉到這個地址,形如 http://192.168.1.2:8080/sso/login?service=http://192.168.1.3/web。
  3. 瀏覽器被重定向到上述CAS服務地址後,出現CAS的登錄頁面,用戶輸入賬號登錄成功後,CAS服務端從瀏覽器請求地址中提取出用戶原來要訪問的http://192.168.1.3/web這個地址,附加上ticket後讓瀏覽器重定向到該地址。
  4. 瀏覽器被重定向到Web應用地址:http://192.168.1.3/web,Web應用服務端收到用戶請求,並從請求中獲取到sessionid、ticket等信息,然後在後臺去訪問http://192.168.1.2:8080/sso,驗證ticket通過後即響應瀏覽器請求,通知瀏覽器重定向到Web應用首頁。整個登錄過程結束。

問題描述


  當系統只在一個獨立的網絡中運行時,CAS登錄過程沒有任何問題。但如果企事業單位採用了網絡隔離模式,典型地,把網絡分成內網和外網兩部分,服務器部署在內網,通過端口映射的方式把服務端口向外網開放,外網用戶只能通過外網IP訪問單位網絡。此時外網用戶將無法完成單點登錄過程。原因如下:
  假設單位的外網地址是10.11.2.3,並且已經成功地做了如下端口映射:

CAS服務外網映射:10.11.2.3:81 -> 192.168.1.2:8080
Web服務外網映射:10.11.2.3:80 -> 192.168.1.3:8080

  當一個外網用戶輸入 http://10.11.2.3:80/web時,端口映射能夠成功將請求轉給內網的http://192.168.1.3:8080/web,而Web應用的配置文件中登記的CAS服務地址是內網地址http://192.168.1.2:8080/sso,因此Web應用會通知瀏覽器重定向該地址。然而瀏覽器是處在外網網絡中,當然無法連上該內網地址,於是瀏覽器報錯,登錄過程中斷。作爲用戶,會看到瀏覽器地址欄顯示一個內網URL。

問題分析


  出現登錄問題的本質原因是:
  CAS登錄過程依賴對CAS服務地址的重定向,而這個地址是在部署系統時固定配置好的,唯一的。如果內外網不能同時訪問該地址,則必定無法登錄成功。 上例中,外網用戶無法訪問http://192.168.1.2:8080/sso這個地址。
  要解決該問題,思路就應該是:用什麼方法能夠使得***在重定向跳轉過程中,始終都使用當前用戶可以訪問***的地址。

解決方案


  長話短說,這裏直接給出幾個可能的解決方案。

解決方案1:通過統一域名和DNS解決


  這是最簡單優雅的解決方案。
  簡單說來,上面的例子中因爲使用了IP地址作爲服務地址,從而造成內外網無法同時訪問的結果,那麼把IP地址改成域名,通過內外網不同的DNS域名服務器設置,讓無論內網還是外網都能通過域名訪問到真正的服務地址,此問題就迎刃而解。具體做法如下:

  1. 給SSO服務和Web服務申請域名:
SSO服務域名:sso.mycompany.net
Web應用域名:web.mycompany.net
  1. 在Web應用服務的配置文件中,配置CAS服務的地址爲:http://sso.mycompany.net:8080/sso。
  2. DNS配置:
  • 在內網的DNS服務器上,把這兩個域名分別映射到192.168.1.2和192.168.1.3;
  • 在外網申請兩個IP地址IP1IP2(滿足特定前提下可以只用一個,參見後文),並分別在外網的DNS服務器分別綁定兩個域名到這兩個IP地址;
  1. 端口映射:分別把IP1IP2的8080端口映射到內網的192.168.1.2和192.168.1.3的8080端口;
  2. 告訴內外網所有用戶,應用的訪問地址是http://web.mycompany.net:8080/web

  上述工作完成後,無論內網用戶還是外網用戶,當訪問http://web.mycompany.net:8080/web時,瀏覽器都會被重定向到http://sso.mycompany.net:8080/sso來進行單點登錄,而這個地址都會根據所在網絡不同,解析並導向到最終的內網服務器上,從而實現成功登錄。
  該方案的一個小小缺陷是因爲地址必須相同,所以內外網必須使用相同的服務端口號,即如果內網服務使用了8080,外網也必須使用8080。上述的例子中,因爲cas和web都使用了8080端口,因此外網不得不用兩個外網IP地址來分別映射。如果cas和web的端口不同,則可以只使用一個外網IP地址的兩個端口來分別映射,此時外網DNS把兩個域名映射到同一個IP地址上即可。
  遺憾的是,現實中很多單位的IT部門要麼難以搞定域名申請和配置,要麼因爲懶而直接甩鍋給業務系統開發商,要求從應用層去解決,總之因爲各種客觀限制,這個最優雅最簡潔的方案卻是最難推動的。

解決方案2:通過應用程序端解決


  首先說明,這是***絕對不建議***採用的方案。這裏僅僅作爲一種可能性簡要介紹一下思路。
  回顧上述的失敗流程,因爲Web應用對外網用戶給出了錯誤的內網CAS服務地址,從而造成外網用戶無法登錄,那麼理論上如果Web應用能夠識別出外網用戶,並讓其重定向到正確的外網CAS地址,則有可能解決此問題。具體來說,需要Web應用通過用戶的HTTP請求中的信息判斷用戶來自哪裏(例如是否來自外網與內網之間的網關),區分內網和外網給出不同的重定向地址,並解決後續從CAS跳轉回Web應用地址、網頁中的超鏈接地址動態切換等一系列問題。
  此方法過於麻煩,配置複雜,需要對系統源碼動手,如果CAS後面對接了多個系統(之所以要上SSO,肯定是應用系統數量比較多對不對?),這工作量……更別說如果這些系統的源碼不在你手上或根本就是別的公司開發的,涉及到複雜的協調問題,那就更難進行了。故該方案在此不再展開,後面有參考鏈接,可直接看。本人之所以提這個方案,是因爲公司裏曾經有程序猿試圖採用這種方式解決,結果遇到剛纔說的工作量、第三方協調等問題,搞不下去了……
  下面幾篇文章都是該方案的思路,供參考:

解決方案3(推薦!):通過Apache/Nginx反向代理


  各位看官,你們是否和我一樣,都很累了。。。打起精神,鋪墊了這麼多,現在終於到本人想重點講解的方案了,敲黑板!!!
  本方案的核心思想是,在內網架設一臺Apache/Nginx服務器,通過端口映射向外網用戶提供系統訪問入口,利用Apache的反向代理能力,把內網服務器生成的重定向內網地址,轉換成外網地址,再傳給外網用戶瀏覽器。整個方案不需要對CAS和應用做任何配置或源碼上的修改! 簡單的示意圖如下:反向代理方案示意圖
  上圖中,內網用戶的訪問方式完全不變,CAS服務和Web應用也不做任何調整,對它們來說,完全不知道自己會被外網用戶訪問。而外網用戶則需要通過被端口映射到外網的Apache服務器間接訪問CAS和Web應用。
  :Nginx是比Apache功能更專一的反向代理軟件,不過因爲本人工作史的緣故,本方案是基於Apache的(我纔不會說我其實沒怎麼玩過Nginx),好在都差不多,用Nginx的童鞋就麻煩參照本文思想自行實現吧。
  Apache的安裝部署過程略去,直接進入配置環節。

配置1:定義VirtualHost,開啓反向代理

  在Apache的配置文件中,定義VirtualHost,監聽8080端口,並指定ServerName 10.11.2.3,意思就是該VirtualHost中的配置僅當接收到的請求URL中的主機名是10.11.2.3時才起作用。其他幾個配置就不一一說明了。

# 外網地址
<VirtualHost *:8080>
    ErrorLog "logs/error.8080.out.log"    
    ServerName 10.11.2.3

    # 關閉正向代理
    ProxyRequests Off
    # 反向代理時不保留原始Request中的HOST(即代理服務器自身Host)
    ProxyPreserveHost Off

    <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>
</VirtualHost>

配置2:反向代理

  在VirtualHost中,通過ProxyPassProxyPassReverse兩個指令,實現URL反向代理:

# SSO反向代理
ProxyPass /sso http://192.168.1.2:8080/sso
ProxyPassReverse /sso http://192.168.1.2:8080/sso

# Web應用反向代理
ProxyPass /web http://192.168.1.3:8080/web
ProxyPassReverse /web http://192.168.1.3:8080/web

  ProxyPass指示Apache接收到某個路徑請求後,需要把請求轉發給哪一個地址。例如:

ProxyPass /web http://192.168.1.3:8080/web

  這句話的意思是,當Apache收到的請求路徑是/web開頭時,需要把請求原封不動地轉發給http://192.168.1.3:8080/web(/sso後面URL的其他部分也會原樣附加上去),然後把http://192.168.1.3:8080/web返回的Response,再原封不動地返回給正在訪問Apache的瀏覽器。這個過程對瀏覽器是透明的,實現了用戶輸入外網地址就能看見內網系統頁面的效果。
  不過,ProxyPass無法處理重定向。當http://192.168.1.3:8080/web的響應報文的HTTP頭中帶有Location重定向標識時,瀏覽器會不折不扣按此標識跳轉。在本文開始的例子中就說過,Web應用返回給訪問者的重定向地址是內網地址。在通過Apache返回時,必須把這個內網地址改成外網地址,這就需要用到ProxyPassReverse指令。例如:

ProxyPassReverse /sso http://192.168.1.2:8080/sso

  這句話的意思是,如果Apache接收到的服務器響應中重定向標識Location是http://192.168.1.2:8080/sso,則將其替換爲當前用戶訪問地址後加上/sso。這樣,web應用本來是要求瀏覽器重定向到http://192.168.1.2:8080/sso/login?xxxxx,但這個地址在經過Apache返回時被Apache篡改成了http://10.11.2.3:8080/sso/login?xxxxx,瀏覽器並不知道這一切幕後工作,只是單純按照接收到的信息進行重定向,去請求http://10.11.2.3:8080/sso/login?xxxxx。該請求再次經過Apache並被ProxyPass指令轉換成真正的內網CAS服務器地址,從而能夠正確到達CAS服務器。後面過程就不詳述了。
  總之,通過這兩個指令就基本實現了在不對應用系統做任何配置修改的情況下的外網訪問。
  值得一提的是,上面的例子中CAS服務和Web應用的URL都是帶有子路徑/sso和/web的。這裏有兩個最佳實踐:

  • 在做反向代理時,最好讓系統運行在某個子路徑下,而不要運行在根路徑下;
  • Apache/Nginx上的反向代理配置,最好配置與後方系統相同的子路徑做爲映射路徑;

  按上述最佳實踐來做,能夠省掉很多麻煩事。
  如果系統運行在URL根目錄下,即http://192.168.1.3:8080,而不是http://192.168.1.3:8080/web,在做反向代理配置時就需要把路徑映射到根路徑,如果有多個後方系統都使用根目錄,反向代理將無法區分。有人一定會說使用ServerName、多IP地址、多端口等方式可以實現根據不同請求來源而區分處理,但現實是我遇到的大多數單位提供的對外訪問接口,既沒有域名,也沒有多個IP,甚至也不會給你開多個端口,這種情況下,只能靠子路徑來讓反向代理知道應該到哪裏去。那位說了,可是已經部署好的系統就是在根目錄的,我怎麼辦?好吧,你施展人格魅力的時候到了,想辦法去說服他們改變吧!如果魅力不夠,也許你可以在內網再架設一個反向代理做二次映射來專門解決這個特定應用的路徑問題,我也沒試過行不行,祝你成功,並且成功後告訴我一聲:-)
  如果映射的子路徑和系統真實的子路徑不同行不行呢?比如說CAS服務地址是http://192.168.1.2:8080/sso,我在Apache上配置用/cas路徑來映射,即對外的地址爲http://10.11.2.3/cas,有什麼問題?答案是,不一定,但有可能會遇到問題。這個問題通常出在一些系統在開發或部署時,有可能把這個子路徑作爲系統變量的一部分,在頁面超鏈接等地方直接使用/sso,因爲Apache無法識別和處理這個地址,所以頁面也就無法正常顯示。總而言之,不要自找麻煩,簡單點,生活儘量簡單點。

配置3:頁面內容替換

  完成上述反向代理配置後,其實大多數系統就已經能夠正常使用了。不過偶爾還會遇到一些系統在頁面上直接超鏈接其他系統的,比如一個門戶系統在頁面上超鏈接其他業務系統的地址是再正常不過的了。超鏈接只能寫一個URL,要麼是內網的,要麼是外網的,怎麼同時滿足內網用戶和外網用戶呢?前面的反向代理配置,不會處理頁面中的URL,此時就需要用到另一個大殺器:mod-substitute
  Apache的mod-substitute模塊可以通過正則表達式,實時替換網頁中的內容。具體語法參考請移步原廠:mod-substitute。下面是配置實例:

##去掉GIZP標識,否則無法替換頁面內容
LoadModule headers_module modules/mod_headers.so
RequestHeader unset Accept-Encoding    
## 加載替換模塊,過濾指定類型頁面
LoadModule substitute_module modules/mod_substitute.so
AddOutputFilterByType SUBSTITUTE text/html
AddOutputFilterByType SUBSTITUTE text/plain
AddOutputFilterByType SUBSTITUTE application/json
AddOutputFilterByType SUBSTITUTE application/x-javascript
AddOutputFilterByType SUBSTITUTE application/javascript
AddOutputFilterByType SUBSTITUTE text/javascript
## 把服務器響應中的內網IP地址改成外網地址
Substitute "s|192.168.1.3:8080/web|10.11.2.3/web|n"
Substitute "s|192.168.1.2:8080/sso|10.11.2.3/sso|n"

  上面的配置中,首先重置Accept-Encoding,這實際上就會去掉瀏覽器發出的請求頭中支持gzip的申明,於是服務器就不會返回經過壓縮的頁面內容,這樣才能夠進行文本替換。AddOutputFilterByType指明要進行替換的Content Type種類,顯然這裏應該根據系統的實際情況,列出所有有可能出現系統URL的地方,越少越好,像圖片之類的當然就不需要了,因爲這玩意肯定影響性能。Substitute進行實際的內容替換,注意前面的地址是被替換內容,後面的是替換內容,別寫反了啊!
  通過上面的配置後,從外網嘗試打開原來有錯誤鏈接的頁面,看看鏈接地址是不是被改變了?當然你可以拿頁面中的任何內容測試一下,比如把版權信息改成“聖誕節快樂”,把你情敵的姓名改成公司領導姓名,看看用戶反應如何?我就是說說而已,被投訴了可別找我啊!現在知道爲什麼我說這個東西是大殺器了吧,隨意篡改網頁內容太可怕了有木有!網絡世界真的不能沒有SSL、HTTPS!如果系統本身已經使用HTTPS方式部署,這方法可能會失效,參見後面的專項討論。
  最後強調一下,這個配置會降低性能,對HTTPS可能會無效,僅當網頁內容中出現了系統URL絕對路徑且需要動態替換時才考慮使用!

一個完整的配置文件

  最後放出一個完整的配置文件。給這個文件隨便起個名字,只要擴展名是.conf,單獨放在Apache服務器的conf.d或類似目錄下(好像不同版本不一樣?),重啓Apache就可以生效了。

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule substitute_module modules/mod_substitute.so
LoadModule headers_module modules/mod_headers.so

NameVirtualHost *:8080

# 外網地址
<VirtualHost *:8080>
    ErrorLog "logs/error.8080.out.log"    
    ServerName 10.11.2.3

    # 關閉正向代理
    ProxyRequests Off
    # 反向代理時不保留原始Request中的HOST(即代理服務器自身Host)
    ProxyPreserveHost Off

    <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>
	
	##去掉GIZP標識,否則無法替換頁面內容
	RequestHeader unset Accept-Encoding    
	## 過濾指定類型頁面
	AddOutputFilterByType SUBSTITUTE text/html
	AddOutputFilterByType SUBSTITUTE text/plain
	AddOutputFilterByType SUBSTITUTE application/json
	AddOutputFilterByType SUBSTITUTE application/x-javascript
	AddOutputFilterByType SUBSTITUTE application/javascript
	AddOutputFilterByType SUBSTITUTE text/javascript
	## 把服務器響應中的內網IP地址改成外網地址
	Substitute "s|192.168.1.3:8080/web|10.11.2.3/web|n"
	Substitute "s|192.168.1.2:8080/sso|10.11.2.3/sso|n"

	# SSO反向代理
	ProxyPass /sso http://192.168.1.2:8080/sso
	ProxyPassReverse /sso http://192.168.1.2:8080/sso

	# Web應用反向代理
	ProxyPass /web http://192.168.1.3:8080/web
	ProxyPassReverse /web http://192.168.1.3:8080/web
	
</VirtualHost>

調試技巧

  在調試Apache/Nginx的反向代理配置時,需要熟練掌握一些方法:

  • 啓用瀏覽器的調試功能(按下F12鍵),學會查看Request、Response中的信息,主要就是Header的內容。尤其是在重定向時,注意查看Response中的Location的內容是什麼。HTTP調試信息
  • 在Chrome的調試窗口中,要勾選Preserve Log選項(如下圖),否則在頁面跳轉等場合會漏掉跳轉後的請求記錄。勾選Preserve Log

關於HTTPS

  前面的討論,都是建立在內網系統本身是HTTP服務,而不是HTTPS服務的前提上進行的。如果客戶要求通過HTTPS訪問系統,需要如何做呢?這裏有兩種方式實現:

  1. 應用服務器本身就以HTTPS方式部署
  2. 應用服務器以HTTP方式部署,通過Apache/Nginx轉換成HTTPS給用戶訪問;

  第一種方式是網上講解配置SSL的文章採用最多的方式。根據HTTPS的原理特點推測,Apache/Nginx應該無法同時滿足即解密處理HTTP數據,又讓客戶端一無所知(更換證書是屬於客戶端可感知的),因爲這種方式就是典型的中間人攻擊。從網上資料來看,Apache是可以進行SSL代理的,搜了一篇 帖子供參考。我想原理上應該就是Apache先用服務器公鑰解密,然後再用Apache上配置的證書重新做加密,與瀏覽器交互。瀏覽器看到的證書是Apache提供的。
  第二種方式是我重點推薦方式。當需要提供HTTPS服務時,只在反向代理Apache/Nginx層進行設置,而源應用總是使用HTTP。這樣做的好處是當業務系統很多且都可以使用子路徑部署方式時,只需要配置一次證書(因爲主URL相同,只有子路徑不同),能夠減輕配置工作量。當應用服務器需要集羣、多個系統多個域名多個入口等情況下,即使需要配置多個證書,能夠只在反向代理服務器上一次性幹完所有工作,不用慢吞吞地重啓Tomcat等應用服務器,畢竟Apache/Nginx都可以秒起甚至熱加載,這是何等幸福!這算是一個最佳實踐吧,以後有時間我可以再寫一篇詳細文章介紹。
  回到本文主題,前面介紹的方案在這兩種場景下是否可以沿用呢?第一種場景我沒有試過,估計可行,如果有人試過請留言說一下結果。第二種場景則是親測通過的,親們可以放心使用。

結束語


  這篇文章所記述的方案,來自於一年多前的項目實際經驗。最近又遇到有同事在諮詢相同的問題,而網上沒有看到特別清晰實用的方案,於是利用閒暇寫就此文。因爲隔的時間比較久,當時一些細節已經記不清了,如果有什麼錯漏,請各位看官給指出啊!
  最後,這篇文章花費了我一天時間,如果它幫到了你,請點個贊哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章