JWT 與 token簽發


一 JWT認證

自帶的認證方式

token值放置請求頭里

 key:AUTHORIZATION

value :JWT+空格+token 值

from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.authentication import get_authorization_header
class test(GenericViewSet):
authentication_classes = [JSONWebTokenAuthentication]
permission_classes = [IsAuthenticated]
def test(self,request):
return APIResponse(data=123456)

 

 

在用戶注冊或登錄后,我們想記錄用戶的登錄狀態,或者為用戶創建身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token(本質就是token)認證機制。

1
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

1.1 構成和工作原理

JWT的構成

JWT就是一段字符串,由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串。就像這樣:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).

1.1.1 header

jwt的頭部承載兩部分信息:

  • 聲明類型,這里是jwt
  • 聲明加密的算法 通常直接使用 HMAC SHA256

完整的頭部就像下面這樣的JSON:

1
2
3
4
{
'typ': 'JWT',
'alg': 'HS256'
}

然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

1.1.2 payload

載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分

  • 標准中注冊的聲明
  • 公共的聲明
  • 私有的聲明

標准中注冊的聲明 (建議但不強制使用) :

  • iss: jwt簽發者
  • sub: jwt所面向的用戶
  • aud: 接收jwt的一方
  • exp: jwt的過期時間,這個過期時間必須要大於簽發時間
  • nbf: 定義在什么時間之前,該jwt都是不可用的.
  • iat: jwt的簽發時間
  • jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避時序攻擊。

公共的聲明 : 公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.

私有的聲明 : 私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。

定義一個payload:

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

然后將其進行base64加密,得到JWT的第二部分。

1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

1.1.3 signature

JWT的第三部分是一個簽證信息,這個簽證信息由三部分組成:

  • header (base64后的)
  • payload (base64后的)
  • secret

這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分。

1
2
3
4
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

將這三部分用.連接成一個完整的字符串,構成了最終的jwt:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。

關於簽發和核驗JWT,我們可以使用Django REST framework JWT擴展來完成。

文檔網站:http://getblimp.github.io/django-rest-framework-jwt/

1.2 本質原理

jwt認證算法:簽發與校驗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"""
1)jwt分三段式:頭.體.簽名 (head.payload.sgin)
2)頭和體是可逆加密,讓服務器可以反解出user對象;簽名是不可逆加密,保證整個token的安全性的
3)頭體簽名三部分,都是采用json格式的字符串,進行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)頭中的內容是基本信息:公司信息、項目組信息、token采用的加密方式信息
{
"company": "公司信息",
...
}
5)體中的內容是關鍵信息:用戶主鍵、用戶名、簽發時客戶端信息(設備號、地址)、過期時間
{
"user_id": 1,
...
}
6)簽名中的內容時安全信息:頭的加密結果 + 體的加密結果 + 服務器不對外公開的安全碼 進行md5加密
{
"head": "頭的加密字符串",
"payload": "體的加密字符串",
"secret_key": "安全碼"
}
"""
簽發:根據登錄請求提交來的 賬號 + 密碼 + 設備信息 簽發 token
1
2
3
4
5
6
7
"""
1)用基本信息存儲json字典,采用base64算法加密得到 頭字符串
2)用關鍵信息存儲json字典,采用base64算法加密得到 體字符串
3)用頭、體加密字符串再加安全碼信息存儲json字典,采用hash md5算法加密得到 簽名字符串

賬號密碼就能根據User表得到user對象,形成的三段字符串用 . 拼接成token返回給前台
"""
校驗:根據客戶端帶token的請求 反解出 user 對象
1
2
3
4
5
"""
1)將token按 . 拆分為三段字符串,第一段 頭加密字符串 一般不需要做任何處理
2)第二段 體加密字符串,要反解出用戶主鍵,通過主鍵從User表中就能得到登錄用戶,過期時間和設備信息都是安全信息,確保token沒過期,且時同一設備來的
3)再用 第一段 + 第二段 + 服務器安全碼 不可逆md5加密,與第三段 簽名字符串 進行碰撞校驗,通過后才能代表第二段校驗得到的user對象就是合法的登錄用戶
"""

drf項目的jwt認證開發流程(重點)

