Json web token(JWT),是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)(RFC 7519),該token被設(shè)計(jì)為緊湊且安全的,特別適用于分布式站點(diǎn)的單點(diǎn)登錄(sso)場(chǎng)景,JWT的聲明一般被用來在身份提供者和服務(wù)者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其他業(yè)務(wù)邏輯所必須的聲明信息,該token也可直接被用于認(rèn)證,也可被加密
token的應(yīng)用于web方向的稱之為jwt
創(chuàng)新互聯(lián)建站主要從事做網(wǎng)站、網(wǎng)站設(shè)計(jì)、網(wǎng)頁設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)大同,十年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):18980820575
JWT就是一段字符串,由三段信息構(gòu)成的,將這三段信息文本用.鏈接一起就構(gòu)成了Jwt字符串。就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似于飛機(jī)上承載的物品),第三部分是簽證(signature)。
jwt的頭部承載兩部分信息:
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后將頭部進(jìn)行base64加密(該加密是可以對(duì)稱解密的),構(gòu)成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
荷載就是存放有效信息的地方。這個(gè)名字像是特指飛機(jī)上承載的貨品,這些有效信息包含三個(gè)部分
JWT的第三部分是一個(gè)簽證信息,這個(gè)簽證信息由三部分組成:
這個(gè)部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進(jìn)行加鹽secret組合加密,然后就構(gòu)成了jwt的第三部分。
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.連接成一個(gè)完整的字符串,構(gòu)成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服務(wù)器端的,jwt的簽發(fā)生成也是在服務(wù)器端的,secret就是用來進(jìn)行jwt的簽發(fā)和jwt的驗(yàn)證,所以,它就是你服務(wù)端的私鑰,在任何場(chǎng)景都不應(yīng)該流露出去。一旦客戶端得知這個(gè)secret, 那就意味著客戶端是可以自我簽發(fā)jwt了。
1)jwt分三段式:頭.體.簽名 (head.payload.sgin)
2)頭和體是可逆加密,讓服務(wù)器可以反解出user對(duì)象;簽名是不可逆加密,保證整個(gè)token的安全性的
3)頭體簽名三部分,都是采用json格式的字符串,進(jìn)行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)頭中的內(nèi)容是基本信息:公司信息、項(xiàng)目組信息、token采用的加密方式信息
{
"company": "公司信息",
...
}
5)體中的內(nèi)容是關(guān)鍵信息:用戶主鍵、用戶名、簽發(fā)時(shí)客戶端信息(設(shè)備號(hào)、地址)、過期時(shí)間
{
"user_id": 1,
...
}
6)簽名中的內(nèi)容時(shí)安全信息:頭的加密結(jié)果 + 體的加密結(jié)果 + 服務(wù)器不對(duì)外公開的安全碼 進(jìn)行md5加密
{
"head": "頭的加密字符串",
"payload": "體的加密字符串",
"secret_key": "安全碼"
}
1)將token按 . 拆分為三段字符串,第一段 頭加密字符串 一般不需要做任何處理
2)第二段 體加密字符串,要反解出用戶主鍵,通過主鍵從User表中就能得到登錄用戶,過期時(shí)間和設(shè)備信息都是安全信息,確保token沒過期,且時(shí)同一設(shè)備來的
3)再用 第一段 + 第二段 + 服務(wù)器安全碼 不可逆md5加密,與第三段 簽名字符串 進(jìn)行碰撞校驗(yàn),通過后才能代表第二段校驗(yàn)得到的user對(duì)象就是合法的登錄用戶
使用的是django_rest_framework里面的jwt模塊
1)用賬號(hào)密碼訪問登錄接口,登錄接口邏輯中調(diào)用 簽發(fā)token 算法,得到token,返回給客戶端,客戶端自己存到cookies中
2)校驗(yàn)token的算法應(yīng)該寫在認(rèn)證類中(在認(rèn)證類中調(diào)用),全局配置給認(rèn)證組件,所有視圖類請(qǐng)求,都會(huì)進(jìn)行認(rèn)證校驗(yàn),所以請(qǐng)求帶了token,就會(huì)反解出user對(duì)象,在視圖類中用request.user就能訪問登錄的用戶
注:登錄接口需要做 認(rèn)證 + 權(quán)限 兩個(gè)局部禁用
pip3 install djangorestframework-jwt
1)遷移表,表需要繼承auth里內(nèi)置的user表,因?yàn)樗J(rèn)使用auth的user表簽發(fā)token
2)創(chuàng)建超級(jí)用戶(auth的user表中要右記錄)
3)不需要寫登錄接口了,如果是使用auth的user表作為用戶表,它可以快速簽發(fā)
4)簽發(fā)(登錄):只需要在路由中配置(因?yàn)樗鼛驮蹅儗懞玫卿浗涌诹耍?/p>
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path('login/', obtain_jwt_token),
]
# 返回結(jié)果
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Inp4ciIsImV4cCI6MTY2NTU3MjU4OSwiZW1haWwiOiJ6eHJAMTYzLmNvbSJ9.0jDd56Jk04-SdZ4AchLHkcfLECS1RvhFwAQ8VoNKiMM"
}
當(dāng)?shù)卿?27.0.0.1:8000/login/時(shí)采用post請(qǐng)求,然后攜帶登錄的用戶名和密碼的信息,服務(wù)器會(huì)自動(dòng)響應(yīng),返回一個(gè)token給前端,效果圖如下:
內(nèi)置認(rèn)證類的使用
在需要添加認(rèn)證的視圖函數(shù)接口上添加以下配置:
from rest_framework.views import APIView
from rest_framework.response import Response
# Create your views here.
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class BookView(APIView):
authentication_classes = [JSONWebTokenAuthentication,]
permission_classes = [IsAuthenticated, ]
def get(self,request):
return Response('ok')
def post(self,request):
return Response('ok11')
添加之后,在訪問該視圖時(shí),token需要放在請(qǐng)求頭中,并且格式為Authorization:jwt token串
Authorization:jwt token串
# 例如:
Authorization:jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Inp4ciIsImV4cCI6MTY2NTU3MzMzMCwiZW1haWwiOiJ6eHJAMTYzLmNvbSJ9.DAX1wLhHYnpGwVzGvMnSFzUudkPgXsYvlrfk-XFdSAc
注意:上述請(qǐng)求頭的key必須位Authorization,值為jwt+空格+token串,如果想要修改jwt可以在配置文件中進(jìn)行如下配置:
自定義auth認(rèn)證類的使用
在一個(gè)py文件夾里定義如下一個(gè)認(rèn)證類
from rest_framework_jwt.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.authentication import jwt_decode_handler
from app01.models import UserInfo
# 寫一個(gè)類繼承自BaseAuthentication,重寫authenticate方法
class JWTAuthentication(BaseAuthentication):
def authenticate(self, request):
# token在請(qǐng)求頭的值為token-->HTTP_TOKEN token在請(qǐng)求頭的值為Authorization時(shí)--->HTTP_AUTHORIZATION
# print(request.META)
jwt_value = request.META.get('HTTP_TOKEN')
# 驗(yàn)證token是否合法,jwt模塊下一定有個(gè)驗(yàn)證token的函數(shù)
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed('token過期了')
except jwt.DecodeError:
raise AuthenticationFailed('token解碼失敗')
except jwt.InvalidTokenError:
raise AuthenticationFailed('認(rèn)證失敗')
# 執(zhí)行到這,說明token合法,payload可以使用
user = payload.get('username')
user = UserInfo.objects.filter(username=user).first() # 每次都要查數(shù)據(jù)庫,效率不太好
return (user, jwt_value) #user對(duì)應(yīng)了request.user jwt_value對(duì)應(yīng)了request.auth
# return (payload,jwt_value)
在需要認(rèn)證的視圖上進(jìn)行局部配置自己的寫的認(rèn)證類
class BookView(APIView):
authentication_classes = [JWTAuthentication,]
permission_classes = [IsAuthenticated, ]
def post(self,request):
print(request.user,request.auth)
return Response('ok11')
登錄成功后,前端看到的格式,太固定了,只有token,我們想做成:
{code:100,msg:'登錄成功',token:adfasdfasdf}
固定寫法:
寫一個(gè)函數(shù),函數(shù)返回什么,前端就看到什么,然后需要在配置文件中配置一下自己寫的這個(gè)函數(shù)
寫一個(gè)函數(shù)
def jwt_response_payload_handler(token, user=None, request=None):
return {
'code': 100,
'msg': '登錄成功',
'username': user.username,
'token': token
}
將函數(shù)配置在配置文件中
# djangorestframework-jwt的配置,這個(gè)配置了以后優(yōu)先使用這個(gè)
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.response.jwt_response_payload_handler',
}
使用用戶名,手機(jī)號(hào),郵箱,都可以登錄
前端需要傳的數(shù)據(jù)格式
{
"username":"lqz//33@qq.com",
"password":"lqz"
}
視圖
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin, ViewSet
from app02 import ser
class Login2View(ViewSet):
def login(self, request, *args, **kwargs):
# 1 需要 有個(gè)序列化的類
login_ser = ser.LoginModelSerializer(data=request.data,context={'request':request})
# 2 生成序列化類對(duì)象
# 3 調(diào)用序列號(hào)對(duì)象的is_validad
login_ser.is_valid(raise_exception=True)
token=login_ser.context.get('token')
# 4 return
return Response({'status':100,'msg':'登錄成功','token':token,'username':login_ser.context.get('username')})
序列化類
from rest_framework import serializers
from api import models
import re
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler,jwt_payload_handler
class LoginModelSerializer(serializers.ModelSerializer):
username=serializers.CharField() # 重新覆蓋username字段,數(shù)據(jù)中它是unique,post,認(rèn)為你保存數(shù)據(jù),自己有校驗(yàn)沒過
class Meta:
model=models.User
fields=['username','password']
def validate(self, attrs):
print(self.context)
# 在這寫邏輯
username=attrs.get('username') # 用戶名有三種方式
password=attrs.get('password')
# 通過判斷,username數(shù)據(jù)不同,查詢字段不一樣
# 正則匹配,如果是手機(jī)號(hào)
if re.match('^1[3-9][0-9]{9}$',username):
user=models.User.objects.filter(mobile=username).first()
elif re.match('^.+@.+$',username):# 郵箱
user=models.User.objects.filter(email=username).first()
else:
user=models.User.objects.filter(username=username).first()
if user: # 存在用戶
# 校驗(yàn)密碼,因?yàn)槭敲芪?,要用check_password
if user.check_password(password):
# 簽發(fā)token
payload = jwt_payload_handler(user) # 把user傳入,得到payload
token = jwt_encode_handler(payload) # 把payload傳入,得到token
self.context['token']=token
self.context['username']=user.username
return attrs
else:
raise ValidationError('密碼錯(cuò)誤')
else:
raise ValidationError('用戶不存在')
# jwt的配置
import datetime
JWT_AUTH={
'JWT_RESPONSE_PAYLOAD_HANDLER':'app02.utils.my_jwt_response_payload_handler',
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 過期時(shí)間,手動(dòng)配置
}