先後端分離下的CAS跨域流程分析

寫在最前

先後端分離其實有兩類:

  1. 開發階段使用dev-server,生產階段是打包成靜態文件整個放入後端項目中。
  2. 開發階段使用dev-server,生產階段是打包成靜態文件放入單獨的靜態資源服務器中,如nginx。

這兩種方案最大的區別就是生產階段。因爲第一種方案前端和後端本質在同一個服務中的,因此壓根就沒有跨域,配置cas的坑比較少。而第二種方案咱們通常使用nginx反向代理完成跨域,配置cas的坑會不少。爲了後面分析方便,咱們分別稱上述兩種方案爲『先後端分離A』和『先後端分離B』html

請求也分爲兩類:

1.HTTP請求:像瀏覽器地址欄發起的請求、瀏覽器自發的訪問某個網址、Postman測試接口,這些行爲其實都是發起的HTTP請求,不會有跨域問題。前端

2.AJAX(XMLHttpRequest)請求:這是瀏覽器內部的XMLHttpRequest對象發起的請求,瀏覽器會禁止其發起跨域的請求,主要是爲了防止跨站腳本僞造的攻擊(CSRF)。nginx

難點分析

先後端分離、跨域、CAS這三項技術單獨使用起來,甚至拿其中兩個出來一塊兒使用,難度都不大,下面來列舉一下:ajax

  1. 先後端分離(AB)+跨域
  2. 先後端分離A+CAS(由於A方案根本就沒有跨域這一說)
  3. 先後端分離B+跨域+CAS

先後端分離(AB)+跨域

這個最簡單,只有跨域,沒有CAS,常見的CORS、反向代理、JSONP均可以解決後端

先後端分離A+CAS

坑:CAS認證過時,莫名出現跨域錯誤的問題

可能有人會問,剛纔不是說方案A壓根就沒有跨域問題嗎?其實,這個跨域錯誤不怪咱們的後端,而是怪CAS那邊的後端,待我詳細說來。api

正常狀況下,CAS認證成功後,瀏覽器會設置好一個來自CAS的Cookie以維持與CAS的Session。以後每次請求,不管是ajax請求仍是http請求,都會帶上這個cookie。而咱們本身的後端服務器也會有一個CAS Authorization的過濾器,把沒有CAS認證過的請求重定向到CAS的login頁面。由於本次請求咱們帶了cas的cookie,因此請求順利經過filter來到controller層,進而返回數據。跨域

可是考慮這樣一個狀況,今天你打開你的瀏覽器,訪問一個大家新作的cms系統的網址,而後跳到cas login頁面,正常登錄,正常使用。而後來到次日早上,由於昨天的頁面你沒關,你直接點了一個查詢按鈕,結果報錯了。你打開瀏覽器控制檯,居然發現報了一個跨域的錯誤。這裏有兩處困惑:瀏覽器

  1. 爲何他喵的會跨域呢?咱們先後端命名部署在一臺服務器上,是同域的啊。
  2. 爲何cas認證失效後,沒有自動跳到cas登陸頁呢?但是我以前直接在瀏覽器輸入cms系統的地址時,由於沒認證過,瀏覽器是能直接跳到cas登陸頁的,爲何此次不行呢?
  3. ajax到底怎麼處理302的

爲何會跨域:
想一想一下這樣的一個流程:次日早上你來,點擊一個查詢按鈕,發起了ajax請求,請求中帶上了一個已經失效的cookie,而後請求被後端cas filter攔截,發現已失效,讓後302跳轉到cas login界面。在這個過程當中,你以前發起的ajax請求其實被redirect到了cas的login.html頁面(這只是表象,本質後面會提到)。你至關於發起ajax請求去請求一個html文件下來,然而cas的服務器並無配置跨域,爲了安全考慮也不能配置跨域,因此你的ajax請求還沒來得及請求下來數據,你就被瀏覽器認爲是跨域了,由於你的確在請求cas服務器的一個靜態資源。安全

退一萬步說,就算cas服務器配置了跨域,雖然你點擊查詢按鈕的行爲不會報跨域錯誤了,但你依然不能自動跳轉到cas login頁面,由於這個login.html直接當作你ajax的success中的回調參數回來了,瀏覽器是不會幫你跳轉的。服務器

爲何不能跳轉:
首先,你打開瀏覽器輸入cms系統的地址去訪問的時候,發起的是HTTP請求,是不存在跨域問題的。所以你的HTTP請求被後端的filter給redirect到了cas的login.html,這個流程是沒問題的。而你點擊查詢按鈕,發起的是ajax請求,是無法跳轉的(具體緣由見下方文字)

ajax在302中的行爲本質
當你點擊查詢按鈕,發起的是ajax請求,請求被後端filter攔截,並告知你302跳轉到login頁,此時瀏覽器首先會感知到此次ajax請求的302狀態,並替ajax去訪問要跳轉到的地址,而後將訪問的結果(其實就是整個login.html頁面)返回到你的ajax的success回調函數中,所以這個回調函數的參數其實就是整個login.html的頁面。而且,直到瀏覽器把html放到ajax的success回調函數後,ajax纔會真正的回調,以前的302狀態ajax是感知不到的,固然也獲取不到,因此想經過ajax判斷status是不是302,進而手動location.href到login頁的方案是不行的。

