談談我對session, cookies和jwt的理解

最近在做項目重構,因為核心功能僅以restful風格接口提供,因此對于會話管理這一部分,目前考慮使用jwt(Json Web Token)。本文是我在項目開發(fā)過程中對這幾種會話管理技術理解的一些總結。不對之處,請指正。

為什么我們需要會話管理

眾所周知,HTTP協(xié)議是一個無狀態(tài)的協(xié)議,也就是說每個請求都是一個獨立的請求,請求與請求之間并無關系。但在實際的應用場景,這種方式并不能滿足我們的需求。舉個大家都喜歡用的例子,把商品加入購物車,單獨考慮這個請求,服務端并不知道這個商品是誰的,應該加入誰的購物車?因此這個請求的上下文環(huán)境實際上應該包含用戶的相關信息,在每次用戶發(fā)出請求時把這一小部分額外信息,也做為請求的一部分,這樣服務端就可以根據上下文中的信息,針對具體的用戶進行操作。所以這幾種技術的出現都是對HTTP協(xié)議的一個補充。使得我們可以用HTTP協(xié)議+狀態(tài)管理構建一個的面向用戶的WEB應用。

Session與Cookies的區(qū)別

這里我想先談談session與cookies,因為這兩個技術是做為開發(fā)最為常見的。那么session與cookies的區(qū)別是什么?個人認為session與cookies最核心區(qū)別在于額外信息由誰來維護。利用cookies來實現會話管理時,用戶的相關信息或者其他我們想要保持在每個請求中的信息,都是放在cookies中,而cookies是由客戶端來保存,每當客戶端發(fā)出新請求時,就會稍帶上cookies,服務端會根據其中的信息進行操作。當利用session來進行會話管理時,客戶端實際上只存了一個由服務端發(fā)送的session_id,而由這個session_id,可以在服務端還原出所需要的所有狀態(tài)信息,從這里可以看出這部分信息是由服務端來維護的。

除此以外,session與cookies都有一些自己的缺點:

  • cookies的安全性不好,攻擊者可以通過獲取本地cookies進行欺騙或者利用cookies進行CSRF攻擊。
  • 使用cookies時,在多個域名下,會存在跨域問題。
  • session在一定的時間里,需要存放在服務端,因此當擁有大量用戶時,也會大幅度降低服務端的性能。
  • 當有多臺機器時,如何共享session也會是一個問題,也就是說,用戶第一個訪問的時候是服務器A,而第二個請求被轉發(fā)給了服務器B,那服務器B如何得知其狀態(tài)。

實際上,session與cookies是有聯系的,比如,我們可以把session_id存放在cookies中的。

JWT認證

什么是JWT

JWT是Json Web Token的全稱,它是由三部分組成:

  • header
  • payload
  • signature

header中通常來說由token的生成算法和類型組成。如:

{
    "alg":"HS256",
    "typ":"JWT"
}

payload中則用來保存相關的狀態(tài)信息。如用戶id,role,name等。

{
    "id": 10111000,
    "role": "admin",
    "name": "Leo"
}

signature部分由header,payload,secret_key三部分生成,其生成公式為:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret_key)

再將這三個部分組合成header.payload.signature的形式。

JWT如何工作

首先用戶發(fā)出登錄請求,服務端根據用戶的登錄請求進行匹配,如果匹配成功,將相關的信息放入payload中,利用上述算法,加上服務端的密鑰生成token,這里需要注意的是secret_key很重要,如果這個泄露的話,客戶端就可以隨意篡改發(fā)送的額外信息,它是信息完整性的保證。生成token后服務端將其返回給客戶端,客戶端可以在下次請求時,將token一起交給服務端,一般來說我們可以將其放在Authorization首部中,這樣也就可以避免跨域問題。接下來,服務端根據token進行信息解析,再根據用戶信息作出相應的操作。

JWT優(yōu)點與缺點及對應的解決方案

考慮JWT的實現,上面所述的關于session,cookies的缺點都不復存在了,不易被攻擊者利用,安全性提高。利用Authorization首部傳輸token,無跨域問題。額外信息存儲在客戶端,服務端占用資源不多,也不存在session共享問題。感覺JWT優(yōu)勢很明顯,但其仍然有一些缺點:

  • 登錄狀態(tài)信息續(xù)簽問題。比如設置token的有效期為一個小時,那么一個小時后,如果用戶仍然在這個web應用上,這個時候當然不能指望用戶再登錄一次。目前可用的解決辦法是在每次用戶發(fā)出請求都返回一個新的token,前端再用這個新的token來替代舊的,這樣每一次請求都會刷新token的有效期。但是這樣,需要頻繁的生成token。另外一種方案是判斷還有多久這個token會過期,在token快要過期時,返回一個新的token。下面是我在項目里的一個實現。

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
            refresh_token_or_not = True if now() + 600 >= data.get('expire_time') else False
        except:
            return None
        return User.query.get(data['id']), refresh_token_or_not
    
    def auth(func):
      """
      According to the token in the cookies(to compatible with the previous api, will abort) or authorization header to determine the login status. if user has logined,
      the user object will be in global varaibles, so we can access it easily and return normally. however, the cases below
      will not be allowed to finish the request:
          1. no token or authorization content
          2. token is in black list
          3. token is expired
      In two cases, the token will be added to the black list.
          1. logout by the user
          2. the outdate token, which means the token will be expired in ten minutes
      """
      
      def wrapper(*args, **kwargs):
          
          if not getattr(func, 'auth', True):
              return func(*args, **kwargs)
    
          token = request.headers.get('Authorization') or request.cookies.get('session_id')
          # to process the authorization correctly
          if not token:
              return unauthorized('Please login first!')
    
          token = token.split(' ')[-1]
          # logout for user
          if token in redis_db.get('token_black_list'):
              return unauthorized("Invalid token")
    
          user, refresh = User.verify_auth_token(token)
          if user:
              g.user = user
              try:
                  res = func(*args, **kwargs)
              except Exception as e:
                  current_app.logger.exception(e)
                  res = internal_error
              if refresh:
                  res.setdefault('token', user.generate_auth_token())
                  redis_db.get('token_black_list').append(token)
              return res
          else:
              return unauthorized("Invalid token or token has expired")
    
      return wrapper
    
  • 用戶主動注銷。JWT并不支持用戶主動退出登錄,當然,可以在客戶端刪除這個token,但在別處使用的token仍然可以正常訪問。為了支持注銷,我的解決方案是在注銷時將該token加入黑名單。當用戶發(fā)出請求后,如果該token在黑名單中,則阻止用戶的后續(xù)操作,返回Invalid token錯誤。這個地方我再稍微補充一下,其實這里的黑名單操作也比較簡單,把已經注銷的token存入比如說一個set中,那么在每次進行token驗證時,先檢查在set中是否已經存在,如果已經存在的話,則視為token已經失效,直接返回未授權。這一部分在上面的授權代碼中也可以看到,不過我是放到redis緩存中的。

總結

無論session還是cookies或是jwt。目前情況是jwt仍然無法代替session,cookies也會有人用。它們各自有自己的優(yōu)勢和缺點,不能因為有一些缺點就否認技術的存在,缺點仍然可以采用一些技術手段來彌補,比如通過添加csrf token來阻止來自CSRF的攻擊,比如利用redis集群來做session的存儲和共享。技術只是工具,選擇最適合你的才是最重要的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容