认证(Authentication)与权限(Permission)不是一回事。
认证是通过用户提供的用户ID/密码组合或者Token来验证用户的身份。认证本身不会允许或拒绝传入的请求,它只是简单识别请求携带的凭证。
权限(Permission)的校验发生验证用户身份以后,决定是否应该接收请求或拒绝访问。
django的认证
对于django,认证模块就是把用户信息绑定到request.user,如果验证通过,则request.user绑定的就是通过验证的用户对象(auth_user表该用户对应的数据object),如果验证不通过,则request.user绑定的是AnonymousUser(匿名用户)。
代码部分
认证相关的代码主要包含三个部分
# 相关的代码主要包含三个部分
django/contrib/auth
django/contrib/session
django/contrib/contenttypes
# 相关的配置(例如settings)
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
]
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
]
# 以下两个非必需,需要覆盖默认值的时候使用
# 用户模型:当默认提供的用户表无法满足需求,需要自定义用户表时使用,用于覆盖Django默认的User模型,指向您自定义的用户模型。格式为 '应用名.模型名'
AUTH_USER_MODEL = "system.Users"
# 认证后端:用于指定Django如何进行用户认证。可以添加自定义的后端以实现如邮箱登录、第三方OAuth登录等复杂认证逻辑。
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
模型部分
主要涉及三张表,主要字段和关联关系如下:
python
# django/contrib/auth/models.py
auth_user表(class User):
username
user_permissions(多对多权限表Permission)
auth_permission表(class Permission):
name
codename
content_type(外键ContentType)
# django/contrib/contenttypes/models.py
django_content_type表(class ContentType):
app_label
model
表字段样例
django_content_type
所有应用的所有model都会生成一条记录。
| id | app_lable | model |
|---|---|---|
| 1 | auth | group |
| 2 | auth | permission |
auth_permission
权限是绑定到model级别的,每个content_type_id对应一个model,对应的code_name就是权限名称的编码。也是在代码中设置要求的权限时要用到的。
| id | name | content_type_id | code_name |
|---|---|---|---|
| 1 | Can add permission | 1 | add_permission |
| 2 | Can change permission | 1 | change_permission |
auth_user
| id | username | is_active |
|---|---|---|
| 1 | 张三 | 1 |
| 2 | 李四 | 1 |
auth_user_user_permissions
用户表和权限表多对多关系生成的用户权限表。
| id | user_id | permission_id |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 1 | 3 |
处理流程
请求处理的流程如下:
中间件
session中间件
auth中间件
request请求
API视图
session中间件:初始化session对象绑定到request.session。
python
# django/contrib/session/middleware.py
class SessionMiddleware(MiddlewareMixin):
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key)
# 初次访问时,因未携带Cookies数据,获取session_key为None,则初始化一个空对象绑定到request.session
# 若访问携带Cookies,则通过Cookies中的sessionid获取数据表中存储的session_data,
# 解析后绑定到request.session,此时则完成了用户的绑定和认证
auth中间件:初始化一个空对象(延迟计算属性)绑定到request.user。
python
# django/contrib/auth/middleware.py
class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
request.user = SimpleLazyObject(lambda: get_user(request))
可以看出中间件的处理仅仅是初始化了两个空对象,没有实质性的动作。
真正是否要开启用户权限校验的开关是API视图来决定的。
开启认证
django框架提供了开启认证的装饰器函数,可以通过快速配置实现权限校验.
-
1、基于函数的视图(FBV)
使用样例
通过在函数上加上装饰器即可开启认证校验。
pythonfrom django.contrib.auth.decorators import login_required # 方法一,最常用 @login_required def my_view(request): pass # 方法二 my_view = login_required(my_view)原理解析
python# django/contrib/auth/decorators.py # 装饰器工厂函数 def user_passes_test( test_func,login_url=None,redirect_field_name=REDIRECT_FIELD_NAME ): def decorator(view_func): @wraps(view_func) def _wrapper_view(request, *args, **kwargs): # 若校验函数返回true,则进入视图处理 if test_func(request.user): return view_func(request, *args, **kwargs) # 若校验函数返回false,转到登陆页 return redirect_to_login(path, resolved_login_url, redirect_field_name) return _wrapper_view return decorator # 认证装饰器 # 此处function参数是要传入视图函数 def login_required( function=None,redirect_field_name=REDIRECT_FIELD_NAME,login_url=None ): # 可以看出test_func(request.user)就是要校验request.user.is_authenticated是否为true actual_decorator = user_passes_test( lambda u: u.is_authenticated, login_url=login_url, redirect_field_name=redirect_field_name, ) # 对应样例中的方法二,my_view = login_required(my_view) if function: return actual_decorator(function) # 对应样例中的方法一,返回实际的装饰器 return actual_decorator -
2、基于类的视图(CBV)
使用样例
通过继承框架提供的混入类实现
pythonfrom django.contrib.auth.mixins import LoginRequiredMixin class MyView(LoginRequiredMixin, View): login_url = "/login/" redirect_field_name = "redirect_to"原理解析
pythonclass LoginRequiredMixin(AccessMixin): def dispatch(self, request, *args, **kwargs): # request.user.is_authenticated为false时转到登陆页 if not request.user.is_authenticated: return self.handle_no_permission() # request.user.is_authenticated为true时进入视图处理 return super().dispatch(request, *args, **kwargs) class AccessMixin: def handle_no_permission(self): return redirect_to_login( path, resolved_login_url, self.get_redirect_field_name(), ) -
3、自己编码校验
就是不用框架提供的工具包,自己在视图里校验,不推荐。
样例
pythonfrom django.conf import settings from django.shortcuts import redirect def my_view(request): # 自行检查 request.user.is_authenticated ,若未登陆则重定向到登录页面 if not request.user.is_authenticated: return redirect(f"{settings.LOGIN_URL}?next={request.path}") # ...基于以上,可以看出认证需求的主题逻辑就是:
检查request.user.is_authenticated,true就允许下一步处理,false就跳转登录页
认证内部逻辑解析
因为要检查request.user.is_authenticated,就需要真正执行request.user = SimpleLazyObject(lambda: get_user(request))了。
python
# django/contrib/auth/__init__.py
def get_user(request):
from .models import AnonymousUser
user = None
try:
user_id = _get_user_session_key(request)
backend_path = request.session[BACKEND_SESSION_KEY]
except KeyError:
pass
else:
if backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
user = backend.get_user(user_id)
...
# 校验session信息成功则返回用户对象
# 否则返回匿名用户对象
return user or AnonymousUser()
若返回AnonymousUser,则request.user.is_authenticated的值为false,则会转到登陆页处理。
登陆处理
django提供了关于登陆、认证的一系列组件,可以直接拿来使用。主要包括:
API view: 位于django/contrib/auth/views.py的LoginView、LogoutView等
工具函数:位于django/contrib/auth/init.py的authenticate(),login()等
但django没有提供登陆页的模板,因此很多时候,都是用户自行编写登陆页、登陆认证相关的视图API等。
不过还是介绍下django提供的登录页的基础逻辑。
python
# django/contrib/auth/views.py
class LoginView(RedirectURLMixin, FormView):
form_class = AuthenticationForm
# 此处为登陆页的模板,框架并没有提供,需要自行编写
# 主要就是用户名、密码的输入框,以Post方式提交
template_name = "registration/login.html"
def form_valid(self, form):
auth_login(self.request, form.get_user())
return HttpResponseRedirect(self.get_success_url())
# django/views/generic/edit.py
class ProcessFormView(View):
# 提交用户名密码的请求处理入口
def post(self, request, *args, **kwargs):
# 获取到form_class = AuthenticationForm,调用AuthenticationForm的is_valid
# is_valid函数会调用到clean()函数
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
# django/contrib/auth/forms.py
class AuthenticationForm(forms.Form):
def clean(self):
# clean函数获取表单中的用户名/密码
username = self.cleaned_data.get("username")
password = self.cleaned_data.get("password")
if username is not None and password:
# 调用auth/__init__.py的authenticate()验证密码
self.user_cache = authenticate(
self.request, username=username, password=password)
return self.cleaned_data
# django/contrib/auth/__init__.py
def authenticate(request=None, **credentials):
# 对于配置多个认证后端的情况,按顺序任何一个通过即返回
for backend, backend_path in _get_backends(return_tuples=True):
try:
user = backend.authenticate(request, **credentials)
# 前面POST方法中,认证通过后,开始调用self.form_valid(form),这个form_valid就是执行login函数。
# django/contrib/auth/__init__.py
def login(request, user, backend=None):
......
# login这个函数主要是处理session信息,认证成功后,将用户的信息存放到request.session
# 这与惯性思维的login概念不一样,常规思维login是登陆入口,负责认证一些列的动作
# 而这里的login是个工具方法,只是做认证通过后的后续处理方法
自定义登陆
如果要使用django提供的方法,首先登陆页要自行编写,后端认证接口的基本流程如下:
python
def post(self, request, *args, **kwargs):
# 获取用户密码信息
username = request.data.get('username')
password = request.data.get('password')
# 验证用户密码信息
user = authenticate(username=username, password=password)
# 验证成功后的处理
login(request, user)
return
认证后端
认证后端可以再settings文件里通过AUTHENTICATION_BACKENDS进行配置。其默认值为('django.contrib.auth.backends.ModelBackend',)。除此之外,可以通过继承BaseBackend自定义认证后端。
自定义认证后端主要关注两个方法:
authenticate:自定义认证方法,主要有校验用户密码等,return True or False
get_user: 根据user_id获取用户对象。这个方法是登陆成功后,后续处理带着cookie信息的请求,由中间件层获取用户对象。
python
# django/contrib/auth/backends.py
class BaseBackend:
def authenticate(self, request, **kwargs):
return None
def get_user(self, user_id):
return None
django的权限
权限是在认证完成后,验证用户是否具有相关视图有访问权限。
权限定义
权限名一般的格式为:{app名}.{权限动作}_{模型名}。
以blog应用为例,Django为Article模型自动创建的4个可选权限名分别为:
查看文章(view): blog.view_article
创建文章(add): blog.add_article
更改文章(change): blog.change_article
删除文章(delete): blog.delete_article
如何增加自定义权限
权限的清单是存储在auth_permission表。
增加自定义权限实质就是在auth_permission表中增加一条记录。
方法1. 在Model的meta属性中添加权限,在执行migrate时会维护权限数据
python
class Article(models.Model):
...
class Meta:
permissions = (
("publish_article", "Can publish article"),
("comment_article", "Can comment article"),
)
方法2. 使用ContentType程序化创建权限
比如可以在交互式的shell中,手工增加数据。
python
python manage.py shell
>>> from blog.models import Article
>>> from django.contrib.auth.models import Permission
>>> from django.contrib.contenttypes.models import ContentType
>>>
# 获取model对象的id
>>> content_type = ContentType.objects.get_for_model(article)
# 新增权限
>>> permission1 = Permission.objects.create(
codename='publish_article',
name='Can publish articles',
content_type=content_type,
)
>>>
>>> permission2 = Permission.objects.create(
codename='comment_article',
name='Can comment articles',
content_type=content_type,
)
>>>
绑定用户和权限
这个一般需要用户自行编写前端页面和API接口,实现权限管理。
校验权限
跟认证一样,默认情况下是不去校验权限的,如果需要校验权限,需要在接口级别做单独的配置。
-
1、基于函数的视图(FBV)
使用样例
通过在函数上加上装饰器即可开启权限校验。
pythonfrom django.contrib.auth.decorators import permission_required # 验证一个权限 @permission_required('polls.can_vote') def my_view(request): pass # 同时具有多个权限才可访问 @permission_required(('polls.can_vote', 'polls.can_edit')) def my_view(request): pass -
2、基于类的视图(CBV)
使用样例
pythonfrom django.contrib.auth.mixins import PermissionRequiredMixin # 验证一个权限 class MyView(PermissionRequiredMixin, View): permission_required = 'polls.can_vote' # 同时具有多个权限才可访问 class MyView(PermissionRequiredMixin, View): permission_required = ('polls.can_open', 'polls.can_edit')
说明:
1、如果permission中指定了多个权限,则是and的关系,必须要同时满足才行。
2、对于CBV情况下,权限是针对所有方法的,比如MyView下有get()、post()方法,则权限校验是全局通用的,而不能针对特定方法。
3、'polls.can_vote'只是一个权限名称。这种权限名是全局性的,可以在任意位置使用,并不是只能用在polls 应用下或者vote的model中使用。
-
3、针对单个方法的校验
若需要针对不同的方法设置不同的权限,可以使用method_decorator装饰器。
pythonfrom django.contrib.auth.decorators import permission_required, login_required from django.utils.decorators import method_decorator from django.views.generic import View from django.http import JsonResponse class ArticleView(View): @method_decorator(permission_required('blog.add_article', raise_exception=True)) def post(self, request, *args, **kwargs): """POST 方法需要 blog.add_article 权限""" return JsonResponse({'message': '文章创建成功'}) # 可以多个组合 @method_decorator(login_required) @method_decorator(permission_required('blog.view_article', raise_exception=True)) def get(self, request, *args, **kwargs): """需要登录且具有 view_article 权限""" return JsonResponse({'message': '查看文章'})
如果还想要实现更复杂的校验机制,就得自己写校验逻辑了。
再回头看看权限校验的源码逻辑
python
# django/contrib/auth/decorators.py
def permission_required(perm, login_url=None, raise_exception=False):
def check_perms(user):
# 调用用户的has_perms方法
if user.has_perms(perms):
return True
return False
return user_passes_test(check_perms, login_url=login_url)
# django/contrib/auth/model.py
class PermissionsMixin(models.Model):
# 用户的has_perms方法,对perm_list里定义的权限进行校验
def has_perms(self, perm_list, obj=None):
# 使用all,必须所有的权限都满足才通过
return all(self.has_perm(perm, obj) for perm in perm_list)
# 对单个权限进行校验
def has_perm(self, perm, obj=None):
return _user_has_perm(self, perm, obj)
# 具体校验逻辑
def _user_has_perms(user, perm, obj):
# 循环所有认证后端,只要有一个后端的校验通过即校验通过
for backend in auth.get_backends():
if backend.has_perm(user, perm, obj):
return True
return False
# 从以上验证方法可以看出,认证通过的backend和校验的backend没有强制要求是同一个。
django框架自带的权限校验确实存在较多的不足,如果要求不高,确实可以直接拿来开箱使用,如果项目要求较高,大概率要自行编码校验。
主要不足有:
1、不能实现行级别的校验,若用户有权限则对所有数据有权限。比如要限制用户只能编辑自己的文档,不能修改别人的文档。
2、不能实现多权限OR的校验,即若多个权限满足任何一个就通过。
DRF的认证和权限
DRF的认证权限流程
视图处理
中间件
session中间件
auth中间件
DRF认证
DRF权限
DRF视图
request请求
通过django认证的流程解析,我们知道中间件处理部分其实就是解析token,绑定到request.user,其并不会对用户请求进行权限校验和拦截。如果需要拦截,需要在django的视图函数上加上装饰器。
对于DRF,则复用了django的中间件部分,把真正校验的部分放在了视图里。
默认配置
默认配置需要在settings目录下
python
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated", # 只有经过身份认证确定用户身份才能访问
],
}
视图级别配置
基于类的视图(CBV)
python
class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
def get():
pass
以上在类级别设置的认证类和权限类,是对该类下的所有函数生效的。如果要为函数类的某个函数单独指定认证类或权限类。则可以参考以下例子
python
## 方法一,重写get_permission方法
class MyView(APIView):
# 类级别的默认设置
authentication_classes = [SessionAuthentication]
permission_classes = [IsAuthenticated]
def get_permissions(self):
"""
根据请求方法动态返回权限类
"""
if self.request.method == 'GET':
return [AllowAny()] # GET 允许任何人
elif self.request.method == 'POST':
return [IsAuthenticated()] # POST 需要登录
else:
return [IsAuthenticated()] # 其他方法也需要登录
# 对于viewset,则可以在action中指定
class UserViewSet(ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAuthenticated] # 默认需要认证
@action(detail=False, methods=['post'],
authentication_classes=[SessionAuthentication],
permission_classes=[AllowAny])
def register(self, request):
pass
基于函数的视图(FBV)
python
@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated,))
def example_view(request, format=None):
pass
使用api_view装饰器,即可将django的函数转成DRF下的函数。就可以使用DRF下默认的DEFAULT_AUTHENTICATION_CLASSES、DEFAULT_PERMISSION_CLASSES这些配置。
如果想要为函数单独设置认证类和权限类,则需要使用authentication_classes、permission_classes装饰器。
自定义认证和权限类
python
# 自定义认证类
from rest_framework.authentication import BaseAuthentication
class MyAuthentication(BaseAuthentication):
# 自定义逻辑,返回user对象和认证信息
def authenticate(self, request):
return (user, None)
# 自定义权限类
from rest_framework.permissions import BasePermission
class MyPermission(BasePermission):
# 自定义逻辑,返回true or false
def has_permission(self, request, view):
return True or False
调用部分源码解析
以APIView为例,APIView视图的调用入口是该类的dispatch()方法。
python
# rest_framework/view.py
class APIView(View):
def dispatch(self, request, *args, **kwargs):
request = self.initialize_request(request, *args, **kwargs)
self.initial(request, *args, **kwargs)
return self.response
def initial(self, request, *args, **kwargs):
# 认证
self.perform_authentication(request)
# 权限
self.check_permissions(request)
self.check_throttles(request)