1.drf-jwt源码执行流程
1.1 签发(登录)
1.代码:
urls.py:
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path('login/',obtain_jwt_token),
]
2.我们点进obtain_jwt_token源码:
drf/views.py:
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()
3.login需要提交用户名和密码,所以是post请求,我们需要在其父类中找到post方法:
ObtainJSONWebToken>>>JSONWebTokenAPIView,在JSONWebTokenAPIView中找到了post方法:
def post(self, request, *args, **kwargs):
# serializer是序列化类的对象
serializer = self.get_serializer(data=request.data)
# 校验,如果校验通过:
if serializer.is_valid():
# 拿到user和token
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
# 拿到返回格式,之前我们自定义过token的返回格式。
"""
当我们点击方法:jwt_response_payload_handler(token, user, request),跳转到了rest_framework_jwt:jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER。说明JWT_RESPONSE_PAYLOAD_HANDLER需要在配置中指定返回格式。因此我们在设置中指定:JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.jwt_response.jwt_response',
}。所以返回格式才能按照我们指定的格式返回。
"""
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
return response
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
"""
执行if serializer.is_valid():这句话时就会执行序列化类中的代码,但是如何得到user和token,在序列化类的全局钩子中寻找答案:
"""
4.还是回到drf/views.py中:
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()
ObtainJSONWebToken后面跟了as_view()说明这是视图类,点进去:
class ObtainJSONWebToken(JSONWebTokenAPIView):
serializer_class = JSONWebTokenSerializer
说明JSONWebTokenSerializer就是序列化类。
5.JSONWebTokenSerializer代码:
class JSONWebTokenSerializer(Serializer):
# 这是一个全局钩子,因为上面没有单个字段的校验规则,所以此时的addr就是{'username':'max','password':'max123'}
def validate(self, attrs):
credentials = {
# 这一步还是拿到了用户名,只不过是绕了一下
self.username_field: attrs.get(self.username_field),
# 拿到密码
'password': attrs.get('password')
}
# 必须用户名和密码都幼值才成立
if all(credentials.values()):
# auth模块中的,如果用户存在会拿到用户对象
user = authenticate(**credentials)
if user:
# 如果能拿到用户对象,并且用户被锁(is_active默认是1,如果用户被锁则是0)
if not user.is_active:
# 如果被锁则提示disabled
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
# 通过用户对象拿到荷载
payload = jwt_payload_handler(user)
# 通过payload生成token
return {
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
1.2 认证 (认证类)
1.认证类需要从JSONWebTokenAuthentication中找到authenticate方法。在其父类中找到了authenticate方法。
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
def authenticate(self, request):
"""
Returns a two-tuple of `User` and token if a valid signature has been
"""
# jwt_value就是token字符串
jwt_value = self.get_jwt_value(request)
# 如果token值没传,直接返回None
if jwt_value is None:
return None
try:
# payload是一个字典:{'user_id': 1, 'username': 'max', 'exp': 1676113688, 'email': ''}
payload = jwt_decode_handler(jwt_value)
# 还有几种可能拿不到,分别是:篡改token、token过期了、未知错误
except jwt.ExpiredSignature:
msg = _('Signature has expired.')
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = _('Error decoding signature.')
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed()
# 如果没有错误顺利能拿到用户对象
user = self.authenticate_credentials(payload)
# 返回当前登录用户,token
return (user, jwt_value)
2.接下来我们来看刚才的方法get_jwt_value(request)是如何拿到token的,该方法在类JSONWebTokenAuthentication中:
def get_jwt_value(self, request):
auth = get_authorization_header(request).split()
3.我们需要找到方法get_authorization_header(request),在BaseAuthentication中找到了该方法:
def get_authorization_header(request):
# request.META可以拿到get请求头当中的值,结果是个字典。在数据发送到后端时键都变成了'HTTP_前端传入的键',如果拿不到就拿一个空字符串。此时的auth是jwt dfjkdlsjf...
auth = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(auth, str):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
# 转码然后返回
return auth
4.继续回到get_jwt_value(request)方法:
def get_jwt_value(self, request):
# auth是个被分割列表:[jwt,dfjkdlsjf]
auth = get_authorization_header(request).split()
# JWT_AUTH_HEADER_PREFIX就是'JWT',转化成小写'jwt'
auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()
if not auth:
# 如果请求头没带,就去cookie中取
if api_settings.JWT_AUTH_COOKIE:
return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
return None
# 如果列表索引0不为jwt返回None
if smart_text(auth[0].lower()) != auth_header_prefix:
return None
if len(auth) == 1:
msg = _('Invalid Authorization header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid Authorization header. Credentials string '
'should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
# 返回列表索引1,也就是token
return auth[1]
2.自定义用户表签发和认证
2.1 签发
views.py:
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from .models import Userinfo
from rest_framework_jwt.settings import api_settings
# 生成荷载的方法,我们直接调用drf的
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
# 生成token的方法,我们也调用drf的
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
class UserView(ViewSet):
@action(methods=['POST'],detail=False)
def login(self,request,*args,**kwargs):
username = request.data.get('username')
password = request.data.get('password')
user = Userinfo.objects.filter(username=username,password=password).first()
if user:
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return Response({'code':100,'msg':'登录成功','token':token})
else:
return Response({'code':101,'msg':'用户名或密码错误'})
urls.py:
router = SimpleRouter()
router.register('user', UserView, 'user') # 此时路由:http://127.0.0.1:8000/api/v1/user/login/
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include(router.urls))
]
通过以上步骤,我们可以自定义出功颁布token:
2.2 认证
新建一个认证类authentication.py,在其中写认证类的代码:
authentication.py:
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.settings import api_settings
import jwt
from rest_framework.exceptions import AuthenticationFailed
from .models import Userinfo
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
class JsonWebTokenAuthentication(BaseAuthentication):
def authenticate(self, request):
token = request.META.get('HTTP_TOKEN') # 前端的格式可以自定义,取的时候在前面加上HTTP_就好,并且键要大写
if token:
try:
# jwt_decode_handler()方法仅仅是通过token找到payload,内部并没有切割字符串的方法 get_jwt_value(),所以前端在传的时候不需要加jwt和空格。
payload = jwt_decode_handler(
token)
print(payload) # :{'user_id': 1, 'username': 'max', 'exp': 1676113688, 'email': ''}
user = Userinfo.objects.filter(pk=payload.get('user_id')).first()
return user, token
except jwt.ExpiredSignature:
raise AuthenticationFailed('token过期')
except jwt.DecodeError:
raise AuthenticationFailed('token认证失败')
except jwt.InvalidTokenError:
raise AuthenticationFailed('token无效')
except Exception as e:
raise AuthenticationFailed('未知异常')
raise AuthenticationFailed('token没有传 认证失败')
views.py:
class BookView(ModelViewSet):
# 手写jwt认证只需要写认证类不用写权限类
authentication_classes = [JsonWebTokenAuthentication]
def list(self, request, *args, **kwargs):
return Response('success')
3.auth_user表密码加密
3.1 手动定义类似token的加密方式:
1.token的加密方式:token由三段构成,第一段声明加密算法和类型,第二段存放有效信息的地方:过期时间、签发时间、用户id、用户名等。第三段是加密后的header和base64加密后的payload。
2.我们也可以定义一中类似token的加密方式:改密码分为三段,用两个$连接起来,第一段是密码加密后的密文,第二段是随机生成的盐(不加密),第三段是加密后的原密码和盐连接在一起(中间不加符号),在通过md5加密。
3.代码:
views.py:
import uuid
import hashlib
def register_hash(request):
"""
password:原密码
res:原密码加密之后的密文
salt:随机生成的盐(不加密)
pwd1:res$salt
pwd_part3:password+salt
res2:给pwd_part3加密之后的密文
pwd2:最终密码:pwd1+res2
"""
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
# print(password) # 123
md51 = hashlib.md5()
md51.update(password.encode('utf8'))
res = md51.hexdigest()
# print('res',res) # 202cb962ac59075b964b07152d234b70
# 随机生成一个盐(不加密)
salt = str(uuid.uuid4())
print('salt',salt)
# 将加密的原密码和不加密的盐组合起来,组成密码的前两部分
pwd1 = res + '$' + salt # 202cb962ac59075b964b07152d234b70$44590a73-2602-4f96-a718-972d83fb7ae6
# 将不加密的密码和盐组合起来,组成明文
pwd_part3 = res + salt
md52 = hashlib.md5()
md52.update(pwd_part3.encode('utf8'))
# 原密码和盐组成的明文加密,组成密码的第三部分
res2 = md52.hexdigest()
# 最终的密码
pwd2 = pwd1 + '$' + res2
print(pwd2)
User_hash.objects.create(username=username,hash_pwd=pwd2)
return HttpResponse('注册成功')
return render(request, 'pwd.html', locals())
def login_view(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
md51 = hashlib.md5()
md51.update(password.encode('utf8'))
res = md51.hexdigest()
user_obj = User_hash.objects.filter(username=username).first()
if not user_obj:
return HttpResponse('用户未注册')
if not res == user_obj.hash_pwd.split('$')[0]:
return HttpResponse('密码错误')
salt = user_obj.hash_pwd.split('$')[1]
pwd_part3 = res + salt
md52 = hashlib.md5()
md52.update(pwd_part3.encode('utf8'))
res2 = md52.hexdigest()
if not res2 == user_obj.hash_pwd.split('$')[2]:
return HttpResponse('密码错误')
return HttpResponse('登陆成功')
return render(request,'login.html',locals())
urls.py:
urlpatterns = [
path('register/',views.register_hash),
path('login1/',views.login_view)
]
3.2 利用django自带的方法make_password()和check_password()来编写登录注册
1.make_password()只有一个参数,就是原密码。返回值是加密后的密码,也是django的auth_user表中的用户密码的加密方式:
from django.contrib.auth.hashers import make_password, check_password
from .models import User1
# 用djanngo的make_password方法注册
def register2(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
# 密码加密:
pwd = make_password(password)
User1.objects.create(username=username,password=pwd)
return HttpResponse('注册成功')
return render(request,'register2.html',locals())
2.check_password()方法用来校验密码,里面有两个参数,第一个是明文密码,第二个参数是密文密码,如果这两个密码匹配那么结果是True,不匹配返回结果是False。
# 用django的check_password方法登陆
def login2(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
real_pwd = User1.objects.filter(username=username).first().password
is_correct = check_password(password,real_pwd)
if is_correct:
return HttpResponse('登陆成功')
return render(request,'login2.html',locals())
"""
如果超级管理员密码忘记了,可以再创建一个超级管理员,然后将新创建的管理员密码(密文)复制到前一个超级管理员的密码处,这两个管理员就会使用同一个密码。
"""
4.simpleui使用
1.之前公司里,做项目,要使用权限,要快速搭建后台管理,使用djagno的admin直接搭建,django的admin界面不好。所以采用第三方软件。
2.第三方的美化:
xadmin:作者弃坑了,bootstrap+jq
simpleui: vue,界面更好看
3.现在阶段,一般前后端分离比较多:django+vue
4.1 使用步骤
1.安装:pip install simpleui
2.在app中注册
要注册在最上面
INSTALLED_APPS = [
'simpleui'
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01',
'rest_framework',
]
然后当我们登录到admin后台管理就变成了这样:
3.然后我们在models.py中构造以下几张表,并且在admin.py中注册:
models.py:
class Book(models.Model):
nid = models.AutoField(primary_key=True)
name = models.CharField(max_length=32)
price = models.DecimalField(max_digits=5, decimal_places=2)
publish_date = models.DateField()
publish = models.ForeignKey(to='Publish',to_field='nid',on_delete=models.CASCADE)
authors=models.ManyToManyField(to='Author')
def __str__(self):
return self.name
class Author(models.Model):
nid = models.AutoField(primary_key=True)
name = models.CharField(max_length=32)
age = models.IntegerField()
author_detail = models.OneToOneField(to='AuthorDetail',to_field='nid',unique=True,on_delete=models.CASCADE)
class AuthorDetail(models.Model):
nid = models.AutoField(primary_key=True)
telephone = models.BigIntegerField()
birthday = models.DateField()
addr = models.CharField(max_length=64)
class Publish(models.Model):
nid = models.AutoField(primary_key=True)
name = models.CharField(max_length=32)
city = models.CharField(max_length=32)
email = models.EmailField()
admin.py:
from .models import Book,Publish,AuthorDetail,Author
admin.site.register(Book)
admin.site.register(Publish)
admin.site.register(AuthorDetail)
admin.site.register(Author)
然后我们就可以在admin后台管理页看到这几张表:
4.在apps.py中加入verbose_name = '图书管理系统',就可以将左侧列表中的app名改成自定义的名字:
class App01Config(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app01'
verbose_name = '图书管理系统'
5.在models.py中每张表下面加入:
class Meta:
verbose_name_plural = '作者表'
在后台管理就可以将表名显示成中文:
然后再数据库加一些数据(可以在admin后台管理加,也可以在pycharm中加),添加好之后可以直接点进去修改,这就完成了对一个图书管理系统增删改查的创建。
"""
DataTimeField字段刚开始默认是英文,如果我们想要把它设置成中文,需要在settings.py中设置:LANGUAGE_CODE = 'zh-hans'。
"""
6.当我们在admin.py中注册好之后,我们在页面上只能看到书名。注册还有一种方式,在admin.py中写一个类,定义哪张表选择显示的字段就继承哪张表,用list_play=('字段名')来定义显示的字段名,但是不能上传多对多的外键字段:
7.还可以在页面上增加按钮:在刚才定义的BookAdmin中继续加内容:
actions = ['custom_button'] # custom_button不能更改
def custom_button(self, request, queryset):
print(queryset) # queryset就是选中对象的queryset,可以额外做一些操作
custom_button.short_description = '额外操作' # 按钮的中文名
custom_button.type = 'success' # 设置按钮颜色
8.侧边栏设置,需要在settings.py中进行如下设置:
SIMPLEUI_CONFIG = {
'system_keep': False,
'menu_display': ['图书管理', '权限认证', '外链'], # 开启排序和过滤功能, 不填此字段为默认排序和全部显示, 空列表[] 为全部不显示.
'dynamic': True, # 设置是否开启动态菜单, 默认为False. 如果开启, 则会在每次用户登陆时动态展示菜单内容
'menus': [
# name要和menu_display中注册的名字保持一致
{
'name': '图书管理',
'app': 'app01',
'icon': 'fas fa-code',
# models继续往下写下面的子目
'models': [
{
'name': '图书',
'icon': 'fa fa-user',
'url': '/admin/app01/book/'
},
#url只能是自己在urls.py中配置的路由或者是自动生成的路由
{
'name': '出版社',
'icon': 'fa fa-user',
'url': 'app01/publish/'
},
{
'name': '作者',
'icon': 'fa fa-user',
'url': 'app01/author/'
},
{
'name': '作者详情',
'icon': 'fa fa-user',
'url': 'app01/authordetail/'
},
]
},
{
'app': 'auth',
'name': '权限认证',
'icon': 'fas fa-user-shield', # 图标
'models': [
{
'name': '用户',
'icon': 'fa fa-user',
'url': 'auth/user/'
},
{
'name': '组',
'icon': 'fa fa-user',
'url': 'auth/group/'
},
]
},
{
'name': '外链',
'icon': 'fa fa-file',
'models': [
{
'name': 'Baidu',
'icon': 'far fa-surprise',
# 第三级菜单 ,
'models': [
{
'name': '爱奇艺',
'url': 'https://www.iqiyi.com/dianshiju/'
# 第四级就不支持了,element只支持了3级
}, {
'name': '百度问答',
'icon': 'far fa-surprise',
'url': 'https://zhidao.baidu.com/'
}
]
},
# 我们自己定义的页面也可以直接写路由:
{
'name': '大屏展示',
'url': '/show/',
'icon': 'fab fa-github'
}]
}
]
}
9.其他配置项:
SIMPLEUI_LOGIN_PARTICLES = False #登录页面动态效果
SIMPLEUI_LOGO = 'https://avatars2.githubusercontent.com/u/13655483?s=60&v=4'#图标替换
SIMPLEUI_HOME_INFO = False #取消首页右侧github提示
SIMPLEUI_HOME_QUICK = False #快捷操作
SIMPLEUI_HOME_ACTION = False # 动作
5.权限控制
5.1 互联网项目:
alc:访问控制列表,权限放在列表中
用户表:存储用户信息,和权限表是一对多的关系
权限表:每个用户拥有的权限
比如:
权限列表:[发视频,发评论,开直播]
max拥有的权限:[发视频,发评论,开直播]
jerry拥有的权限:[发视频]
5.2 公司内部项目(python写公司内部项目居多):
1.rbac:是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
2表关系:
用户表:用户和角色是多对多关系(一个用户可以对应多个角色)
角色表:类似于公司中的岗位
权限表:用户表不直接和用户表建立联系,而是和角色表建立联系(某个用户成为了某个角色之后才拥有某项权限)。角色表和权限表是多对多关系。
所以描述以上三者关系需要建立5张表:
用户表、角色表、权限表、用户角色表、角色权限表
3.用户和权限不直接建立联系是为了简化流程方便管理,但是也有特殊情况:比如公司人资想要获取拉取代码的权限,但是开发角色拥有的权限不仅仅是拉取代码而且还能操作代码。
如果将开发的角色赋给人资就会导致人资的权限过大。所以角色和权限直接监理联系,产生第6张表:角色权限中间表。
4.以图书管理系统为例,目前设置2个用户,一个是root(超级管理员),一个是max(普通用户)。目前想要设置max的权限为查看书籍列表和作者列表,需要首先创建一个组(角色),该组中规定了查看书籍列表和作者列表的权限。
在进入到用户设置列表中:首先取消该用户超级管理员身份(公司内超级管理员数量很有限)。
再登陆用户max,发现系统中只有作者和图书两个选项,并且只能查看:
5.管理员也可以直接设置用户和权限的对应关系:
6.在表中权限、组以及它们的对应关系都在以下6张表中:
auth_user:用户表
auth_group:角色表,组表
auth_permission:权限表
auth_user_groups:用户和角色中间表
auth_group_permissions:角色和权限中间表
auth_user_user_permissions:用户和权限中间表
7.用管理员给用户max通过用户和权限对应关系给max添加了一个权限(不通过角色),该功能也可以叠加到max的权限中,用户max现在有三个功能:查看图书和作者(通过角色添加权限)、查看出版社(通过用户添加权限):