JWT
目录
JWT
传统token和jwt认证的区别
-
基于传统的token认证方式
用户登录, 服务端给返回token,并将token保存在服务端 以后在访问的时候,需要携带token,服务端获取token后,再去数据库获取token校验
-
JWT
用户登录,服务端给用户返回一个token(服务端不保存) 以后访问的时候,需要携带token,在服务端获取token,在做token的校验 优势:相较于传统的token相比,在无需保存在服务端token
Jwt实现的过程
jwt的生成token格式如下,即:由 .
连接的三段字符串组成。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
生成规则
-
第一段:HEADER部分,固定包含算法和token类型,对此json进行base64url加密,这就是token的第一段。
{ "alg": "HS256", "typ": "JWT" }
-
第二段: PAYLOAD部分,包含一些数据,对此json进行base64url加密,这就是token的第二段。
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 ... }
-
第三段: SIGNATURE部分,把前两段的base密文通过
.
拼接起来,然后对其进行HS256
加密,再然后对hs256
密文进行base64url加密,最终得到token的第三段。base64url( HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret (秘钥加盐) ) )
最后将三段字符串通过 .
拼接起来就生成了jwt的token
注意:base64url加密是先做base64加密,然后再将 -
替代 +
及 _
替代 /
。
代码实现
我们可以先用pyjwt,后面用 rest_framework_jwt
实现
-
实现
import jwt import datetime from jwt import exceptions SALT = 'iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv=' def create_token(): # 构造header headers = { 'typ': 'jwt', 'alg': 'HS256' } # 构造payload payload = { 'user_id': 1, # 自定义用户ID 'username': 'wupeiqi', # 自定义用户名 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # 超时时间 } result = jwt.encode(payload=payload, key=SALT, algorithm="HS256", headers=headers).decode('utf-8') return result if __name__ == '__main__': token = create_token() print(token)
jwt校验token
一般在认证成功后,把jwt生成的token返回给用户,以后用户再次访问时候需要携带token,此时jwt需要对token进行超时
及合法性
校验。
获取token之后,会按照以下步骤进行校验:
-
将token分割成
header_segment
、payload_segment
、crypto_segment
三部分jwt_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" signing_input, crypto_segment = jwt_token.rsplit(b'.', 1) header_segment, payload_segment = signing_input.split(b'.', 1)
-
对第一部分
header_segment
进行base64url解密,得到header
-
对第二部分
payload_segment
进行base64url解密,得到payload
-
对第三部分
crypto_segment
进行base64url解密,得到signature
-
对第三部分
signature
部分数据进行合法性校验- 拼接前两段密文,即:
signing_input
- 从第一段明文中获取加密算法,默认:
HS256
- 使用 算法+盐 对
signing_input
进行加密,将得到的结果和signature
密文进行比较。
- 拼接前两段密文,即:
import jwt
import datetime
from jwt import exceptions
def get_payload(token):
"""
根据token获取payload
:param token:
:return:
"""
try:
# 从token中获取payload【不校验合法性】
# unverified_payload = jwt.decode(token, None, False)
# print(unverified_payload)
# 从token中获取payload【校验合法性】
verified_payload = jwt.decode(token, SALT, True)
return verified_payload
except exceptions.ExpiredSignatureError:
print('token已失效')
except jwt.DecodeError:
print('token认证失败')
except jwt.InvalidTokenError:
print('非法的token')
if __name__ == '__main__':
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzM1NTU1NzksInVzZXJuYW1lIjoid3VwZWlxaSIsInVzZXJfaWQiOjF9.xj-7qSts6Yg5Ui55-aUOHJS4KSaeLq5weXMui2IIEJU"
payload = get_payload(token)
jwt认证算法:签发和校验
-
签发 :根据登录请求提交来的账号,密码,设备信息签发token
1)用基本信息存储json字典,采用base64算法加密得到 头字符串 2)用关键信息存储json字典,采用base64算法加密得到 体字符串 3)用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串 账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台
-
校验:根据客户端带来的token的请求,反解出user对象
1)将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理 2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时同一设备来的 3)再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户
认证的流程开发
1)用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中
2)校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户
注:登录接口需要做 认证 + 权限 两个局部禁用
drf 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空格)
使用全套的djangorestframework-jwt(内置权限类)
# urls.py
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path('login/',obtain_jwt_token),
path('order/',views.OrderView.as_view())
]
# views.py
class OrderView(GenericAPIView):
authentication_classes = [JSONWebTokenAuthentication]
permission_classes = [IsAuthenticated] # 加上游客不能访问,不加游客能访问
def get(self, request, *args, **kwargs):
return Response('订单信息')
- 这里的登录视图rest_framework_jwt帮我们在内部做了视图,我们只需要在配置一个登录的路由去调用它就可以了
- 关于视图里面的函数做认证,我们需要配置一个内置用户访问权限限制,JSONWebTokenAuthentication和IsAuthenticated才是一套的控制登录用户访问,
- JSONWebTokenAuthentication加这个游客是可以访问的,校验规则:这里里面必须填authenticate,jwt空格传,才解析,如果不传不解析,到request里面没有东西,
控制登录接口返回的数据格式
1. 自己写登录接口
2. 用内置,控制返回的数据格式
-jwt的配置信息中有这个属性
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',
-重写jwt_response_payload_handler,配置成咱们自己的
- 用内置,控制返回的数据格式
第一步:
class OrderView(GenericAPIView):
authentication_classes = [JSONWebTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
return Response('订单信息')
第二步:
def jwt_response_payload_handler(token, user=None, request=None): # 返回什么样式,前端就是上面样式
return {
'token': token,
'msg': '登录成功',
'status': 100,
'username': user.username
}
第三步:在自己的settings配置
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER':'api.utils.ahth.jwt_response_payload_handler'
}
自定义权限类
from api.utils.ahth import MyJwtAuthentication
class OrderView2(GenericAPIView):
authentication_classes = [MyJwtAuthentication]
# permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
print(request.user)
print(1111)
return Response('商品信息')
# 第一种
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework_jwt.utils import jwt_decode_handler # 跟上面死一样的
from api.models import User
class MyJwtAuthentication(BaseAuthentication):
def authenticate(self, request):
jwt_value=request.META.get('HTTP_AUTHORIZATION')
if jwt_value:
try:
#jwt提供了通过三段token,取出payload的方法,并且有校验功能
payload=jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed('签名过期')
except jwt.InvalidTokenError:
raise AuthenticationFailed('用户非法')
except Exception as e:
# 所有异常都会走到这
raise AuthenticationFailed(str(e))
# 因为payload就是用户信息的字典
print(payload)
# return payload, jwt_value
# 需要得到user对象,
# 第一种,去数据库查
user=models.User.objects.get(pk=payload.get('user_id'))
# 第二种不查库
#user=User(id=payload.get('user_id'),username=payload.get('username'))# 生成对象里面
return user,jwt_value
# 没有值,直接抛异常
raise AuthenticationFailed('您没有携带认证信息')
# 第2种
class DrfJwtTokenAuth(JSONWebTokenAuthentication):
def authenticate(self, request):
jwt_value = request.META.get('HTTP_AUTHORIZATION') # 从请求头中取出token值
try:
payload = jwt_decode_handler(jwt_value) # 从token值中取出payload(字典)
except exceptions.ExpiredSignatureError: # 捕获异常
raise AuthenticationFailed('token已失效')
except jwt.DecodeError:
raise AuthenticationFailed('token认证失败')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法的token')
return self.authenticate_credentials(payload), jwt_value
# 从payload中取出user对象,内部是查询数据库实现的
# authenticate_credentials是JSONWebTokenAuthentication提供的方法
"""
当认证通过后,需要返回一个包含user对象的二元组,获取这个user对象的方式有两种;
第一种是通过payload中的user_id或者username查数据哭获取user对象;
第二种是通过用户类实例化一个该user对象【仅仅是一个对象,不是user用户对象】
"""
手动签发token,多方式登录
vews.py
from rest_framework.viewsets import ViewSet
from api.ser import UserModelSerializer
class LoginView(ViewSet):
def login(self,request,*args,**kwargs):
login_ser = UserModelSerializer(data=request.data,context={'request':request})
login_ser.is_valid(raise_exception=True)
token = login_ser.context.get('token')
username = login_ser.context.get('username')
return Response({'status':100,'msg':"成功",'token':token,'username':username})
ser.py
from rest_framework import serializers
from api import models
from rest_framework_jwt.utils import jwt_encode_handler
from rest_framework_jwt.utils import jwt_payload_handler # 签发token
from rest_framework.exceptions import ValidationError
class UserModelSerializer(serializers.ModelSerializer):
username = serializers.CharField() # 这里要去覆盖username。因为他是唯一字段
class Meta:
model = models.User
fields = ['username', 'password']
def validate(self, attrs):
print(self.context)
username = attrs.get('username')
password = attrs.get('password')
# 判断,username 数据不同,查询字段不一样
# 正则匹配手机
import re
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:
# 校验密码
if user.check_password(password):
# 签发token
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
self.context['token'] = token
self.context['username'] = user.username
return attrs
else:
raise ValidationError('密码错误')
else:
raise ValidationError('用户不存在')
"""
from rest_framework_jwt.utils import jwt_encode_handler
from rest_framework_jwt.utils import jwt_payload_handler # 签发token
payload = jwt_payload_handler(user) # 把user传入,得到payload
token = jwt_encode_handler(payload) 把payload 传进去得到token
"""
- payload = jwt_payload_handler(user) # 把user传入,得到payload,
- token = jwt_encode_handler(payload) 把payload 传进去得到token
- 因为username在user是一个字段,因为我们走的是post,它默认是查数据库保存,我们创建一个useraname字段覆盖,重新覆盖username字段,数据中它是unique,post,认为你保存数据,自己有校验没过
jwt的参数配置
# jwt的配置
import datetime
JWT_AUTH={
'JWT_RESPONSE_PAYLOAD_HANDLER':'app02.utils.my_jwt_response_payload_handler',
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 过期时间,手动配置
}
pyjwt的应用
视图
from app01.utils.jwt_token import create_token
class ProLoginView(APIView):
"""jwt登录认证"""
def post(self,request,*args,**kwargs):
user = request.data.get('username')
pwd = request.data.get('password')
user_obj = models.UserInfo.objects.filter(username=user,password=pwd).first()
if not user_obj:
return Response({'code':1000,'error':'用户名或密码错误'})
token = create_token({'id':user_obj.id,'name':user_obj.username})
return Response({'code': 10001, 'data': token})
from app01.exensions.auth import JwtQueryParamsAuthentication
class ProOrderView(APIView):
authentication_classes = [JwtQueryParamsAuthentication]
def get(self, request, *args, **kwargs):
print(request.user)
return Response('订单列表')
生成token
import jwt
import datetime
from django.conf import settings
def create_token(payload,timeout=1):
salt = settings.SECRET_KEY
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
# 构造payload
payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout) # 超时时间
token = jwt.encode(payload=payload, key=salt, algorithm="HS256", headers=headers).decode('utf-8')
return token
认证token
from rest_framework.authentication import BaseAuthentication
from django.conf import settings
from jwt import exceptions
from rest_framework.response import Response
import jwt
from rest_framework.exceptions import AuthenticationFailed
class JwtQueryParamsAuthentication(BaseAuthentication):
def authenticate(self, request):
token = request.query_params.get('token')
salt = settings.SECRET_KEY
try:
payload = jwt.decode(token, salt, True)
except exceptions.ExpiredSignatureError:
msg = 'token已失效'
raise AuthenticationFailed({'code':1003,"msg":msg})
except jwt.DecodeError:
msg = 'token认证失败'
raise AuthenticationFailed({'code': 1003, "msg": msg})
except jwt.InvalidTokenError:
msg = '非法的token'
raise AuthenticationFailed({'code': 1003, "msg": msg})
return (payload, token) # payload就是user,
#可以有三种返回值
# 抛出异常,后面的函数就不会执行
# return一个元组(1,2)认证通过,在视图中调用request.user,就是元组的一个值,request.auth时第二个值
# return None 在验证