今天本來是要介紹 djangorestframework-jwt 這個套件的,但去查 djangorestframework-jwt 時,才發現專案不維護了,專案的 #484 issue 也做了說明,作者建議轉換到 django-rest-framework-simplejwt 或 django-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。
除了這兩個設定之外,你也可以加入以下設定 (官方文件) 來擴充 (有些我省略了):
測試範例專案網址: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 真的是很不錯的解決方案。