JWT認證
什么是JWT
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
翻譯成人話是:JWT就是一段字符串,用來進行用戶身份認證的憑證,該字符串分成三段【頭部、載荷、簽證】
頭部header
jwt的頭部承載兩部分信息:
- 聲明類型,這里是jwt
- 聲明加密的算法 通常直接使用 HMAC SHA256
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
載荷payload
載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分
- 標准中注冊的聲明
- 公共的聲明
- 私有的聲明
標准中注冊的聲明 (建議但不強制使用) :
- iss: jwt簽發者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避時序攻擊。
公共的聲明 : 公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
私有的聲明 : 私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。
定義一個payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后將其進行base64加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
簽證signature
JWT的第三部分是一個簽證信息,這個簽證信息由三部分組成:
- header (base64后的)
- payload (base64后的)
- secret
這個部分需要base64加密后的header和base64加密后的payload使用.
連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret
組合加密,然后就構成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.
連接成一個完整的字符串,構成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
關於簽發和核驗JWT,我們可以使用Django REST framework JWT擴展來完成。
文檔網站:http://getblimp.github.io/django-rest-framework-jwt/
jwt認證原理
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": "安全碼"
}
"""
簽發:根據登錄請求提交來的 賬號 + 密碼 + 設備信息 簽發 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就能訪問登錄的用戶
- 注:登錄接口需要做 認證 + 權限 兩個局部禁用
'''
補充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)
jwt的簡單使用
官網
# http://getblimp.github.io/django-rest-framework-jwt/
安裝第三方模塊djangorestframework-jwt
# pip install djangorestframework-jwt
簡單使用
# 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空格)
Auth中使用jwt認證
路由中導入認證類
三個認證類(ObtainJSONWebToken, VerifyJSONWebToken, RefreshJSONWebToken),且都繼承一個基類(JSONWebTokenAPIView),基類又繼承了APIView
我們可以導入obtain_jwt_token、refresh_jwt_token、verify_jwt_token,這樣我們就不需要.as_view
方法了
'''
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()
'''
# 路由中配置
# 基類JSONWebTokenAPIView中設置了permission_classes = ()、authentication_classes = ()
# 因此我們設置全局也沒關系
path('login/', obtain_jwt_token),
path('show/', views.UserView.as_view()) # 用於驗證是否登錄
視圖中導入認證模塊,局部配置或者全局直接配置
# 視圖中配置
from rest_framework.views import APIView
from rest_framework_jwt.authentication import JSONWebTokenAuthentication # 認證模塊
from rest_framework.permissions import IsAuthenticated
from homework.models import User
from utils.response import UserResponse
class UserView(APIView):
# 局部配置認證模塊
# 當發送get請求時,請求頭中必須配置Authorization:JWT+空格+登錄后返回的token
# 由jwt配置文件中的'JWT_AUTH_HEADER_PREFIX': 'JWT',來控制
# 默認JWT+空格+token,內部會根據空格來切割,取出token進行后續操作
# 如果不傳,則也能訪問但是此時用戶是游客模式
authentication_classes = [JSONWebTokenAuthentication, ]
# 加上下面的代碼,就取消了游客模式,只有登錄用戶才能夠訪問
# permission_classes = [IsAuthenticated, ]
def get(self, request):
return UserResponse()
# 全局配置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'users.app_auth.JSONWebTokenAuthentication',
),
}
自定義基於jwt的權限類
新建一個py文件編寫認證類
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework.exceptions import AuthenticationFailed
import jwt
# 繼承BaseJSONWebTokenAuthentication,重寫父類的authenticate方法
class MyToken(BaseJSONWebTokenAuthentication):
def authenticate(self, request):
# 獲取token的第二部分
jwt_value = request.META.get('HTTP_AUTHORIZATION')
# 判斷get請求頭中是否攜帶token,如果沒有就是游客模式,直接返回None
if jwt_value is None:
return None
try:
# 將token數據進行decode解碼后得到payload(用戶信息字典),如果token超時或者錯誤,就會拋出異常
payload = jwt_decode_handler(jwt_value)
# token = jwt_encode_handler(payload) 獲取token
except jwt.ExpiredSignature:
raise AuthenticationFailed('認證超時')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法用戶')
except Exception as e:
raise AuthenticationFailed(str(e))
# 將用戶信息字典傳入,內部會查詢數據庫返回user對象
user = self.authenticate_credentials(payload)
# 最后返回兩個參數,一個是user對象,另一個是token(也可以是其他數據看你自己要返回啥)
return user, jwt_value
# 方法二:基於BaseAuthentication實現
class MyBaseAuthentication(BaseAuthentication):
# 重寫authenticate
def authenticate(self, request):
# 獲取token的第二部分
jwt_value = request.META.get('HTTP_AUTHORIZATION')
if not jwt_value:
# 如果沒有就拋異常
raise AuthenticationFailed('您沒有攜帶認證信息')
try:
# 將token反解成用戶信息字典
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed('認證超時')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法用戶')
except Exception as e:
raise AuthenticationFailed(str(e))
# 直接返回對象 不查庫,速度快些(只能獲取傳入的參數的值)
# user = User(id=payload.get('user_id'), username=payload.get('username'))
# 去數據庫中查找獲取user對象(能獲取的字段更多)
user = User.objects.filter(pk=payload.get('user_id')).first()
return user, jwt_value
視圖中導入自定義的認證類並局部使用
from rest_framework.views import APIView
from utils.response import UserResponse # 自己封裝的Response
from jwtdemo.auth import MyToken
class UserView(APIView):
authentication_classes = [MyToken, ]
# authentication_classes = [MyBaseAuthentication, ]
def get(self, request):
print(request.user)
return UserResponse()
jwt控制返回數據格式
# 方案一:自己寫登錄接口
# 方案二:寫一個函數,然后在配置文件中配置自己寫的函數
def jwt_response_payload_handler(token, user=None, request=None):
# 返回啥前端就能看到啥
return {
'token': token,
'message': '成功',
'status': 100,
'username': user.username
}
JWT_AUTH={
# 默認的
# 'JWT_RESPONSE_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_response_payload_handler',
# 自己寫的
'JWT_RESPONSE_PAYLOAD_HANDLER': 'jwtend.auth.jwt_response_payload_handler'
}
base64的使用
# base64編碼和解碼
#md5固定長度,不可反解
#base63 變長,可反解
#編碼(字符串,json格式字符串)
import base64
import json
dic={'name':'lqz','age':18,'sex':'男'}
dic_str=json.dumps(dic)
ret=base64.b64encode(dic_str.encode('utf-8'))
print(ret)
# 解碼
# ret是帶解碼的串
ret2=base64.b64decode(ret)
print(ret2)
手動簽發token(多方式登錄)
# 使用用戶名,手機號,郵箱,都可以登錄#
# 前端需要傳的數據格式
{
"username":"lqz/1332323223/33@qq.com",
"password":"lqz12345"
}
邏輯寫在序列化類中
序列化類的編寫
from rest_framework import serializers
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.utils import jwt_encode_handler, jwt_payload_handler
from django.db.models import Q
import re
# 多方式登錄,邏輯寫在序列化類中
class LoginModelSerializer(serializers.ModelSerializer):
# 需要重新覆蓋username
# 原因:數據庫中的username是unique,post請求時會被認為是保存數據,就會被校驗阻擋
username = serializers.CharField()
class Meta:
model = User
fields = ['username', 'password']
def validate(self, validated_data):
username = validated_data.get('username')
password = validated_data.get('password')
# 可以使用Q查詢,也可以一個一個查詢然后判斷
user = User.objects.filter(Q(username=username) | Q(mobile=username) | Q(email=username)).first()
# if re.match('^1[3-9][0-9]{9}', username):
# user = User.objects.filter(mobile=username).first()
# elif re.match('.*@.*', username):
# user = User.objects.filter(email=username).first()
# else:
# user = User.objects.filter(username=username).first()
if not(user and user.check_password(password)):
raise AuthenticationFailed('用戶名或密碼錯誤')
# 使用jwt_payload_handler方法將user對象變成用戶信息字典
payload = jwt_payload_handler(user)
# 使用jwt_encode_handler方法將用戶信息字典變成token
token = jwt_encode_handler(payload)
# 使用context來進行視圖與序列化類之間的數據傳遞,context是一個字典
self.context['token'] = token
self.context['username'] = user.username
return validated_data
視圖類的編寫
from rest_framework.viewsets import ViewSet
from utils.response import UserResponse
from jwtend import ser
class LoginViewSet(ViewSet):
def login(self, request, *args, **kwargs):
# 需要一個序列化類
# 生成序列化類對象, 可以指定context將數據傳到序列化類中
serializer = ser.LoginModelSerializer(data=request.data)
# 調用序列化對象的is_valid方法
serializer.is_valid(raise_exception=True)
# return
return UserResponse(message='登錄成功', token=serializer.context.get('token'), username=serializer.context.get('username'))
路由的編寫
# 多方式登錄,邏輯寫在序列化類中
path('login/', views.LoginViewSet.as_view(actions={'post': 'login'})),
邏輯寫在視圖類中
視圖類的編寫
from django.db.models import Q
from rest_framework.viewsets import ViewSet
from rest_framework_jwt.utils import jwt_encode_handler, jwt_payload_handler
from utils.response import UserResponse
from homework.models import User
# 多方式登錄,邏輯寫在視圖類中
class Login2ViewSet(ViewSet):
def login(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
# 使用Q查詢
user = User.objects.filter(Q(username=username) | Q(email=username) | Q(mobile=username)).first()
if not user:
return UserResponse(code=101, message='用戶名錯誤')
else:
if not user.check_password(password):
return UserResponse(code=101, message='密碼錯誤')
else:
# 使用jwt_payload_handler方法將user對象變成用戶信息字典
payload = jwt_payload_handler(user)
# 使用jwt_encode_handler方法將用戶信息字典變成token
token = jwt_encode_handler(payload)
return UserResponse(message='登錄成功', token=token, username=user.username)
路由的編寫
# 多方式登錄,邏輯寫在視圖類中
path('login2/', views.Login2ViewSet.as_view(actions={'post': 'login'})),
jwt的配置參數
# jwt的配置
import datetime
JWT_AUTH={
'JWT_ENCODE_HANDLER': 'rest_framework_jwt.utils.jwt_encode_handler',
'JWT_DECODE_HANDLER': 'rest_framework_jwt.utils.jwt_decode_handler',
'JWT_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_payload_handler',
# 將payload丟到jwt_get_user_id_from_payload_handler中會的到一個userid
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
# 可以配置非對稱加密
'JWT_PRIVATE_KEY': None,
'JWT_PUBLIC_KEY': None,
# 默認的key
'JWT_SECRET_KEY': settings.SECRET_KEY,
# 過期時間默認為seconds=300
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 過期時間,手動配置
# 刷新過期時間
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
}