起初写了一个基于token的认证授权方式,建一张token表,有创建时间和更新时间两个字段,和user是many to many关系,这样可以保存登录记录,登录的时候如果通过用户名密码的验证就查一下token表该用户的上一次更新时间,如果相差小于30分钟就不生成新的token了,更新token表的最后一次登录记录的更新时间为当前时间就行了,否则就根据用户名和当前时间新建一个token,创建一条新的登录记录插入token表,创建时间和更新时间都是当前时间,然后返回token。
每次访问api都在http请求头中带着token,用装饰器在视图函数上认证和授权,认证的方法是查一次token表看token存在不存在,是否超时,授权的方法是查一次user和group表看用户在不在授权用户组里。
使用这种方式的好处是token放在请求头里,不用cookie,方便跨源资源共享(CORS)和前后端分离开发方式中前后端的认证交互,但本质上和session的方式是一样的。
jwt也是token,好处除了加密以外最主要的是可以在payload里自定义信息和自带超时校验,这样就不需要token表了,登录验证成功以后生成token时把用户组信息和超时时间都放到token里,每次请求api的时候直接解出token里的信息进行验证和授权,一次数据库都不用查,大大提高了认证授权的效率,登录记录通过日志保存就行了。
网上介绍jwt的中文文章很多,介绍的都是基本的认证方法,没找到把使用方法说得细致透彻的,我结合英文文章和源码实现了一下。
core/jwt.py
import time
def jwt_payload_handler(user):
"""自定义payload内容"""
return {
'username': user.username,
# 用户组信息,用于认证和授权
'groups': [i.name for i in user.groups.all()],
'is_superuser': user.is_superuser,
'exp': time.time() + 60 * 30 # 超时期限
}
def jwt_response_payload_handler(token, user=None, request=None):
"""自定义登录成功后的返回信息,是给前端的"""
return {
'username': user.username,
'groups': [i.name for i in user.groups.all()],
'token': token
}
settings.py
REST_FRAMEWORK = {
# 使用JWT认证
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
JWT_AUTH = {
# 上面那个文件里的两个自定义函数
'JWT_RESPONSE_PAYLOAD_HANDLER': 'myapp.core.jwt.jwt_response_payload_handler',
'JWT_PAYLOAD_HANDLER': 'myapp.core.jwt.jwt_payload_handler',
}
urls.py
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path('login/', obtain_jwt_token), # 登录url,用于获取token
]
views.py
# 用于认证和授权的装饰器函数
def group_required(*required_group_names, method_map=None):
"""
校验用户是否登录并且属于要求的用户组中的一个
:param required_group_names: 视图集要求的各个用户组名称
:param method_map: 对于指定方法要求的用户组名称{方法名:(用户组)}
"""
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
token = request.META.get('HTTP_AUTHORIZATION', None)
if not token:
return JsonResponse({'code': 0, 'msg': '失败,尚未登录', 'data': {}},
status=status.HTTP_401_UNAUTHORIZED)
from jwt.exceptions import ExpiredSignatureError
try:
jwt_info = jwt.decode(token.split()[-1], SECRET_KEY)
except ExpiredSignatureError:
return JsonResponse({'code': 0, 'msg': '失败,登录已超时', 'data': {}},
status=status.HTTP_401_UNAUTHORIZED)
if jwt_info['is_superuser']:
return view_func(request, *args, **kwargs)
# 对于指定方法的校验,用户不属于要求组中的任何一个则认证失败
if method_map and request.method in method_map:
if set(method_map[request.method]).isdisjoint(set(jwt_info['groups'])):
return JsonResponse({'code': 0, 'msg': '失败,无访问权限', 'data': {}},
status=status.HTTP_403_FORBIDDEN)
return view_func(request, *args, **kwargs)
# 通用方法校验,用户不属于要求组中的任何一个则认证失败
if set(required_group_names).isdisjoint(set(jwt_info['groups'])):
return JsonResponse({'code': 0, 'msg': '失败,无访问权限', 'data': {}},
status=status.HTTP_403_FORBIDDEN)
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
# 使用示例
from django.utils.decorators import method_decorator
@method_decorator(group_required('订单管理员', method_map={'GET': ('订单管理员', '仓库管理员')}), name='dispatch')
class OrderViewSet(CustomModelViewSet):
queryset = Order.objects.all()
serializer_class = OrderSerializer
filterset_class = OrderFilter
使用说明:
登录获得token:每次登录成功就得到jwt_response_payload_handler
返回的token,然后访问api的时候在请求头的Authorization
字段带上token,格式是JWT
。
通过token认证授权:给ModelViewSet
类的dispatch
方法加装饰器要使用method_decorator
,装饰器函数的*required_group_names
参数是视图集类里面所有方法都要求的用户组,上例中订单视图集的各个方法都要求只有订单管理员用户组的成员才可以访问,method_map
是针对个别方法的特殊授权要求,先匹配method_map
里的http方法,如果匹配上了就按照这个字典里设置的授权组进行验证,没匹配上的再去验证*required_group_names
。上例就是仓库管理员也可以查看订单。