微服務session落坑記

68號小喇叭發表於2018-06-17

微服務session落坑記

本文適用於對session、cookie有一定了解的同學,主要以問題定位過程為線索,簡單講述tomcat session生成機制、oauth2認證過程以及spring方法引數對映處理等內容

背景知識

  • session:由於http協議無狀態,為了儲存使用者狀態資訊,web容器支援session管理機制,當客戶端請求web應用時,如果在處理過程中呼叫了request.getSession()方法,則web容器會先根據url或者cookie中上傳的JSESSIONID(預設,可以另行設定,該值會被設定到request物件的requestedSessionId欄位)查詢對應session,如果獲取不到將自動建立一個session物件;當session過期或被放棄後,伺服器將終止該session
protected Session doGetSession(boolean create) {
    // 略去部分程式碼
    if (requestedSessionId != null) {
        session = manager.findSession(requestedSessionId);
        // 略去部分程式碼
        String sessionId = getRequestedSessionId();
        // 略去部分程式碼
        session = manager.createSession(sessionId);

        // 建立cookie並寫入response
        if (session != null
                && context.getServletContext()
                        .getEffectiveSessionTrackingModes()
                        .contains(SessionTrackingMode.COOKIE)) {
            Cookie cookie =
                ApplicationSessionCookieConfig.createSessionCookie(
                        context, session.getIdInternal(), isSecure());
            response.addSessionCookieInternal(cookie);
        }
        // 略去部分程式碼
    }
複製程式碼
  • cookie:為了伺服器能夠識別不同的客戶端,客戶端通過cookie儲存服務端返回的資料(如JSESSIONID,tomcat伺服器預設的session對應的cookie key為JSESSIONID),然後服務端通過客戶端請求時放在http header中的cookie資料找到之前為客戶端建立的session;cookie分為會話cookie和持久cookie,會話Cookie瀏覽器會話有效期間存在,持久cookie服務端會設定http header 快取相關欄位指示客戶端快取策略,cookie傳到客戶端後,儲存在某個目錄下
  • oauth2:oauth是一個開放標準,允許使用者讓第三方應用訪問該使用者在某一網站上儲存的私密的資源(如照片,視訊,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用,可以用於微服務環境下的公共鑑權,而本文服務鑑權走的就是oauth2

前人挖坑後人落,都是框架惹的禍

問題特徵

  • 需求上線在即,發現某個介面請求後會新生成JSESSIONID並以Cookie形式回寫瀏覽器,導致後續請求鑑權失敗,必現
  • 服務相互呼叫關係比較複雜,需要在開發環境測試該功能,本地沒有測試

問題定位過程:

  • 交流得知,此前定位過程中,將被請求方法體程式碼全部登出後,問題仍然存在,於是錯過了檢視問題程式碼(其實是方法引數出了問題),儘早定位出問題的好機會
  • 懷疑是否nginx會話保持策略導致的問題(但是該策略應該會對所有請求均產生影響,而不只是單個請求),檢視nginx.conf配置,該服務沒有走負載均衡
  • 懷疑是否nginx對該請求路徑(路徑中包含auth敏感欄位)做了特殊配置,但檢視配置,nginx對於該服務請求路徑配置規則很簡單,且將請求路徑中auth欄位刪掉後測試,問題仍然存在
  • 懷疑是否有外圍程式碼重寫了cookie,搜尋沒找到相關程式碼
  • 梳理服務鑑權過程(見top圖,圖中做了縮略,將公共認證服務刪除,直接對接oauth2),描述如下:
    1)認證鑑權邏輯被封裝在了公共Filter中,對所有請求進行攔截,判斷是否已登入
    2)如果沒有上傳JSESSIONID或者無法據其在redis找到session及token資訊,返回客戶端重定向到login介面
    3)客戶端呼叫login介面:服務端呼叫oauth2(其實有個公共的鑑權服務)認證合法性,將認證返回的token資訊融合自身的JSESSIONID寫入redis,隨後將服務端JSESSIONID寫回客戶端cookie,坑:此處複用了tomcat預設的JSESSIONID,推測是為了複用request的getRequestedSessionId方法,該方法會直接取客戶端cookie提交的JSESSIONID,業務中多次用到了getRequestedSessionId方法,tomcat session和服務認證返回session除了都叫JSESSIONID外,沒有半毛錢關係,但是因為重名,相互之間會覆寫
    4)客戶端帶著認證返回的JSESSIONID繼續呼叫之前的業務介面,通過認證,走業務邏輯,其實此時tomcat中的JSESSIONID和上傳的JSESSIONID不一致,根據上傳的JSESSIONID在tomcat是找不到對應session的
    5)認證過程還有其他邏輯,此處做了刪減,不影響主體流程
  • 梳理之後,發現二者之間雖然key一致但是值不同,而且彼此會干擾,於是懷疑外圍程式碼呼叫了getSession方法,搜尋程式碼未找到
  • 檢視請求路徑對應方法,方法中使用了session引數,但是方法體中未使用該引數,聯想到spring的引數值自動對映機制(見ServletRequestMethodArgumentResolver),在為session賦值的過程中呼叫了getSession方法,進而由於在tomcat找不到對應session而新建、回寫相關cookie到客戶端
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    Class<?> paramType = parameter.getParameterType();
    // 略去部分程式碼
    if (HttpSession.class.isAssignableFrom(paramType)) {
        HttpSession session = request.getSession();
        // 略去部分程式碼
    }
}
複製程式碼
  • 刪除引數,測試,問題解決

  • 使用該認證機制的服務的業務方法中不要使用tomcat的session機制(如getSession或session引數),否則就會出現這個問題

解決方案

  • 針對此問題,將session引數去掉
  • 將認證機制的JSESSIONID換成另外一個名字,但是改動量比較大,可能會有其他坑
  • 走分散式session(token)解決方案,將tomcat的session管理機制和分散式session(token)結合起來
  • 採用jwt token形式鑑權,服務端不儲存token,僅做驗證和重新整理
歡迎關注我的微信公眾號

68號小喇叭

相關文章