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现在有三个功能:查看图书和作者(通过角色添加权限)、查看出版社(通过用户添加权限):