其實,這麼看起來就像是你的ajax直接請求到了login.html頁面。
另外,在實際cas跳轉的過程當中,在ajax的success回調以前,你的ajax操做就被瀏覽器認爲是跨域了,因此你壓根就沒機會回調success,也所以獲取不到status狀態或者那個沒卵用的login.html。

好了,疑惑解決完了,該說說解決方案了:

咱們要實現的就是:在cookie失效時,點擊查詢按鈕後,能自動跳轉到cas登陸頁。
方案不少,但都靠一下兩點:
用HTTP請求替代Ajax請求去跳轉到登陸頁
用200代替302告知ajax當前請求的狀態

舉幾個例子:

一、錯誤方案:設法攔截ajax的response,而後判斷response的status是不是302,若是是302就手動location.href跳到cas登陸頁,可是這樣是不行的,由於咱們根本獲取不到這個302狀態。
二、必需要後端配合,後端須要額外加1個filter和1個controller, 起個名字吧,就叫ValidateFilter和ValidateController吧。

ValidateFilter只過濾那些須要被cas攔截的請求,在doFilter裏面判斷HttpServletRequest的狀態,看看這個request裏能不能獲取到當前用戶名,若是能獲取到,表明認證沒問題,讓這個請求繼續往下走chain.doFilter,若是不能獲取到,表明認證失效了(由於filter不能直接返回,因此咱們須要一個ValidateController),咱們request.dispatch這個請求到ValidateController的redirect方法中(本身寫的),讓這個redirect方法返回一個result,result中設置一個標誌,好比給code:xxx。

而後前端設法在ajax的response以前獲取response的result,看看result的code是否爲xxx,若是是,那就location.href跳轉到cas登陸頁便可,其中service參數寫cas登錄以後要回調的後端接口,而後讓後端去跳轉到前端頁面。

爲何不能直接service寫前端?
由於咱們不只要跟cas服務器維持session,還要跟咱們本身的後端維持session,若是不回調後端,後端就不會感知到咱們的登陸狀態了。

好比:

//前端:
if(result.code === xxx) {
    location.href = "http://cas.server.com/login?service=http://後端服務器地址/redirect/to/frontend?currentPath=當前頁面路徑"
    //currentPath是爲了login以後再調回當前頁面
}
//後端 filter 僞代碼:
void doFilter(request, response, chain) {

    if(request中有用戶名) {
      chain.doFilter()
    } else if(request.uri == '/redirect/to/caslogin') {
      chain.doFilter()
    } else {
        request.dispatch("/redirect/to/caslogin")
    }
}
//後端 controller 僞代碼
// 用來接受filter過來的那些認證失效的請求
@path("/redirect/to/caslogin")
String redirectToCasLogin(request, response) {
    return {
        "code": xxx
    }
}
// 用來在login以後回調用
@path("/redirect/to/frontend")
String redirectToFrontend(request, response) {
  String path = request中的currentPath參數
  request.sendRedirect(path)
}

// 另外,這個controller必定不要被validateFilter過濾,由於若是這個controller也要被過濾,那就陷入cas驗證的死循環了。

3.和2相似,可是location.href中直接寫

location.href = "http://後端服務器地址/redirect/to/caslogin?currentPath=當前頁面路徑"

此時咱們直接請求後端接口/redirect/to/caslogin,他首先被validateFilter攔截,可是由於有一個if判斷,他被直接doFilter,而後請求來到了cas的Filter,由於沒登陸,該filter會自動拼接咱們配置的cas serverName+當前請求的uri,一樣會造成
"http://cas.server.com/login?service=http://後端服務器地址/redirec...徑"這樣的url。

先後端分離B+跨域+CAS

寫不動了,總之要注意:要保持cookie的域一致

對於nginx,若是從 www.a.com/ 代理到 www.b.com/api,那麼造成的cookie的域是會是/api,而瀏覽器發起請求時只能攜帶/域的cookie,因此致使cookie丟失,session失效。能夠經過nginx配置,把/api域下的cookie都放到/便可解決。
爲了不額外的麻煩,最好保持代理先後url一致吧,即都有一個/api前綴,或者都沒有。

對於瀏覽器,發起的ajax所帶的cookie是發起請求的host域名有嚴格關係的,不一樣的域名帶不一樣的cookie,因此若是出現,你明明已經登錄了,可是在此發起ajax請求,後端仍是識別不出來你的登陸狀態,那就多是你發起的請求的域名不一致了。也就是說,你去請求後端接口的時候用www.a.com,結果cas登錄成功後的要回調的接口成了www.b.com,這樣你的cas登陸狀態的cookie就附着在www.b.com的域名上了,而後當你再發起www.a.com的請求的時候,發現你根本帶不上cas下來的cookie,由於域不一樣。
這種狀況一般發生在反向代理的時候,前端發起ajax請求代理服務器www.a.com,代理服務器發起請求到www.b.com,這時候就容易致使域名不一致,請必定要注意這點。

另外,對於當前先後端分開部署的狀況,location.href中,service的回調接口不能直接寫後端地址(至關於www.b.com),而應該寫www.a.com,讓代理服務器去訪問www.b.com,這樣才能保持cookie的域的一致性!!!!

相關文章
相關標籤/搜索