1
2
3
4
5
6
7
"""
1)用賬號密碼訪問登錄接口,登錄接口邏輯中調用 簽發token 算法,得到token,返回給客戶端,客戶端自己存到cookies中

2)校驗token的算法應該寫在認證類中(在認證類中調用),全局配置給認證組件,所有視圖類請求,都會進行認證校驗,所以請求帶了token,就會反解出user對象,在視圖類中用request.user就能訪問登錄的用戶

注:登錄接口需要做 認證 + 權限 兩個局部禁用
"""

1.2.1 補充base64編碼解碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64
import json
dic_info={
"sub": "1234567890",
"name": "lqz",
"admin": True
}
byte_info=json.dumps(dic_info).encode('utf-8')
# base64編碼
base64_str=base64.b64encode(byte_info)
print(base64_str)
# base64解碼
base64_str='eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogImxxeiIsICJhZG1pbiI6IHRydWV9'
str_url = base64.b64decode(base64_str).decode("utf-8")
print(str_url)

二 drf-jwt安裝和簡單使用

2.1 官網
1
http://getblimp.github.io/django-rest-framework-jwt/
2.2 安裝
1
pip install djangorestframework-jwt
2.3 使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 1 創建超級用戶
python3 manage.py createsuperuser
# 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

# 4 setting.py中配置認證使用jwt提供的jsonwebtoken
# 5 postman發送訪問請求(必須帶jwt空格)

三 實戰之使用Django auth的User表自動簽發

3.1 配置setting.py

1
2
3
4
5
6
7
8
import datetime
JWT_AUTH = {
# 過期時間1天
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
# 自定義認證結果:見下方序列化user和自定義response
# 如果不自定義,返回的格式是固定的,只有token字段
'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}

3.2 編寫序列化類ser.py

1
2
3
4
5
6
from rest_framework import serializers
from users import models
class UserModelSerializers(serializers.ModelSerializer):
classMeta:
model = models.UserInfo
fields = ['username']
3.3 自定義認證返回結果(setting中配置的)
1
2
3
4
5
6
7
8
9
10
11
#utils.py
from users.ser import UserModelSerializers
defjwt_response_payload_handler(token, user=None, request=None):
return {
'status': 0,
'msg': 'ok',
'data': {
'token': token,
'user': UserModelSerializers(user).data
}
}

3.4 基於drf-jwt的全局認證:

                                                                          從頭部  Authorization  字段取出 JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework_jwt.authentication import get_authorization_header,jwt_get_username_from_payload
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
import jwt
from rest_framework_jwt.serializers import jwt_payload_handler,jwt_encode_handler
class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
def authenticate(self, request):
jwt_value = get_authorization_header(request)
if not jwt_value:
raise AuthenticationFailed('Authorization 字段是必須的')
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed('簽名過期')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法用戶')
user = self.authenticate_credentials(payload)
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return user, jwt_value
 

3.5 全局使用

1
2
3
4
5
6
7
# setting.py
REST_FRAMEWORK = {
# 認證模塊
'DEFAULT_AUTHENTICATION_CLASSES': (
'users.app_auth.JSONWebTokenAuthentication',
),
}

3.6 局部啟用禁用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 局部禁用
authentication_classes = []
# 局部啟用
from user.authentications import JSONWebTokenAuthentication
authentication_classes = [JSONWebTokenAuthentication]
# 實際代碼如下view.py
# 自定義Response
classCommonResponse(Response):
def__init__(self,status,msg,data='',*args,**kwargs):
dic={'status':status,'msg':msg,'data':data}
super().__init__(data=dic,*args,**kwargs)
# 測試訂單接口
from users.app_auth import JSONWebTokenAuthentication
classOrderView(APIView):
# authentication_classes = [JSONWebTokenAuthentication]
authentication_classes = []
defget(self,request):
return CommonResponse('100', '成功',{'數據':'測試'})

3.7 多方式登錄:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
## views.py
# 重點:自定義login,完成多方式登錄
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
classLoginViewSet(ViewSet):
# 需要和mixins結合使用,繼承GenericViewSet,不需要則繼承ViewSet
# 為什么繼承視圖集,不去繼承工具視圖或視圖基類,因為視圖集可以自定義路由映射:
# 可以做到get映射get,get映射list,還可以做到自定義(靈活)
deflogin(self, request, *args, **kwargs):
serializer = serializers.LoginSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
token = serializer.context.get('token')
return Response({"token": token})




## ser.py
# 重點:自定義login,完成多方式登錄
classLoginSerializer(serializers.ModelSerializer):
# 登錄請求,走的是post方法,默認post方法完成的是create入庫校驗,所以唯一約束的字段,會進行數據庫唯一校驗,導致邏輯相悖
# 需要覆蓋系統字段,自定義校驗規則,就可以避免完成多余的不必要校驗,如唯一字段校驗
username = serializers.CharField()
classMeta:
model = models.User
# 結合前台登錄布局:采用賬號密碼登錄,或手機密碼登錄,布局一致,所以不管賬號還是手機號,都用username字段提交的
fields = ('username', 'password')

defvalidate(self, attrs):
# 在全局鈎子中,才能提供提供的所需數據,整體校驗得到user
# 再就可以調用簽發token算法,將user信息轉換為token
# 將token存放到context屬性中,傳給外鍵視圖類使用
user = self._get_user(attrs)
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
self.context['token'] = token
return attrs

# 多方式登錄
def_get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
import re
if re.match(r'^1[3-9][0-9]{9}$', username):
# 手機登錄
user = models.User.objects.filter(mobile=username, is_active=True).first()
elif re.match(r'^.+@.+$', username):
# 郵箱登錄
user = models.User.objects.filter(email=username, is_active=True).first()
else:
# 賬號登錄
user = models.User.objects.filter(username=username, is_active=True).first()
if user and user.check_password(password):
return user

raise ValidationError({'user': 'user error'})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# utils.py
import re
from .models import User
from django.contrib.auth.backends import ModelBackend
classJWTModelBackend(ModelBackend):
defauthenticate(self, request, username=None, password=None, **kwargs):
try:
if re.match(r'^1[3-9]\d{9}$', username):
user = User.objects.get(mobile=username)
else:
user = User.objects.get(username=username)
except User.DoesNotExist:
returnNone
if user.check_password(password) and self.user_can_authenticate(user):
return user

3.8 配置多方式登錄

1
2
settings.py
AUTHENTICATION_BACKENDS = ['user.utils.JWTModelBackend']

四 實戰之自定義User表,手動簽發

4.1 手動簽發JWT:

1
2
3
4
5
6
7
# 可以擁有原生登錄基於Model類user對象簽發JWT
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)

