DRF 三大认证
用户权限管理
一般项目中,采用的是传统的RBAC(Role-BasedAccessControl)。
传统的RBAC有两种实现方式:权限三表和权限五表
- 权限三表:User、Group、Permission
- 权限五表:User、Group、Permission、UG关系表、GP关系表
基于RBAC的权限管理
RBAC(role-Bases Access Control)基于角色的访问控制,就是用户通过角色与权限进行关联。
简单的说一个用户拥有若干角色,一个角色拥有若干权限,这样,就构成“用户-角色-权限”的授权模型,
在这种模型中,用户与角色之间,角色与权限之间都是多对多的关系
django中auth组件认证六表
六表:User、Group、Permission、UG关系表、GP关系表、UP关系表
自定义扩展auth组件的User表
- 自定义User表继承AbstractUser
- 在settings中配置AUTH_USER_MODEL
- admin注册自定义User表,配置UserAdmin
认证方式
session认证、jwt认证
详见jwt认证
DRF三大认证
认证组件
在请求的时候,认证组件被触发。
主要用于身份认证,用于校验用户:游客、合法用户、非法用户
DRF的APIView类中:
def dispatch(self, request, *args, **kwargs):
...
try:
# django 三大认证
self.initial(request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
...
def initial(self, request, *args, **kwargs):
...
# Ensure that the incoming request is permitted
# 认证模块:校验用户是否登录:合法用户、非法用户、匿名用户
self.perform_authentication(request)
# 权限模块:校验用户是否拥有权限
self.check_permissions(request)
# 频率模块:访问接口的次数是否在设定的时间范围内
self.check_throttles(request)
源码分析
认证组件的模块是self.perform_authentication(request)
二次封装过后的request对象调用.user
实际上调用了Request类中的user方法,通过@property
装饰器来装饰的数据方法的属性
@property
def user(self):
if not hasattr(self, '_user'):
# self是二次封装的request
# 调用这个_authenticate()方法
with wrap_attributeerrors():
self._authenticate()
return self._user
self.authentication_classes
的配置信息就是在APIView类中全局配置的内容。因此我们也可以通过重写该属性来完成局部配置。该全局配置的默认配置内容就是:
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
],
发现默认的配置就是使用authentication
文件下的SessionAuthentication
类和BasicAuthentication
配置认证模块
通过上面的源码分析,我们知道了全局配置和局部配置的实现方法
全局配置:
REST_FRAMEWORK = {
# 认证组件的全局配置
'DEFAULT_AUTHENTICATION_CLASSES': [
# 系统默认的session的认证方法
# 'rest_framework.authentication.SessionAuthentication',
# 'rest_framework.authentication.BasicAuthentication',
# 使用drf-jwt认证
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
],
}
局部配置:
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class UserListAPIView(ListAPIView):
authentication_classes = [JSONWebTokenAuthentication]
使用drf-jwt认证获取token
首先需要安装这个插件
pip3 install djangorestframework-jwt
使用自带
设定好的jwt
from rest_framework_jwt.views import obtain_jwt_token
'''
url('^login/$', obtain_jwt_token)其实相当于 url('login/', ObtainJSONWebToken.as_view())
因为我们之间进源码可以看到
obtain_jwt_token = ObtainJSONWebToken.as_view() #获得
refresh_jwt_token = RefreshJSONWebToken.as_view() #刷新
verify_jwt_token = VerifyJSONWebToken.as_view() #验证
'''
urlpatterns = [
url(r'^login/',obtain_jwt_token),
]
测试接口:post请求
postman发生post请求
接口:http://127.0.0.1:8000/api/login/
数据:
{
"username":"admin",
"password":"admin123"
}
返回一个token字符串
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTc3ODc0MzU0LCJlbWFpbCI6IiJ9.5z8Ya-mxj-oPSOwdXenSKUWf7M5pt3r8YVlFKu1cskY"
}
自定义签发token的登录接口(多方式登录)
- 将请求数据交给序列化类,执行序列化校验(请求的账号可能是用户名、邮箱或手机号,采用正则匹配不同字段校验数据库即可)
- 在序列化全局校验钩子函数中,完成user的认证与token的签发,保存在序列化的content属性中
- 在视图类汇总从序列化对象的content属性中拿到user对象与token相关信息
需要注意的:
- token只能由登录接口签发
- 登录接口也是APIView的子类,使用它的时候一定也要进行三大认证的校验,与自定义出现冲突
- 应该局部应禁用认证与权限的系统的认证类
配置路由
urlpatterns = [
# 自定义签发token的登录login接口
url(r'login/$',views.LoginAPIView.as_view()),
]
配置视图类
from rest_framework.views import APIView
from . import serializer
class LoginAPIView(APIView):
# 局部禁用认证和权限组件
authentication_classes = []
permission_classes = []
def post(self,request,*args,**kwargs):
serializer_obj = serializer.LoginModelSerializer(data=request.data)
serializer_obj.is_valid(raise_exception=True)
return APIResponse({
"username":serializer_obj.content.get('user').username,
"token":serializer_obj.content.get('token')
})
序列化类配置
from rest_framework_jwt.serializers import jwt_payload_handler
from rest_framework_jwt.serializers import jwt_encode_handler
import re
class LoginModelSerializer(serializers.ModelSerializer):
# post请求,默认当做create方法会入库并进行校验,
# 自定义序列化字段,不要入库
username = serializers.CharField(max_length=16,min_length=3)
password = serializers.CharField(min_length=3,max_length=16)
class Meta:
model = models.User
fields = ('username','password')
def _validate_user(self,attrs):
username = attrs.get('username')
password = attrs.get('password')
# 使用re正则匹配账号类型,然后去数据库查询用户对象
if re.match(r'.*@.*',username):
user_obj = models.User.objects.filter(email=username).first()
elif re.match(r'^1[3-9][0-9]{9}$',username):
user_obj = models.User.objects.filter(mobile=username).first()
else:
user_obj = models.User.objects.filter(username=username).first()
if not user_obj or not user_obj.check_password(password):
raise serializers.ValidationError({'message':'用户信息异常'})
return user_obj
# 使用全局钩子,完成token的签发
def validate(self, attrs):
# 使用自定义的方法获取用户对象
user_obj = self._validate_user(attrs)
# 将user对象包装到载荷里加密
payload = jwt_payload_handler(user_obj)
# 载荷签发token字符串
token = jwt_encode_handler(payload)
# 将生成好的user对对象与token封装到serializer对象中,方便在视图类中使用
# 在视图类中使用:
# serializer_obj.content.get('user').username,
# serializer_obj.content.get('token')
self.content = {
"user":user_obj,
"token":token
}
return attrs
postman接口测试
postman发生post请求
接口:http://127.0.0.1:8000/api/login/
数据:
{
"username":"admin",
"password":"admin123"
}
返回一个token字符串
{
"status": {
"username": "admin",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6InFpbnlqIiwiZXhwIjoxNTc3OTY4MTEzLCJlbWFpbCI6IiJ9.ZeJFCZoI-HFcI0EgeGoeEHycj8MtGy_rAAk887PAUIU"
},
"msg": "ok"
}
权限组件
drf默认提供的权限模块:
- AllowAny:匿名用户和合法用户用有所有权限
- IsAuthenticated:只有合法用户拥有所有权限
- IsAdminUser:只有后台用户(admin)拥有全部权限
- IsAuthenticatedOrReadOnly:匿名用户只有只读权限,合法用户拥有全部权限
如果有特殊需要,需要自定义权限类:如针对VIP用户某个视图类有权限
配置权限模块
全局配置:
# drf的配置
REST_FRAMEWORK = {
# 权限模块的全局配置
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
'rest_framework.permissions.IsAuthenticated',
# 自定义权限类
"api.permissions.VIPUserPermission",
],
}
局部配置:
from rest_framework.permissions import AllowAny
class UserListViewSet(mixins.ListModelMixin,GenericViewSet):
permission_classes = [AllowAny]
自定义权限类
例子:以验证用户是否所属于vip组为例,演示自定义权限类的使用
- 继承BasePermission类,重写has_permission方法
- 权限规则(has_permission实现体)
- 返回True:代表有权限
- 返回False:代表无权限
前提工作准备:
- 登录admin后台管理,创建一个用户
- 添加一个组,名叫vip
- 将此用户添加到vip组中
url路由层配置
router.register('user_detail',views.UserViewSet,basename='user_detail')
# 登录需要服务器签发token
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# 签发token的登录login接口
url(r'^login/',obtain_jwt_token),
url(r'',include(router.urls))
]
视图类配置
from rest_framework.viewsets import ViewSet
from .permissions import VIPUserPermission
class UserViewSet(ViewSet):
# 局部配置只有VIP用户才可以查看个人详情
permission_classes = [VIPUserPermission]
def retrieve(self,request,*args,**kwargs):
return APIResponse(results={
"username":request.user.username,
"email":request.user.email,
"mobile":request.user.mobile
})
自定义权限的类VIPUserPermission
from rest_framework.permissions import BasePermission
class VIPUserPermission(BasePermission):
def has_permission(self, request, view):
# 循环遍历请求用户的所属组
for group in request.user.groups.all():
# 如果是vip用户才有权限访问
if group.name.lower() == "vip":
return True
# 如果不是vip用户没有权限访问
return False
postman测试:
首先post请求方式,并携带数据包参数(body --> json)
{
"username":"admin",
"password":"admin123"
}
接口:http://127.0.0.1:8000/api/login/
获得token
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTc3OTY0MjQ5LCJlbWFpbCI6IiJ9.yS-hvJxCdLXdkfWa7yfR0b65t_rFc3yBYuf2uJbVofU"
}
将此token粘贴到headers中,
key VALUE
authorization jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTc3OTY0MjQ5LCJlbWFpbCI6IiJ9.yS-hvJxCdLXdkfWa7yfR0b65t_rFc3yBYuf2uJbVofU
发送get请求
接口:http://127.0.0.1:8000/api/user_detail/1/
此用户是admin,并不属于vip组的用户,所以会报权限不足的异常:
{
"detail": "<api.views.UserViewSet object at 0x000000000DB44550> - GET - You do not have permission to perform this action."
}
再次使用新创建的用户来请求token并发送get请求
接口:http://127.0.0.1:8000/api/user_detail/1/
权限验证通过,查询结果如下:
{
"status": 0,
"msg": "ok",
"results": {
"username": "qinyj",
"email": "",
"mobile": "11111111111"
}
}
频率组件
频率模块就是显示规定时间内的访问次数,drf中默认提供了一些频率模块类
from rest_framework.throttling import SimpleRateThrottle
- AnonRateThrottle:对匿名用户进行频率限制
- UserRateThrottle:对所有用户进行频率限制
若有特殊需求,需要自定义频率类,如对手机发送短信进行限制,1/min (1分钟之内一个手机号只能发送一次验证码)
配置频率模块
全局配置:
# drf的配置
REST_FRAMEWORK = {
# 频率组件:频率类一般做局部配置,但是频率调节在settings中配置
'DEFAULT_THROTTLE_RATES': {
'user': '5/day', # 表示一天之内只能访问5次
'anon': '3/min', # 表示一分钟之内只能访问3次
},
}
局部配置:
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
class UserListViewSet(mixins.ListModelMixin,GenericViewSet):
throttle_classes = [UserRateThrottle]
自定义频率类
- 继承SimpleRateThrottle,重写get_cache_key方法,设置scope类属性
- scope就是一个认证字符串,在配置文件中配置scope字符串对应的频率设置
- get_cache_key的返回值是字符串,该字符串是缓存访问次数的缓存key
- 返回与限制条件有关的字符串,表示限制,一般配合全局配置DEFAULT_THROTTLE_RATES来使用
- 返回None,表示不限制
我们还沿用上面验证用户是否所属于vip组为例,演示自定义频率类的使用
配置自定义频率类:
throttls.py:
from rest_framework.throttling import SimpleRateThrottle
class MobileRateThrottle(SimpleRateThrottle):
scope = "mobile"
def get_cache_key(self, request, view):
if not request.user.is_authenticated or not request.user.mobile:
# 如果走此条件,说明是匿名用户
return None
# 只要有电话号的用户进行限制
return self.cache_format % {
'scope':self.scope,
'ident':request.user.mobile
}
配置视图类
from rest_framework.viewsets import ViewSet
from .permissions import VIPUserPermission
# 导入自定义的频率类
from .throttls import MobileRateThrottle
class UserViewSet(ViewSet):
# 局部配置只有VIP用户才可以查看个人详情
permission_classes = [VIPUserPermission]
# 局部配置自定义的频率类
throttle_classes = [MobileRateThrottle]
def retrieve(self,request,*args,**kwargs):
return APIResponse(results={
"username":request.user.username,
"email":request.user.email,
"mobile":request.user.mobile
})
需要配合settings中配置scope字符串的频率
'DEFAULT_THROTTLE_RATES': {
'user': '5/min',
'anon': '3/min',
# 限制自定义的scope字符串的频率
'mobile': '3/min',
},
访问测试:
将此token粘贴到headers中,
key VALUE
authorization jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTc3OTY0MjQ5LCJlbWFpbCI6IiJ9.yS-hvJxCdLXdkfWa7yfR0b65t_rFc3yBYuf2uJbVofU
发送get请求
接口:http://127.0.0.1:8000/api/user_detail/1/
连续使用新创建的用户来请求token并发送get请求三次
会出现异常,此时自定义频率类已生效
{
"detail": "<api.views.UserViewSet object at 0x000000000DB97F98> - GET - Request was throttled. Expected available in 53 seconds."
}