JWT
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