drf-jwt源码执行流程(了解)、自定义用户表签发和认证、权限控制(acl,rbac)、额外知识点补充
drf-jwt源码执行流程(了解)、自定义用户表签发和认证、权限控制(acl,rbac)、额外知识点补充
drf-jwt源码执行流程(了解)
-
签发(登录)源码解析
# 登录接口,路由匹配成功,执行obtain_jwt_token---》post请求---》ObtainJSONWebToken的post方法 path('login/', obtain_jwt_token), # ObtainJSONWebToken的post方法 继承APIView def post(self, request, *args, **kwargs): # 实例化得到序列化类 serializer = self.get_serializer(data=request.data) # 做校验:字段自己,局部钩子,全局钩子 if serializer.is_valid(): # user:当前登录用户 user = serializer.object.get('user') or request.user # 签发的token token = serializer.object.get('token') # 构造返回格式 response_data = jwt_response_payload_handler(token, user, request) response = Response(response_data) if api_settings.JWT_AUTH_COOKIE: expiration = (datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA) response.set_cookie(api_settings.JWT_AUTH_COOKIE, token, expires=expiration, httponly=True) #最终返回了咱们定制的返回格式 return response return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # 如何得到user,如何签发的token----》在序列化类的全局钩子中得到的user和签发的token -JSONWebTokenSerializer---全局钩子---validate #前端传入,校验过后的数据---》{"username":"lqz","password":"lqz1e2345"} def validate(self, attrs): credentials = { 'username':attrs.get('username') 'password': attrs.get('password') } if all(credentials.values()): # auth 模块,authenticate 可以传入用户名,密码如果用户存在,就返回用户对象,如果不存就是None # 正确的用户 user = authenticate(**credentials) if user: # 校验用户是否是活跃用户,如果禁用了,不能登录成功 if not user.is_active: msg = _('User account is disabled.') raise serializers.ValidationError(msg) # 荷载----》通过user得到荷载 {id,name,email,exp} payload = jwt_payload_handler(user) return { # jwt_encode_handler通过荷载得到token串 '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 通过user得到荷载:payload = jwt_payload_handler(user) 2 通过荷载签发token:jwt_encode_handler(payload)
2 认证(认证类)
# JSONWebTokenAuthentication---->父类BaseJSONWebTokenAuthentication----》authenticate方法 def authenticate(self, request): # 前端带在请求头中的token 值 jwt_value = self.get_jwt_value(request) # 如果没有携带token,就不校验了 if jwt_value is None: return None try: # jwt_value就是token # 通过token,得到荷载,中途会出错 # 出错的原因: -篡改token -过期了 -未知错误 payload = jwt_decode_handler(jwt_value) 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() # 如果能顺利解开,没有被异常捕获,说明token是可以信任的 # payload就可以使用,通过payload得到当前登录用户 user = self.authenticate_credentials(payload) # 返回当前登录用户,token return (user, jwt_value) def get_jwt_value(self, request): # 拿到了前端请求头中传入的 jwt dasdfasdfasdfa # auth=[jwt,asdfasdfasdf] auth = get_authorization_header(request).split() # '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 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) return auth[1] # 认证类配置了,如果不传jwt,不会校验,一定配合权限类使用
自定义用户表签发和认证
1签发
路由代码: from django.contrib import admin from django.urls import path from rest_framework_jwt.views import obtain_jwt_token from rest_framework.routers import SimpleRouter from app01.views import UserView router = SimpleRouter() router.register('user',UserView,'user') urlpatterns = [ path('admin/', admin.site.urls), path('login/', obtain_jwt_token), ] urlpatterns += router.urls
视图代码: from django.shortcuts import render from rest_framework_jwt.settings import api_settings jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER from .models import UserInfo from rest_framework.decorators import action from rest_framework.viewsets import ViewSet from rest_framework.response import Response 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: # 登录成功,签发token # 通过user得到payload payload = jwt_payload_handler(user) # 通过payload得到token token = jwt_encode_handler(payload) return Response({'code':1000,'msg':'登录成功','token':token}) else: return Response({'code':1001,'msg':'用户名或者密码不正确'}) # 在写错误信息提示的时候,msg里面最好写用户名或者密码不正确,这样别有居心的人也不会通过提示破解用户密码了
2 认证
路由:
from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
from rest_framework.routers import SimpleRouter
from app01.views import UserView,TestView
router = SimpleRouter()
# http://127.0.0.1:8000/user/login/
router.register('user',UserView,'user')
# http://127.0.0.1:8000/test/test/
router.register('test',TestView,'test')
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', obtain_jwt_token),
]
urlpatterns += router.urls
视图类:
# 写一个测试类
class TestView(ViewSet):
# 加了这个就不用加权限了
authentication_classes = [JsonWebTokenAuthentication]
@action(methods=['GET'],detail=False)
def test(self,request):
return Response('ok')
# authentication.py:
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt
from .models import UserInfo
from rest_framework_jwt.settings import api_settings
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
class JsonWebTokenAuthentication(BaseAuthentication):
def authenticate(self, request):
# 取出token---->然后放在请求头中
token = request.META.get('HTTP_TOKEN')
if token:
try:
payload = jwt_decode_handler(token)
# 得到当前登录用户
user = UserInfo.objects.get(pk=payload.get('user_id'))
# 只要访问一次需要登录的接口,就会去UserInfo中查看用户一次
# user = UserInfo(id=payload.get('user_id'),username=payload.get('username'))
# 字典形式优化
user = {'id':payload.get('user_id')}
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没有传,认证失败')
配置:
import datetime
JWT_AUTH = {
# 把过期时间改长一点
'JWT_EXPIRATION_DELTA':datetime.timedelta(days=7)
}
权限控制(acl,rbac)
1.什么是rbac
RBAC模型(Role-Based Access Control:基于角色的访问控制)模型是20世纪90年代研究出来的一种新模型,但其实在20世纪70年代的多用户计算时期,这种思想就已经被提出来,直到20世纪90年代中后期,RBAC才在研究团体中得到一些重视,并先后提出了许多类型的RBAC模型。其中以美国George Mason大学信息安全技术实验室(LIST)提出的RBAC96模型最具有代表,并得到了普遍的公认。
RBAC认为权限授权的过程可以抽象地概括为:Who是否可以对What进行How的访问操作,并对这个逻辑表达式进行判断是否为True的求解过程,也即是将权限问题转换为What、How的问题,Who、What、How构成了访问权限三元组。
2.RBAC0、RBAC1、RBAC2、RBAC3简单介绍
RBAC0:是RBAC的核心思想。
RBAC1:是把RBAC的角色分层模型。
RBAC2:增加了RBAC的约束模型。
RBAC3:其实是RBAC2 + RBAC1。
3.RBAC的组成
RBAC模型里面,有三个基础组成部分:用户,角色和权限
RBAC通过定义角色的权限,并对用户授予某个角色从而控制用户的权限,实现了用户和权限的逻辑分离(区别于ACL模型)
User(用户):每个角色都有唯一的uid识别
Role(角色):不同角色具有不同的权限
Permission(权限):访问权限
用户-角色映射:用户和角色之间是多对多的关系
角色-权限映射:角色和权限之间是多对多的关系
六张表:
用户表
角色表
权限表
用户和角色关联表
角色和权限关联表
用户和权限的多对多关系表
4.额外知识:
django的后台管理admin就自带了rbac的权限,通过auth模块实现的,比普通rbac更高级一些。之前很多公司写后台管理使用dajngo,使用django的admin二次开发,不用写权限了,快速加功能即可
5.ACL、RBAC、ABAC(PBAC,CBAC)权限控制的介绍
ACL(Access Control List,访问控制列表)
将用户或组等使用者直接与对象的权限对接。
用户表,权限表,中间 给用户授予某些权限即可
RBAC(Role-Based Access Control,基于角色的访问控制)
将用户与角色对接,然后角色与对象的权限对接
RBAC+ACL django,公司用的比较多啊
ABAC(Attribute-Based Access Control,基于属性的访问控制)
ABAC(Attribute-Based Access Control,基于属性的访问控制), 又称为 PBAC(Policy-Based Access Control,基于策略的访问控制) 或者 CBAC(Claims-Based Access Control,基于声明的访问控制)
6.Casbin:权限控制
使用地址:https://casbin.org/docs/zh-CN/get-started
额外知识点补充
1.django的auth,user表,密码都是加密的,即便是相同的密码,密文都不一样
# 每次加密的时候,都会随机生成一个盐,然后就将盐拼接到加密后的串中
eg: pbkdf2_sha256$260000$B9ZRmPFpWb3H4kdDDmgYA9$CM3Q/ZfYyXzxwvjZ+HOcdovaJS7681kDsW77hr5fo5o=
明文:lqz12345
密文:pbkdf2_sha256$260000$
盐:B9ZRmPFpWb3H4kdDDmgYA9
2.自定义用户表,生成密码密文
用到的方法是make_password
from django.contrib.auth.hashers import make_password
res = make_password(lqz12345)
print(res)
#pbkdf2_sha256$260000$B9ZRmPFpWb3H4kdDDmgYA9$CM3Q/ZfYyXzxwvjZ+HOcdovaJS7681kDsW77hr5fo5o=
3.加盐
加盐通常指的是加盐加密,加盐加密是一种对系统登录口令的加密方式,它实现的方式是将每一个口令同一个叫做”盐“(salt)的n位随机数相关联。无论何时只要口令改变,随机数就改变。
4.为什么需要加盐
用户注册的密码一般网站管理员利用md5方法加密,这种加密方法好处就是单向加密,不用担心被破解密码,通过加盐提高md5哈希算法的安全性,降低黑客对密码强行破解的可能性
5.Python加盐的简单实现
# md5加密
import hashlib
# 先创建一个md5对象
obj = hashlib.md5()
# 写入要加密的字节
obj.update("admin".encode("utf-8"))
# 获得密文
secret = obj.hexdigest()
print(secret)
6.用户表的密码忘记了怎么办
新增一个用户,把他的密码复制过去就可以了
后台管理simplui的介绍和使用
django admin自带了权限控制(RBAC),但是是前后端混合的,我们可以二次开发,开发出公司内部的自动化运行,自动化测试,人事管理系统,订单系统。它的样式不太好看
对django进行admin美化
xadmin(不用了,过时了)
simpleui(正红
地址:https://simpleui.72wo.com/docs/simpleui/
快速安装
pip3 install django-simpleui
然后在setings.py文件中配置:
INSTALLED_APPS = [
'simpleui', # 一定要放在最上面
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
]
修改菜单功能
settings.py文件中的配置
import time
SIMPLEUI_CONFIG = {
'system_keep': False,
'menu_display': ['Simpleui', '图书管理', '权限认证', '多级菜单测试', '动态菜单测试'], # 开启排序和过滤功能, 不填此字段为默认排序和全部显示, 空列表[] 为全部不显示.
'dynamic': True, # 设置是否开启动态菜单, 默认为False. 如果开启, 则会在每次用户登陆时动态展示菜单内容
'menus': [{
'name': 'Simpleui',
'icon': 'fas fa-code',
'url': 'https://gitee.com/tompeppa/simpleui',
# 浏览器新标签中打开
'newTab': True,
}, {
'app': 'app01',
'name': '图书管理',
'icon': 'fas fa-user-shield',
'models': [{
'name': 'user',
'icon': 'fa fa-user',
'url': 'app01/userinfo/'
}, {
'name': 'book',
'icon': 'fa fa-user',
'url': 'app01/book/'
}, {
'name': 'publish',
'icon': 'fa fa-user',
'url': 'app01/publish/'
}]
}, {
'app': 'auth',
'name': '权限认证',
'icon': 'fas fa-user-shield',
'models': [{
'name': '用户',
'icon': 'fa fa-user',
'url': 'auth/user/'
}]
}, {
# 自2021.02.01+ 支持多级菜单,models 为子菜单名
'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': 'zr',
'url': 'https://www.wezoz.com',
'icon': 'fab fa-github'
}]
}, {
'name': '动态菜单测试',
'icon': 'fa fa-desktop',
'models': [{
'name': time.time(),
'url': 'http://baidu.com',
'icon': 'far fa-surprise'
}]
}]
}
基于drf+vue自己写前后端分离的管理权限
地址:https://django-vue-admin.com/document/
双token校验机制
1.什么是token?
token是客户端登陆的时候由服务端生成,之后每次请求都需要携带token进行请求,起到校验身份的作用
2.为什么要使用token?
减少敏感信息的传递
可以校验用户身份的准确性以及有效期
3.注意点:
token是使用base64的形式进行加密的,有一部分是存储在客户端中的,所以,token不建议存放敏感信息
4.场景设想:
用户正在app或者应用中操作 token突然过期,此时用户不得不返回登陆界面,重新进行一次登录,这种体验性不好,于是引入双token校验机制
5.使用
首次登陆时服务端返回两个token,access Token和refresh Token,access Token过期时间比较短,refresh Token时间较长,并且每次用后会刷新,每次刷新后的refresh Token都是不同的。
6.优势
accessToken的存在,保证了登录态的正常验证,因其过期时间的短暂也保证了帐号的安全性,refreshToekn的存在,保证了用户无需在短时间内进行反复的登陆操作来保证登录态的有效性,同时也保证了活跃用户的登录态可以一直存续而不需要进行重新登录,反复刷新也防止某些不怀好意的人获取refreshToken后对用户帐号进行动手动脚的操作
7.流程
(1).登录操作,在后台服务器验证账号密码成功之后返回2个token:accessToken和refreshToken。
(2).在进行服务器请求的时候,先将Token发送验证,如果accessToken有效,则正常返回请求结果;如果accessToken无效,则验证refreshToken。
(3).此时如果refreshToken有效则返回请求结果和新的accessToken和新的refreshToken。如果refreshToken无效,则提示用户进行重新登陆操作。