iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
0

今天本來是要介紹 djangorestframework-jwt 這個套件的,但去查 djangorestframework-jwt 時,才發現專案不維護了,專案的 #484 issue 也做了說明,作者建議轉換到 django-rest-framework-simplejwtdjango-rest-framework-jwt(drf-jwt) 去。 drf-jwt 看起來是 fork 自 djangorestframework-jwt ,新的作者有在持續更新,上傳到 pypi 的名稱則變更為 drf-jwt,所以我們就改介紹這個套件了。

為什麼會需要 JWT?REST framework 裡就有包含一個 Token 的方案了 - rest_framework.authtoken,這個方案的缺點是會需要額外的資料庫查詢來查驗 Token 是否正確,因為他的實作是產生一個資料表格來存放 Token,在每次 API 請求時,拿 Token 去資料表格查詢。在一般的情況下,是可以被接受,但是在大量 API 請求的情況下,這個查詢成本就增加了。

而 JWT 就是為了解決這個問題,JWT 的全名是 JSON Web Token,是一種基於 JSON 的開放標準 (RFC 7519),它定義了一種簡潔 (compact) 且自包含 (self-contained) 的方式,用於在雙方之間安全地將訊息作為 JSON 物件傳輸。而這個訊息是經過數位簽章 (Digital Signature),因此可以不用預先產生 Token 放在資料庫,就可以被驗證及信任。也可以使用密碼 (經過 HMAC 演算法) 或用一對 公鑰/私鑰 (經過 RSA 或 ECDSA 演算法) 來對 JWT 進行簽章來增加安全性。 (摘錄自是誰在敲打我窗?什麼是 JWT ?)

drf-jwt 這個套件主要就是為 REST framework 擴充 JWT 驗證的功能。

安裝

poetry add drf-jwt

使用

第一步,是指定 authentication 以確保 API 需要 JWT 驗證。這個步驟有兩個方法,一個是全域的,一個則是個別指定。

全域的方法是在 settings 裡指定,APIView 沒有指定 authentication_classes 的話,就自動使用。

# settings
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        # ...
    ),
}

個別指定的話,是在 APIView 的 authentication_classes 裡指定

from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

class ExampleView(APIView):
    authentication_classes = [JSONWebTokenAuthentication,]  # 就是這裡!
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        content = {
            'username': request.user.username,  # `django.contrib.auth.User` instance.
        }
        return Response(content)

第二步是修改 urls.py,這邊要加入三個 drf-jwt 已經寫好的 APIView,一個是取得 JWT,一個則是拿舊的 JWT 來更新為新的 JWT,最後一個則是檢查 JWT 是否正確。

from django.urls import path, include
from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token, verify_jwt_token
from news import views
#...

urlpatterns = [
    '',
    # ...
    path('api/example/', views.ExampleView.as_view()),

    path('api/token-auth/', obtain_jwt_token),
    path('api/token-refresh/', refresh_jwt_token),
    path('api/token-verify/', verify_jwt_token),
]

接下來就可以執行 runserver 來測試了,先取得 JWT

$ curl -X POST -d "username=admin&password=password123" http://localhost:8000/api/token-auth/

# 結果是 JSON 格式,你可以用 jq 這個指令取出 "token" key 對應的值: jq -r ".token" 
# {"pk":1599101388,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTk5MTAxMzg4LCJleHAiOjE1OTkxMDE2ODgsImp0aSI6IjI5ZDc4NWFlLWM1NmItNDJiMC04YTRiLTEyYTM3YWM0ZTZkMyIsInVzZXJfaWQiOjEsIm9yaWdfaWF0IjoxNTk5MTAxMzg4fQ.kXFRt_AF-X9AEI-18C8vH5HFpFIxrtlBgmLuUhHeePU"

然後帶著拿到的 JWT 去呼叫其他 API

$ curl -H "Authorization: Bearer <your_token>" http://localhost:8000/api/example/

JWT 是會過期的,所以經過一段時間之後,需要拿舊的 JWT 去換新的。

$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api/token-refresh/

設定

前面提到 JWT 會過期,那麼,該怎麼設定這個時間呢?

這個設定一樣是要寫在 settings 裡:

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(
        seconds=env.int("JWT_EXPIRATION_DELTA_SECONDS", default=300)),
    'JWT_ALLOW_REFRESH': True,
}

這邊遵循 12-factor 原則,我把過期時間做成設定(JWT_EXPIRATION_DELTA),讓它可以讀取環境變數或外部設定檔來決定,那如果讀取不到,預設值為 300 秒。

JWT_ALLOW_REFRESH 設為 True,則是表示允許更新 Token。

除了這兩個設定之外,你也可以加入以下設定 (官方文件) 來擴充 (有些我省略了):

  • JWT_ENCODE_HANDLER / JWT_DECODE_HANDLER :加解密的處理常式,預設是 rest_framework_jwt.utils.jwt_encode_payload 跟 rest_framework_jwt.utils.jwt_decode_token
  • JWT_PAYLOAD_HANDLER / JWT_PAYLOAD_GET_USER_ID_HANDLER / JWT_RESPONSE_PAYLOAD_HANDLER:打包 payload 用的處理常式
  • JWT_SECRET_KEY:這跟 Django secret key 一樣意思,預設值是用 Django 的 secret key
  • JWT_GET_USER_SECRET_KEY:可以更加提升安全性的 secret key,這個值要填為函式,函數必須要接收一個 user 的參數,以便為每個使用者來產生 secret key,這邊如果更動到,那麼所有現行的 JWT 都會失效。
  • JWT_PUBLIC_KEY:公鑰,這個跟 JWT_SECRET_KEY 是互斥的。預設值是 None
  • JWT_PRIVATE_KEY:私鑰,同上。預設值是 None
  • JWT_ALGORITHM:指定公私鑰的演算法。
  • JWT_VERIFY:預設為真,必須檢查。
  • JWT_VERIFY_EXPIRATION:是否檢查 JWT 過期,預設為真。
  • JWT_REFRESH_EXPIRATION_DELTA
  • JWT_AUTH_HEADER_PREFIX:Authorization 表頭後的前置字串,預設是 Bearer 。

測試範例專案網址:https://github.com/elleryq/ithome-iron-2020-django/tree/day-05

結語

在目前的應用上,JWT 已經變成一個蠻主流的選項,但是,在使用 JWT 時得小心,不要把 JWT 當做 session 來使用,因為第一個,這不是它主要的用途,第二個,Token 是可以使用 jwt.io 上的服務來 decode 出 JSON 的,而 JSON 揭露了一定程度的資訊,你如果放了敏感資訊,Decode 後會被看到。

說完這些,來說說 JWT 的好處,JWT 的 Token 看起來夠長、夠亂,有期限,不需要茲額外存放到資料庫,而且也能使用公私鑰來檢驗,例如 Firebase 也是使用 JWT 作為 Token,所以 JWT 真的是很不錯的解決方案。

參考資料


上一篇
04. djangorestframework (2)
下一篇
06. django-extensions
系列文
加速你的 Django 網站開發 - Django 的好用套件30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言