JWT

HuangQiaoqi發表於2024-04-21

JWT介紹

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

image-20240418171423016

構成和工作原理

JWT的構成

​ JWT就是一長串字串,被.分成三段,分別是頭部,載荷,簽名

​ jwt的頭部承載兩部分的資訊,頭部會進行base64的轉碼

  1. 聲名型別,標識這裡是jwt
  2. 聲名加密的演算法 通常是HMAC SHA256
# header
{
  'typ': 'JWT',
  'alg': 'HS256'
}

# 轉碼後
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

payload

​ 載荷就是存放有效資訊的地方,它也會被進行base64轉碼

# payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

# 轉碼後
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

signature

​ 簽名是用來驗證前兩個部分有沒有被篡改過,它由三部分組成,這個部分需要base64加密後的header和base64加密後的payload使用.連線組成的字串,然後透過header中宣告的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分

  1. header (base64過後)
  2. payload(base64過後)
  3. secret(鹽)

本質原理

jwt認證演算法:簽發與驗證

"""
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": "安全碼"
}
"""

簽發

# 當使用者登入校驗透過後
1-將基本資訊字典轉成json字串,再進行base64轉碼,得到頭字串
2-將使用者資訊字典轉成json字串,再進行base64轉碼,得到載荷字串
3-將頭和荷載字串打包成json字典字串,再按頭字串的加密方式加密,得到簽名字串

校驗

# 根據客戶端帶token的請求反解析出user物件
1-將token分成三段
2-將第二段的使用者資訊解析出來,從使用者表得到登入使用者
3-將第一段和第二段加密 再與第三段比較,一致則校驗透過,否則校驗不透過

drf專案的jwt認證開發流程

1-瀏覽器發請求帶賬號密碼,登入介面透過校驗,簽發token給瀏覽器,瀏覽器儲存到自己的cookies裡面
2-瀏覽器帶token發請求,drf認證元件反解出使用者物件,

注意:燈籠褲幾口需要做 認證 + 許可權 兩個區域性禁用

base64編碼解碼

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安裝和簡單使用

安裝

pip install djangorestframework-jwt

使用

自定義返回內容

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

# 序列化類 繼承TokenObtainPairSerialize
class AutoLoginSerializer(TokenObtainPairSerializer):
    # 重寫父類的全域性鉤子
    def validate(self, attrs):
        dic = super().validate(attrs)
        data = {
            'code': 100,
            'message': '登陸成功',
            'username': self.user.username,
            'refresh': dic.get('refresh'),
            'access': dic.get('access')
        }
        return data

# 檢視類
class AutoUserView(GenericViewSet):
    serializer_class = AutoLoginSerializer

    @action(methods=['POST'], detail=False)
    def login(self, request):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            return Response(serializer.validated_data)
        
        
# 註冊路由
router.register(prefix='auto', viewset=AutoUserView, basename='auto')

# settings配置
# JWT配置 裡面具體配置可以參考文件
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),  # 配置過期時間
    'REFRESH_TOKEN_LIFETIME': timedelta(days=3),
    # 用於生成access和刷refresh的序列化器。
    "TOKEN_OBTAIN_SERIALIZER": "app02.serializer.AutoLoginSerializer",
}


INSTALLED_APPS = [
    # 不要忘記註冊app
    'rest_framework_simplejwt',
]

定製payload內容

​ 只需要重寫get_token方法,該方法傳入user物件,返回token

class AutoLoginSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        # 往第二段資料裡面加東西 這裡加一個名字
        token['name'] = user.username
        return token

    # 重寫全域性鉤子
    def validate(self, attrs):
        dic = super().validate(attrs)
        data = {
            'code': 100,
            'message': '登陸成功',
            'username': self.user.username,
            'refresh': dic.get('refresh'),
            'access': dic.get('access')
        }

        return data

多方式登入

​ 可以透過多種方式登入,如使用者名稱/手機號/郵箱

檢視類

class UserView(GenericViewSet, CreateModelMixin):
    serializer_class = None

    def get_serializer_class(self):
        if self.action == 'login':
            return UserLSerializer
        else:
            return UserRSerializer

    @action(methods=['POST'], detail=False)
    def register(self, request):
        res = super().create(request)
        return Response({'code': 200, 'message': '註冊成功', 'result': res.data})

    @action(methods=['POST'], detail=False)
    def login(self, request):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            refresh = serializer.context.get('refresh')
            access = serializer.context.get('access')
            return Response({'code': 200, 'message': '登入成功', 'refresh': refresh, 'access': access})
        return Response(serializer.errors)

