JWT介紹
在使用者註冊或登入後,我們想記錄使用者的登入狀態,或者為使用者建立身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token(本質就是token)認證機制。
構成和工作原理
JWT的構成
JWT就是一長串字串,被.分成三段,分別是頭部,載荷,簽名
header
jwt的頭部承載兩部分的資訊,頭部會進行base64的轉碼
- 聲名型別,標識這裡是jwt
- 聲名加密的演算法 通常是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的第三部分
- header (base64過後)
- payload(base64過後)
- 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