4.2 編寫登陸視圖類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# views.py
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
from users.models import User
classLoginView(APIView):
authentication_classes = []
defpost(self,request):
username=request.data.get('username')
password=request.data.get('password')
user=User.objects.filter(username=username,password=password).first()
if user: # 能查到,登陸成功,手動簽發
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return Response('100','登陸成功',data={'token':token})
else:
return Response('101', '登陸失敗')

4.3 編寫認證組件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app_auth.py
from users.models import User
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
class MyJSONWebTokenAuthentication(BaseAuthentication):
def authenticate(self, request):
jwt_value = get_authorization_header(request)

ifnot jwt_value:
raise AuthenticationFailed('Authorization 字段是必須的')
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed('簽名過期')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法用戶')
username = jwt_get_username_from_payload(payload)
print(username)
user_ID = payload.get(user_id)
user = User.objects.filter(username=username).first()
print(user)

return user, jwt_value

                        

class LoginView(APIView):

def post(self,request):
        authentication_classes = [MyJSONWebTokenAuthentication]
 print(request.user)
return

 

4.4 登陸獲取token

image-20200708185227693

4.5 編寫測試接口

1
2
3
4
5
6
7
from users.app_auth import JSONWebTokenAuthentication,MyJSONWebTokenAuthentication
classOrderView(APIView):
# authentication_classes = [JSONWebTokenAuthentication]
authentication_classes = [MyJSONWebTokenAuthentication]
defget(self,request):
print(request.user)
return CommonResponse('100', '成功',{'數據':'測試'})

4.6 測試

image-20200708185327976


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM