互聯網安全認證的問題、場景及方案

在傳統web開發B-S模型中,用戶登陸後創建一個sessionId返回給Browser(User-Agent);在Browser每次請求後端Server時,根據sessionId獲取用戶登陸時的客戶信息,從而實現安全認證(Authentication)。進入移動互聯網時代我們需要對這個模型進行升級,從而實現更廣泛的安全認證。

在移動互聯網開發中我們遇到的問題首先是RESTful無狀態的架構風格變換,簡單來說就是去session。第二個問題在於原生APP和webview中h5訪問的混合式開發的cookie處理複雜度。目前主流採用OAuth2.0+JWT實現。本文專注於具體如何使用OAuth2.0+JWT,以及各種場景下的問題和解決。

安全認證的場景

安全認證的場景

  • 用戶登陸
  • 用戶訪問受保護的資源
  • 超時自動登陸
  • 一級域名下SSO:web 頁面從m.a.com域名跳轉m1.a.com域名
  • APP原生服務跳轉webview頁面
  • Remenber-Me:關閉瀏覽器後重新訪問
  • 企業間聯合登陸:web 頁面從a.com域名跳轉b.com域名

下圖是一個典型的互聯網公司前臺架構。
移動互聯網安全認證
下圖展示了移動APP混合式開發安全認證的SSO主要場景
APP混合式開發SSO主要場景

用戶登陸

OAuth2.0最初的核心目的是解決企業間客戶授權聯合登陸的問題。用戶賬號密碼登陸是登陸的一個特例,並不匹配企業自身互聯網登陸認證的所有場景。本文不考慮用戶使用何種憑證登陸。在使用OAuth2.0的auth server中,登陸後會得到以下數據:

{
    "access_token": "2e17505e-1c34-4ea6-a901-40e49ba786fa",
    "token_type": "bearer",
    "refresh_token": "e5f19364-862d-4212-ad14-9d6275ab1a62",
    "expires_in": 59,
    "scope": "read write",
}

OAuth2.0同時返回了兩個token,access_token用來訪問受保護的資源,refresh_token在access_token失效後用來獲取access_token。

客戶端(User-Agent)需要將這些信息保存起來,從而在後續的接口訪問中使用。如果是原生APP登陸,需要持久化到本地存儲中。如果是web登陸,需要保存在sessionStorage中。

爲什麼要使用refresh_token

既然通過refresh_token可以獲得access_token,爲什麼不直接把access_token的失效時間設置得和refresh_token一樣,這樣不就不需要刷新了嗎?個人認爲有以下考量:

  1. 安全考慮:將access_token失效時間設置過長會增大token劫持別濫用的風險。通過刷新token的機制讓後端有條件自行定義token續期機制,增加安全控制能力。
  2. 職責分離:access_token用戶訪問普通的受限資源,refresh_token通過特地的url來獲取access_token,避免安全問題擴散到各業務邏輯中。

用戶訪問受保護的資源

用戶請求後端受保護的資源時,需要將access_token在報文頭中傳遞到後端。請求報文通常是如下格式:

GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM

第三行報文頭“Authorization: Bearer mF_9.B5f-4.1JqM”Bearer後有個空格,空格後是登陸返回參數中的“access_token”的值。在H5開發中,可以通過axios的攔截器裏面添加。

超時自動登陸

超時自動登陸即access_token過期,通過refresh_token重新獲得access_token。在訪問受保護資源時,如果access_token過期,後端將返回401認證失敗。在不同場景中技術方案又有所差異:

APP原生接口

原生接口請求401可以通過攔截網絡組件,自動請求/oauth/token接口用本地存儲的refresh_token換取新的token後重新發起請求。

Web頁面

web頁面接口請求401也可以攔截網絡請求組件,嘗試刷新access_token後重新發起請求。在SSO場景中刷新token需要做一些特殊處理。

當前域登陸後access_token超時

客戶在當前domain登陸後,sessionStorage保存了refresh_token,在攔截到401報文時,可以直接用存儲的refresh_token刷新access_token。

Webview中超時

H5可以根據與原生開發的約定,通過webview的user-agent屬性判斷是否在指定app中,在需要刷新token時,通過jsBridge委託給原始APP刷新token後重新發起請求

桌面web中超時

在公司內部一級域名下web頁面跳轉的sso場景下,token超時可以通過postMessage和iframe實現委託刷新。

一級域名下SSO

常見的web 頁面從m.a.com域名跳轉m1.a.com域名。如果公司web服務由不同的二級域名提供,這個時候頁面跳轉就存在單點登陸SSO的問題。由於web瀏覽器sessionStorage的同源策略,在新域名下的js無法獲取到用戶登陸時存儲的token。我們可以按如下方案做聯合登陸:

  1. 在H5頁面跳轉時攔截http請求,判斷如果是非同源跳轉,就自動在url地址後增加參數access_token,值爲sessionStorage存儲的access_token。
  2. 在所有頁面頁面加載事件中,判斷url地址中是否有access_token,如果存在就存儲到本地sessionStorage中。
  3. 存儲access_token有個特殊場景,如果當前頁面access_token已由於超時刷新了,url地址中的token是舊的,可以解析JWT載荷中的有效時間對兩個token進行比較,只保存最新的token。

