JWT認證(5星)
token發展史
在使用者註冊或登入後,我們想記錄使用者的登入狀態,或者為使用者建立身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token(本質就是token)認證機制。
構成和工作原理
JWT的構成
JWT就是一段字串,由三段資訊構成的,將這三段資訊文字用.
連結一起就構成了Jwt字串。就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我們稱它為頭部(header),第二部分我們稱其為荷載(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).
header(頭部)
jwt的頭部承載兩部分資訊:
- 宣告型別,這裡是jwt
- 宣告加密的演算法 通常直接使用 HMAC SHA256
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然後將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload(荷載)
荷載就是存放類似使用者資訊,過期時間,簽發時間...
{
"userid": "1",
"name": "John Doe",
"exp": 1214356
}
然後將其進行base64加密,得到JWT的第二部分。
eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9
signature(簽證)
JWT的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:
- header (base64解密後加密演算法加密後的)
- payload (base64解密後加密演算法加密後的)
- secret(金鑰=加鹽)
這個部分需要base64加密後的header和base64加密後的payload使用.
連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret
組合加密,然後就構成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.
連線成一個完整的字串,構成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是儲存在伺服器端的,jwt的簽發生成也是在伺服器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。
關於簽發和核驗JWT,我們可以使用Django REST framework JWT擴充套件來完成。
文件網站:http://getblimp.github.io/django-rest-framework-jwt/
補充base64編碼解碼
import base64
import json
payload = {
"userid": "1",
"name": "John Doe",
"exp": 1214356
}
json_payload = json.dumps(payload)
# 編碼
res = base64.b64encode(json_payload.encode('utf8'))
print(res)
# 解碼
res2 = json.loads(base64.b64decode(res))
print(res2)
# b'eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9'
# {'userid': '1', 'name': 'John Doe', 'exp': 1214356}
本質原理
jwt認證演算法:簽發與校驗
1)jwt分三段式:頭.體.簽名 (head.payload.sgin)
2)頭和體是可逆加密,讓伺服器可以反解出user物件;簽名是不可逆加密,保證整個token的安全性的(base64反解出的是hash加密後的密文)
3)頭體簽名三部分,都是採用json格式的字串,進行加密,可逆加密一般採用base64演算法,不可逆加密一般採用hash(md5)演算法
4)頭中的內容是基本資訊:公司資訊、專案組資訊、token採用的加密方式資訊
{
"company": "公司資訊",
...
}
5)體中的內容是關鍵資訊:使用者主鍵、使用者名稱、簽發時客戶端資訊(裝置號、地址)、過期時間
{
"user_id": 1,
...
}
6)簽名中的內容是安全資訊:頭的加密結果 + 體的加密結果 + 伺服器不對外公開的安全碼(對整個字典進行md5加密)
{
"head": "頭的加密字串",
"payload": "體的加密字串",
"secret": "安全碼"
}
簽發:根據登入請求提交來的 賬號 + 密碼 + 裝置資訊 簽發 token
1)用基本資訊儲存json字典,採用base64演算法加密得到 頭字串
2)用關鍵資訊儲存json字典,採用base64演算法加密得到 體字串
3)用頭、體加密字串再加安全碼資訊儲存json字典,採用hash md5演算法加密得到 簽名字串
賬號密碼就能根據User表得到user物件,形成的三段字串用 . 拼接成token返回給前臺
校驗:根據客戶端帶token的請求 反解出 user 物件
1)將token按 . 拆分為三段字串,第一段 頭加密字串 一般不需要做任何處理
2)第二段 體加密字串,要反解出使用者主鍵,通過主鍵從User表中就能得到登入使用者,過期時間和裝置資訊都是安全資訊,確保token沒過期,且時同一裝置來的
3)再用 第一段 + 第二段 + 伺服器安全碼 不可逆md5加密,與第三段 簽名字串 進行碰撞校驗,通過後才能代表第二段校驗得到的user物件就是合法的登入使用者
drf專案的jwt認證開發流程(重點)
1)用賬號密碼訪問登入介面,登入介面邏輯中呼叫 簽發token 演算法,得到token,返回給客戶端,客戶端自己存到cookies中
2)校驗token的演算法應該寫在認證類中(在認證類中呼叫),全域性配置給認證元件,所有檢視類請求,都會進行認證校驗,所以請求帶了token,就會反解出user物件,在檢視類中用request.user就能訪問登入的使用者
注:登入介面需要做 認證 + 許可權 兩個區域性禁用
drf-jwt安裝和簡單使用(2星)
安裝
pip3 install djangorestframework-jwt
簡單使用
簽發
# 1 建立超級使用者
python3 manage.py createsuperuser
# 解釋下為什麼要建立超級使用者:因為djangorestframework-jwt認證是基於django的auth裡的user表作關聯的,所以驗證的資料也必須源自於這張表
# 2 配置路由urls.py
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path('login/', obtain_jwt_token),
]
# 3 postman測試
向後端介面傳送post請求,攜帶使用者名稱密碼,即可看到生成的token
認證
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class BookAPIView(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookModelSerializer
# 必須用這個認證類
authentication_classes = [JSONWebTokenAuthentication, ]
# 還要配合這個許可權
permission_classes = [IsAuthenticated, ]
在postman裡
JWT使用auth表簽發token,自定製返回格式(3星)
配置setting.py
JWT_AUTH ={
# token的過期時間
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
# 如果不自定義,返回的格式是固定的,只有token欄位
# 這裡把下面自定製的函式註冊進來
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
}
自定製的py檔案內
def jwt_response_payload_handler(token, user=None, request=None):
return {
'code': 1000,
'msg': '登陸成功',
'username': user.username,
'token': token
}
這時登陸時返回的格式就變成了:
djangorestframework-jwt模組原始碼分析(2星)
簽發token
ObtainJSONWebToken.as_view()--->ObtainJSONWebToken---->post方法
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid(): # 驗證使用者登入和簽發token,都在序列化類的validate方法中完成的
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
# 返回了我們們自定指的格式
'''
{
'code':100,
'msg':'登入成功',
'username':user.username,
'token': token,
}
'''
return response
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# 全域性鉤子函式
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}
if all(credentials.values()):
# 根據使用者名稱密碼去auth的user表校驗,是否存在
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
# 生成payload
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload), # 通過payload生成token
'user': user
}
else:
# 不在拋異常,前端就看到資訊了
raise serializers.ValidationError(msg)
else:
raise serializers.ValidationError(msg)