drf之三大认证
一、前言
我们知道drf的APIView类的as_view直接对原生django的csrf进行了禁用,是什么让drf有如此底气?从之前对drf的源码分析可以看到,三条语句。
self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)
这就是drf的三大认证。
二、用户认证
1.drf的用户认证
我们的某些接口需要对用户进行辨别,那么我们该如何区分A用户和B用户呢?如果A用户想访问B用户的余额,这种操作是不被允许的。在django中,已经完成过基于cookie和session的身份认证,对登录的用户返回一个cookie,并在服务端也进行保存,用户必须携带cookie才能通过用户认证。
drf的认证规则:
- 如果没有认证信息,则认为是游客
- 如果认证失败,抛出异常
- 认证成功返回(user,token)
rest_framework文件下的authentication.py中为我们写好了用户认证的基类,及一些基础的认证类。我们可以通过重写authenticate和相关的方法来定义自己的用户认证类。
class BaseAuthentication:
"""
All authentication classes should extend BaseAuthentication.
"""
def authenticate(self, request):
"""
Authenticate the request and return a two-tuple of (user, token).
"""
raise NotImplementedError(".authenticate() must be overridden.")
def authenticate_header(self, request):
"""
Return a string to be used as the value of the `WWW-Authenticate`
header in a `401 Unauthenticated` response, or `None` if the
authentication scheme should return `403 Permission Denied` responses.
"""
pass
2.基于token的drf-jwt认证
我们借用第三方djangorestframework-jwt来完成我们的用户认证。jwt是通过签发token、校验token来完成用户认证的。token是有 头、体、签名信息组成的一串字符串,以 . 分割开。
'''
token示例eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6ImN5YW4iLCJleHAiOjE1ODI1NDA0MDcsImVtYWlsIjoiIn0.52E5R00GL0gx-3O3OTosXz0cDWmVzmNU16xEbZpmAkg
1.头和体部一般可以是一个字典序列化成Json格式后转化为二进制然后进行 双向加密如:base64算法得到的。
2.头部信息一般只是对token的所属进行声明,例如项目名称、公司名称
3.体部信息则包含用户的标识信息、及关键信息,如用户的主键、用户的设备信息、token的有效时间、token的签发时间。服务端可以通过对其进行反向解密获得相关信息。
4.尾部信息由 经过加密后的头部和尾部 与 服务器存储的秘钥 进行单向加密如:md5算法生成。
分析:服务端拿到token后会对 头、体及自身存储的秘钥进行md5加密,如果加密的结果与第三段不符,则一定是头、体的信息发生了改变,token便会认为无效。所以服务器端的秘钥是至关重要的,如果泄漏了则攻击者可以伪造任意的token对任意的接口进行访问。
优势:服务端不用再保存用户的认证信息,也就意味着不需要频繁的读写数据库,降低了数据库的压力,在实现服务器集群时也非常方便。
'''
jwtauthentication源码分析
# from rest_framework_jwt.authentication.JSONWebTokenAuthentication类中
class BaseJSONWebTokenAuthentication(BaseAuthentication):
"""
Token based authentication using the JSON Web Token standard.
"""
def authenticate(self, request):
"""
Returns a two-tuple of `User` and token if a valid signature has been
supplied using JWT-based authentication. Otherwise returns `None`.
"""
# 获取request请求中的token
jwt_value = self.get_jwt_value(request)
if jwt_value is None:
return None
# 对token进碰撞校验:编码格式、过期时间、token是否有效
try:
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中包含的user对象
user = self.authenticate_credentials(payload)
return (user, jwt_value)
def authenticate_credentials(self, payload):
"""
Returns an active user that matches the payload's user id and email.
"""
User = get_user_model()
username = jwt_get_username_from_payload(payload)
if not username:
msg = _('Invalid payload.')
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get_by_natural_key(username)
except User.DoesNotExist:
msg = _('Invalid signature.')
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = _('User account is disabled.')
raise exceptions.AuthenticationFailed(msg)
return user
class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
"""
Clients should authenticate by passing the token key in the "Authorization"
HTTP header, prepended with the string specified in the setting
`JWT_AUTH_HEADER_PREFIX`. For example:
Authorization: JWT eyJhbGciOiAiSFMyNTYiLCAidHlwIj
"""
www_authenticate_realm = 'api'
def get_jwt_value(self, request):
auth = get_authorization_header(request).split()
auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()
if not auth:
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
# token如果不由 前缀JWT + 原token组成则抛出异常
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]
def authenticate_header(self, request):
"""
Return a string to be used as the value of the `WWW-Authenticate`
header in a `401 Unauthenticated` response, or `None` if the
authentication scheme should return `403 Permission Denied` responses.
"""
return '{0} realm="{1}"'.format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)
通过了用户认证的类,request.user的对象要么是Anonymous或者是数据库中合法的user对象,至此,用户认证完毕。
三、权限认证
只有通过了用户认证的request请求才会进行权限认证。我们的某些接口通常需要vip用户才能访问,普通用户是无法进行访问的。但vip用户和普通用户都会通过用户认证,我们又该如何区分呢?
rest_framework文件夹下的permissions.py。
class BasePermission(metaclass=BasePermissionMetaclass):
"""
A base class from which all permission classes should inherit.
"""
def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
def has_object_permission(self, request, view, obj):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
'''
drf自带的权限认证类:
- AllowAny 允许任何人
- IsAuthenticated 只允许登录用户
- IsAdminUser 只允许后台用户
- IsAutenticatedOrReadOnly 只允许未登录用户读,允许登录用户读写
我们可以通过继承BasePermission类重写has_permission方法来实现自定义的权限认证类,认证通过返回True,否则返回False即可。
'''
校验用户是否是VIP或属于VIP分组的权限类 案例
class IsVipPermission(BasePermission):
def has_permission(self, request, view):
if request.user and request.user.is_authenticated and request.user.is_vip:
return True
else:
return False
# 没有is_vip字段,有vip分组控制时
class IsVipPermission(BasePermission):
def has_permission(self, request, view):
vip_group = Group.objects.get(name='vip')
if request.user and request.user.is_authenticated and (vip_group in request.user.groups.all()):
return True
else:
return False
四、频率认证
当request请求通过用户认证和权限认证后,还要进行频率的检测,如果我们接口不限制访问频率,那么可能会让攻击者有机可乘,造成服务器的瘫痪。
rest_framework文件夹下的throttling.py中已经定义了基础的频率校验类。我们只需要继承SimpleRateThrottle类并重写get_cache_key方法。
案例:自定义频率类,只限制get请求的访问频率,不限制其他访问请求
class MethodRateThrottle(BaseThrottle):
scope = 'method' # scope
def get_cache_key(self,request,view):
if request.method.lower() == 'get':
return self.cache_format % {
'scope': self.scope,
'ident': self.get_ident(request)
}
else:
return None
'''
scope需要在settings.py中进行配置
get_cache_key方法,返回None代表 无限制访问,如果返回字符串,则该字符串会在缓冲中被保存(因为数据库中没有相应的表,所以我们推断内存中应该由一张虚拟的表,用来记录访问的频率)。
例如:限制了 3/min的访问频率,如果同一用户在1分钟内访问了3次,则会返回3次相同的字符串(因为该字符串是带有用户标识信息的get_indent,不同的用户的表示信息不同,一个用户被限制不会影响其他用户)。当第4次访问时,reqeust请求就会被拒绝。
'''
五、token刷新
drf-jwt为我们提供了token 的刷新功能,我们可以给token属性设置为可刷新。在token有效并且在刷新过期时间内,可以访问接口来刷新token的过期时间。
"""
1)运用在像12306这样极少数安全性要求高的网站
2)第一个token由登录签发
3)之后的所有正常逻辑,都需要发送两次请求,第一次是刷新token的请求,第二次是正常逻辑的请求
"""
settings.py
import datetime
JWT_AUTH = {
# 配置过期时间
'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=5),
# 是否可刷新
'JWT_ALLOW_REFRESH': True,
# 刷新过期时间
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
}
urls.py
from rest_framework_jwt.views import ObtainJSONWebToken, RefreshJSONWebToken
urlpatterns = [
url('^login/$', ObtainJSONWebToken.as_view()), # 登录签发token接口
url('^refresh/$', RefreshJSONWebToken.as_view()), # 刷新toekn接口
]
六、多方式登录
drf-jwt只为我们提供了 用户名-密码登录的签发token,是不支持用户以手机号、邮箱登录进行登录的。如果我们想实现多方式登录,必须自定义签发token。
serializers.py
class LoginSerializer(serializers.ModelSerializer):
# 局部禁用
authentication_classes = []
permission_classes = []
# 需要对字段进行覆盖,以免drf认为这是在做增数据自动校验
username = serializers.CharField()
password = serializers.CharField()
class Meta:
model = models.User
fields = ['username', 'password']
def validate(self, attrs):
from rest_framework_jwt.serializers import jwt_payload_handler,jwt_encode_handler
user = self._get_user(attrs)
if user:
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
self.context['token'] = token
return attrs
else:
raise exceptions.ValidationError({"error":"username or password valid!"})
def _get_user(self, attrs):
import re
username = attrs.get('username') # type:str
password = attrs.get('password')
if re.match(r'^.+@.+$', username):
# 邮箱登录
print('..邮箱登录')
user = models.User.objects.filter(email=username).first()
elif re.match(r'^1[3-9][0-9]{9}$', username):
print('..手机登录')
user = models.User.objects.filter(mobile=username).first()
else:
user = models.User.objects.filter(username=username).first()
if user and user.check_password(password):
return user
urls.py
urlpatterns = [
url('login/',views.LoginAPIViewSet.as_view({'post':"login"})),
]
views.py
class LoginAPIViewSet(viewsets.GenericViewSet):
def login(self, request, *args, **kwargs):
serializer = serializers.LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
token = serializer.context.get('token')
return Response({'token': token})
认证类的配置
'''
类的优先顺序:局部-->全局-->drf默认
'''
settings.py
JWT_AUTH = {
# token存活时间
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
# token前缀
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
REST_FRAMEWORK = {
# 用户认证
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
],
# 权限认证
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
# 频率认证
'DEFAULT_THROTTLE_CLASSES': [],
# 频率配置
'DEFAULT_THROTTLE_RATES': {
'user': '3/min', # duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
'anon': None,
},
}