原則上這個方案也適合不同的一級域名的SSO,比如從a.com跳轉到b.com;但這個方案在刷新token時將受信域擴大化了,帶來安全隱患。同時公司不同的域名下一般應該使用不同的認證中心auth-server,應該使用企業間聯合登陸方案。

以上方案前提假設是server端有一個統一的auth-server,access_token在不同的後端服務器間都可以用同一個密鑰驗籤,從而達到認證的效果。這也是在分佈式服務中使用JWT的好處,可以脫離session和集中式存儲做安全認證。

APP原生服務跳轉webview頁面

APP打開新的webview時,直接在url地址後面增加參數access_token。H5頁面統一按SSO方案處理。

Remenber-Me

APP自動登陸

APP關閉後重新打開,用戶不需要重新登陸就可以直接訪問。由於APP持久化了首次登陸的token信息,所以可以直接通過refresh_token刷新access_token實現自動登陸。

桌面web自動登陸

web登陸信息被保存到了sessionStorage,在瀏覽器關閉時sessionStorage的數據就丟失了。如果需要實現這個場景下的自動登陸,需要前後端都做處理。參考:spring-security-oauth2-remember-me

  1. 在登陸時如果用戶選擇了remenber-me,就在返回cookie中增加refresh_token;
Cookie cookie = new Cookie("refreshToken", refreshToken);
cookie.setHttpOnly(true);
cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
cookie.setMaxAge(2592000); // 30 days
ctx.getResponse().addCookie(cookie);
  1. 前端在訪問資源時遇到401時,調用刷新token接口。由於前面設置了httpOnly的cookie,refreshToken被傳遞到了後端。
  2. 在刷新token接口中增加RemenberMeFilter;從cookie中獲取refreshToken,並重置到請求參數中。

爲什麼不直接使用localstorage

localstorage能夠永久保存數據,和APP持久化數據效果是一樣的。爲什麼在APP中永久保存,在桌面web就要搞得這麼麻煩呢?個人認爲有以下原因:

  1. 移動端的核心假設是一個手機只被一個用戶使用,即切換賬號是極爲偶然的場景。所以APP總是默認用戶用相同賬號登陸,切換賬號反而是特例,需要用戶手動操作。
  2. APP可以通過設備號定位用戶,切換終端時可以被檢測到,避免token濫用。
  3. 手機通常都有訪問控制,相對而言終端安全性較高,refreshToken可以長期保存。
  4. Web端在多賬號操作一個電腦時,localstorage非常容易串數據。

總之,用localstorage長期保存token帶來的壞處遠遠大於好處。

企業間聯合登陸方案

如果企業間都是使用OAuth2.0的auth-server,建議直接使用OAuth2.0的授權碼模式做聯合登陸。
在這裏插入圖片描述
在互信的企業間(或者集團公司內部子公司,如淘寶和支付寶),可以跳過用戶確認的環節,實現無感的SSO跳轉。
以上官方流程過於抽象,在實際操作中可以按如下步驟理解:

  1. 當前頁面需要從a.com跳轉另外一個一級域名b.com時,將targetUrl請求後端a.com/auth-server做code跳轉;
  2. a.com/auth-server對access_token做校驗,對targetUrl的host做校驗,通過後生成一個臨時code;
  3. a.com/auth-server認證服務器將用戶導向targetUrl,同時附上一個授權碼code;
  4. 瀏覽器自動重定向到targetUrl,即向b.com/auth-server認證服務器申請令牌;
  5. b.com/auth-server認證服務器拿着code向b.com/auth-server校驗客戶合法性,通過後頒發b.com的access_token,並自動重定向到最終的targetUrl。
  6. 瀏覽器自動重定向到最終targetUrl。
  7. b.com的頁面自行處理access_token。

在整個SSO過程中,爲了滿足多次自動重定向,targetUrl組裝比較複雜。假設a.com服務下認證服務器域名爲"a.com/auth-server",b.com服務下認證服務器域名爲"b.com/auth-server"。a.com下的頁面希望跳轉到"b.com/resouce",請求地址格式爲:

GET /auth-server/authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
        &redirect_uri=${targetUrl} HTTP/1.1
Host: a.com

url地址中targetUrl的組裝方式爲:

urlEncode(
//”/signin/sso“是b.com實現的聯合登陸接口,根據實際情況調整。
 "b.com/auth-server/signin/sso?redirect_uri="
      +urlEncode("b.com/resouce?a=x&b=y")
) 

按以上包裝後,第4步重定向的目標是:b.com/auth-server/signin/sso?redirect_uri=${encodedUrl}&code=xxxx

encodedUrl即urlEncode(“b.com/resouce?a=x&b=y”)的結果,也是第6步重定向的目標

第6步重定向的目標是:b.com/resouce?a=x&b=y&access_token=yyyyyyyy

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