序列化類

class UserLSerializer(serializers.Serializer):
    # 因為登入的時候只輸入使用者名稱密碼
    # 所以只需要序列化兩個欄位
    username = serializers.CharField()
    password = serializers.CharField()

    # 這個方法用來拿到登入物件
    def _get_user(self, attrs):
        # 手機號正規表示式
        phone_regex = r'^1[3-9]\d{9}$'
        # 郵箱正規表示式
        email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
        # 拿到 手機號/郵箱/使用者名稱
        username = attrs.get('username')
        # 拿到密碼
        password = attrs.get('password')
        # 下面分別做判斷 看使用者名稱是以什麼方式登入
        if re.match(phone_regex, username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(email_regex, username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            return user


    def validate(self, attrs):
        user = self._get_user(attrs)
        # 如果上面驗證失敗user就為None,就拋異常
        if not user:
            raise ValidationError('使用者名稱或密碼錯誤')
        refresh = RefreshToken.for_user(user)
        self.context['refresh'] = str(refresh)
        self.context['access'] = str(refresh.access_token)
        return attrs

自定義使用者表,手動簽發和認證

# 自定義使用者表,不是擴寫auth的user表
# 自己寫登陸簽發token

檢視類

class NormaUserView(GenericViewSet, CreateModelMixin):
    serializer_class = None

    def get_serializer_class(self):
        if self.action == 'login':
            return NormalUserLSerializer
        else:
            return NormalUserRSerializer

    @action(methods=['POST'], detail=False)
    def register(self, request):
        res = super().create(request)
        return Response({'code': 200, 'message': '註冊成功', 'result': res.data})

    @action(methods=['POST'], detail=False)
    def login(self, request):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            refresh = serializer.context.get('refresh')
            access = serializer.context.get('access')
            return Response({'code': 200, 'message': '登入成功', 'refresh': refresh, 'access': access})
        return Response(serializer.errors)

序列化類

class NormalUserRSerializer(ModelSerializer):
    class Meta:
        model = NormalUser
        fields = ['username', 'password']

    def validate(self, attrs):
        username = attrs.get('username')
        user = NormalUser.objects.filter(username=username).first()
        if user:
            raise ValidationError(f'使用者{user.username}已存在')
        return attrs


class NormalUserLSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()

    def _get_user(self, attrs):
        # 拿到 手機號/郵箱/使用者名稱
        username = attrs.get('username')
        # 拿到密碼
        password = attrs.get('password')
        user = NormalUser.objects.filter(username=username, password=password).first()
        print(user)
        return user

    def validate(self, attrs):
        user = self._get_user(attrs)
        if not user:
            raise ValidationError('使用者名稱或密碼錯誤')
        token = RefreshToken.for_user(user)
        access = str(token.access_token)
        refresh = str(token)
        self.context['access'] = access
        self.context['refresh'] = refresh
        return attrs

自定義使用者表,手動簽發和認證

​ 現在我定義一個圖書表,只有登入之後的使用者才能呼叫這張表的介面

圖書檢視類 序列化類

class BookView(GenericViewSet, ListModelMixin, RetrieveModelMixin):
    serializer_class = BookSerializer
    queryset = Book.objects.all()
    authentication_classes = [CommonAuthentication]
    
    
class BookSerializer(ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

認證類

from rest_framework.exceptions import AuthenticationFailed

# 繼承JWTAuthentication
class CommonAuthentication(JWTAuthentication):
    # 重寫authenticate
    def authenticate(self, request):
        # 從請求頭中取出token
        token = request.META.get('HTTP_AUTHORIZATION')
        # 如果沒有token就說明沒有登入,所以丟擲異常
        if not token:
            raise AuthenticationFailed('請先登入再操作')
        # 呼叫父類get_validated_token以此校驗token,返回值是payload段,包含使用者id
        validated_token = self.get_validated_token(token)
        # 取到id,從而得到使用者物件
        user_id = validated_token.get('user_id')
        user = NormalUser.objects.filter(pk=user_id).first()
        return user, token