1. 程式人生 > >php 談談我對session, cookies和jwt的理解

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

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

為什麼我們需要會話管理

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

Session與Cookies的區別

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

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

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

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

JWT認證

什麼是JWT

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

  • header
  • payload
  • signature

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

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

payload中則用來儲存相關的狀態資訊。如使用者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如何工作

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

JWT優點與缺點及對應的解決方案

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

  • 登入狀態資訊續簽問題。比如設定token的有效期為一個小時,那麼一個小時後,如果使用者仍然在這個web應用上,這個時候當然不能指望使用者再登入一次。目前可用的解決辦法是在每次使用者發出請求都返回一個新的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加入黑名單。當用戶發出請求後,如果該token在黑名單中,則阻止使用者的後續操作,返回Invalid token錯誤。這個地方我再稍微補充一下,其實這裡的黑名單操作也比較簡單,把已經登出的token存入比如說一個set中,那麼在每次進行token驗證時,先檢查在set中是否已經存在,如果已經存在的話,則視為token已經失效,直接返回未授權。這一部分在上面的授權程式碼中也可以看到,不過我是放到redis快取中的。

總結

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