超详细的JWT认证
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),
}