Django已经提供了开箱即用的认证系统,但是可能并不满足我们的个性化需求。自定义认证系统需要知道哪些地方可以扩展,哪些地方可以替换。本文就来介绍自定义Django认证系统的相关技术细节。

自定义认证后端

AUTHENTICATION_BACKENDS

Django默认认证后端为:

  1. ['django.contrib.auth.backends.ModelBackend']

可以在settings.py中配置AUTHENTICATION_BACKENDS为自定义的认证后端,其本质是Python class,在调用django.contrib.auth.authenticate()时会进行遍历:

  1. def authenticate(request=None, **credentials):
  2. """
  3. If the given credentials are valid, return a User object.
  4. """
  5. for backend, backend_path in _get_backends(return_tuples=True):
  6. backend_signature = inspect.signature(backend.authenticate)
  7. try:
  8. backend_signature.bind(request, **credentials)
  9. except TypeError:
  10. # This backend doesn't accept these credentials as arguments. Try the next one.
  11. continue
  12. try:
  13. user = backend.authenticate(request, **credentials)
  14. except PermissionDenied:
  15. # This backend says to stop in our tracks - this user should not be allowed in at all.
  16. break
  17. if user is None:
  18. continue
  19. # Annotate the user object with the path of the backend.
  20. user.backend = backend_path
  21. return user
  22. # The credentials supplied are invalid to all backends, fire signal
  23. user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)

列表中的认证后端是有先后顺序的,Django会依次进行认证,只要有后端认证成功,就会结束认证,如果有后端抛出PermissionDenied异常,也会停止认证。

如果修改了认证后端,想要用户重新认证,那么需要调用Session.objects.all().delete()清除session数据,因为session中会缓存已认证过的认证后端。

编写认证后端

先看看默认认证后端的源码片段:

  1. class ModelBackend(BaseBackend):
  2. """
  3. Authenticates against settings.AUTH_USER_MODEL.
  4. """
  5. def authenticate(self, request, username=None, password=None, **kwargs):
  6. if username is None:
  7. username = kwargs.get(UserModel.USERNAME_FIELD)
  8. if username is None or password is None:
  9. return
  10. try:
  11. user = UserModel._default_manager.get_by_natural_key(username)
  12. except UserModel.DoesNotExist:
  13. # Run the default password hasher once to reduce the timing
  14. # difference between an existing and a nonexistent user (#20760).
  15. UserModel().set_password(password)
  16. else:
  17. if user.check_password(password) and self.user_can_authenticate(user):
  18. return user
  19. ...
  20. def get_user(self, user_id):
  21. try:
  22. user = UserModel._default_manager.get(pk=user_id)
  23. except UserModel.DoesNotExist:
  24. return None
  25. return user if self.user_can_authenticate(user) else None

总结一下:

  1. 继承BaseBackend。

  2. 实现了authenticate()。(backend也有个authenticate方法,跟django.contrib.auth.authenticate()不一样哦)authenticate(request=None, **credentials)方法的第一个入参是request,可为空,第二个入参是credentials(用户凭证如用户名、密码),示例:

    1. from django.contrib.auth.backends import BaseBackend
    2. class MyBackend(BaseBackend):
    3. def authenticate(self, request, username=None, password=None):
    4. # Check the username/password and return a user.
    5. ...

    用户凭证也可以是token:

    1. from django.contrib.auth.backends import BaseBackend
    2. class MyBackend(BaseBackend):
    3. def authenticate(self, request, token=None):
    4. # Check the token and return a user.
    5. ...

    如果认证成功就返回User对象,如果认证失败就返回None。

  3. 实现了get_user()get_user(user_id)方法入参是user_id,可以是username/数据库ID等,必须是User的主键,返回值为User对象或者None。

我们试着来编写一个认证后端,为了演示效果,我们不用客户端服务器模式,而是在settings.py文件中增加2个配置,然后用我们自定义的认证后端进行认证,代码如下:

  1. from django.conf import settings
  2. from django.contrib.auth.backends import BaseBackend
  3. from django.contrib.auth.hashers import check_password
  4. from django.contrib.auth.models import User
  5. class SettingsBackend(BaseBackend):
  6. """
  7. 认证settings中ADMIN_LOGIN和ADMIN_PASSWORD变量,比如:
  8. ADMIN_LOGIN = 'admin'
  9. ADMIN_PASSWORD = 'pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M='
  10. """
  11. def authenticate(self, request, username=None, password=None):
  12. login_valid = (settings.ADMIN_LOGIN == username)
  13. pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
  14. if login_valid and pwd_valid:
  15. try:
  16. user = User.objects.get(username=username)
  17. except User.DoesNotExist:
  18. # 创建一个新用户
  19. user = User(username=username)
  20. user.is_staff = True
  21. user.is_superuser = True
  22. user.save()
  23. return user
  24. return None
  25. def get_user(self, user_id):
  26. try:
  27. return User.objects.get(pk=user_id)
  28. except User.DoesNotExist:
  29. return None

自定义认证后端授权

认证后端可以重写方法get_user_permissions(), get_group_permissions(), get_all_permissions(), has_perm(), has_module_perms(), with_perm()来实现授权。示例:

  1. from django.contrib.auth.backends import BaseBackend
  2. class MagicAdminBackend(BaseBackend):
  3. def has_perm(self, user_obj, perm, obj=None):
  4. # 如果是超管,就会获得所有权限,因为不管perm是什么,都返回True
  5. return user_obj.username == settings.ADMIN_LOGIN

可以根据业务编写具体的判断逻辑,给不同用户/组授予不同权限。

user_obj可以是django.contrib.auth.models.AnonymousUser,用来给匿名用户授予某些权限。

User有个is_active字段,ModelBackend和RemoteUserBackend不能给is_active=False的用户授权,如果想授权,可以使用AllowAllUsersModelBackend或AllowAllUsersRemoteUserBackend。

自定义新权限

除了增删改查权限,有时我们需要更多的权限,例如,为myapp中的BlogPost创建一个can_publish权限:

方法1 meta中配置

  1. class BlogPost(models.Model):
  2.     ...
  3.     class Meta:
  4.         permissions = (
  5.             ("can_publish", "Can Publish Posts"),
  6.         )

方法2 使用create()函数

  1. from myapp.models import BlogPost
  2. from django.contrib.auth.models import Permission
  3. from django.contrib.contenttypes.models import ContentType
  4. content_type = ContentType.objects.get_for_model(BlogPost)
  5. permission = Permission.objects.create(
  6. codename='can_publish',
  7. name='Can Publish Posts',
  8. content_type=content_type,
  9. )

在使用python manage.py migrate命令后,就会创建这个新权限,接着就可以在view中编写代码判断用户是否有这个权限来决定能否发表文章。

扩展User模型

代理模型

如果不需要修改表结构,只扩展行为,那么可以使用代理模型。示例:

  1. from django.contrib.auth.models import User
  2. class MyUser(User):
  3. class Meta:
  4. proxy = True
  5. def do_something(self):
  6. # ...
  7. pass

OneToOneField

如果需要扩展字段,那么可以使用OneToOneField。示例:

  1. from django.contrib.auth.models import User
  2. class Employee(models.Model):
  3. user = models.OneToOneField(User, on_delete=models.CASCADE)
  4. department = models.CharField(max_length=100)

这样会新增一张表:

  1. CREATE TABLE `user_employee` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `department` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  4. `user_id` int(11) NOT NULL,
  5. PRIMARY KEY (`id`),
  6. UNIQUE KEY `user_id` (`user_id`),
  7. CONSTRAINT `user_employee_user_id_9b2edd10_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
  8. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

在代码中使用User也能访问到Employee的属性:

  1. >>> u = User.objects.get(username='fsmith')
  2. >>> freds_department = u.employee.department

虽然这种方式能实现扩展,但是OneToOneField会增加数据库查询的复杂度,加重数据库处理负担,并不建议采用。

替换User模型

新版Django的推荐做法是,如果不想用默认User模型,那么就把它替换掉。Django除了User模型,还有2个抽象模型AbstractUser和AbstractBaseUser,从源码中可以看到它们的继承关系:

  1. class User(AbstractUser):
  2. class AbstractUser(AbstractBaseUser, PermissionsMixin):
  3. class AbstractBaseUser(models.Model):

为什么不用User模型,还要做2个抽象模型呢?这是因为一般继承有2个用途,一是继承父类的属性和方法,并做出自己的改变或扩展,实现代码重用。但是这种方式会导致子类也包含了父类的实现代码,代码强耦合,所以实践中不会这么做。而是采用第二种方式,把共性的内容抽象出来,只定义属性和方法,不提供具体实现(如java中的接口类),并且只能被继承,不能被实例化。AbstractUser和AbstractBaseUser就是对User的不同程度的抽象,AbstractUser是User的完整实现,可用于扩展User,AbstractBaseUser是高度抽象,可用于完全自定义User。

继承AbstractUser

除了代理模型和OneToOneField,扩展User的新方式是定义新的MyUser并继承AbstractUser,把User替换掉,再添加额外信息。具体操作步骤我们通过示例来了解:

替换User最好是创建项目后,首次python manage.py migrate前,就进行替换,否则数据库的表已经生成,再中途替换,会有各种各样的依赖问题,只能手动解决。

第一步,myapp.models中新建MyUser,继承AbstractUser:

  1. from django.contrib.auth.models import AbstractUser
  2. class MyUser(AbstractUser):
  3. pass

第二步,settings.py中配置AUTH_USER_MODEL,指定新的用户模型:

  1. AUTH_USER_MODEL = 'myapp.MyUser'

第三步,settings.py中配置INSTALLED_APPS:

  1. INSTALLED_APPS = [
  2. 'django.contrib.admin',
  3. 'django.contrib.auth',
  4. 'django.contrib.contenttypes',
  5. 'django.contrib.sessions',
  6. 'django.contrib.messages',
  7. 'django.contrib.staticfiles',
  8. 'myapp.apps.MyappConfig' # 新增
  9. ]

第四步(可选),如果需要使用Django自带管理后台,那么要在admin.py中注册:

  1. from django.contrib import admin
  2. from django.contrib.auth.admin import UserAdmin
  3. from .models import MyUser
  4. admin.site.register(MyUser, UserAdmin)

我们看下数据库中的效果,提交数据迁移:

  1. python manage.py makemigrations

执行数据迁移:

  1. python manage.py migrate

从表能看出来,默认User已经替换为MyUser了:

替换之后,就可以进行扩展了。比如自定义表名:

  1. from django.contrib.auth.models import AbstractUser
  2. class MyUser(AbstractUser):
  3. class Meta:
  4. db_table = "user"
  5. pass

替换User后,就不能直接引用django.contrib.auth.models.User了,可以使用get_user_model()函数或者settings.AUTH_USER_MODEL

继承AbstractBaseUser

继承AbstractUser只能做扩展,如果我们想完全自定义用户模型,那么就需要继承AbstractBaseUser,再重写属性和方法。

USERNAME_FIELD

USERNAME_FIELD是用户模型的唯一标识符,不一定是username,也可以是email、phone等。

唯一标识符是Django认证后端的要求,如果你实现了自定义认证后端,那么也可以用非唯一标识符作为USERNAME_FIELD。

我们可以参考AbstractUser的实现:

  1. username = models.CharField(
  2. _('username'),
  3. max_length=150,
  4. unique=True,
  5. help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
  6. validators=[username_validator],
  7. error_messages={
  8. 'unique': _("A user with that username already exists."),
  9. },
  10. )
  11. USERNAME_FIELD = 'username'

修改为自定义:

  1. class MyUser(AbstractBaseUser):
  2. identifier = models.CharField(max_length=40, unique=True)
  3. ...
  4. USERNAME_FIELD = 'identifier'

EMAIL_FIELD

参考AbstractUser的实现:

  1. email = models.EmailField(_('email address'), blank=True)
  2. EMAIL_FIELD = 'email'

REQUIRED_FIELDS

REQUIRED_FIELDS是指必填字段。参考AbstractUser的实现:

  1. REQUIRED_FIELDS = ['email']

这表示email是必填的,在使用createsuperuser命令时,会提示必须输入。

修改为自定义:

  1. class MyUser(AbstractBaseUser):
  2. ...
  3. date_of_birth = models.DateField()
  4. height = models.FloatField()
  5. ...
  6. REQUIRED_FIELDS = ['date_of_birth', 'height']

不需要再填USERNAME_FIELD和password,因为Django已经默认包含了,只需要填其他字段即可。

is_active

可以用来做软删(不删除数据而是把is_active置为False)。参考AbstractUser的实现:

  1. is_active = models.BooleanField(
  2. _('active'),
  3. default=True,
  4. help_text=_(
  5. 'Designates whether this user should be treated as active. '
  6. 'Unselect this instead of deleting accounts.'
  7. ),
  8. )

get_full_name()

参考AbstractUser的实现:

  1. def get_full_name(self):
  2. """
  3. Return the first_name plus the last_name, with a space in between.
  4. """
  5. full_name = '%s %s' % (self.first_name, self.last_name)
  6. return full_name.strip()

get_short_name()

参考AbstractUser的实现:

  1. def get_short_name(self):
  2. """Return the short name for the user."""
  3. return self.first_name

更多属性和方法请看源码。

查看源码的方法:在from django.contrib.auth.models import AbstractBaseUser代码上,按住CTRL点击AbstractBaseUser即可。

重写manager

如果自定义用户模型改变了username, email, is_staff, is_active, is_superuser, last_login, and date_joined字段,那么可能需要继承BaseUserManager,并重写以下2个方法:

create_user(username_field, password=None, **other_fields)

create_user(username_field, password=None, **other_fields)

示例:

  1. from django.contrib.auth.models import BaseUserManager
  2. class CustomUserManager(BaseUserManager):
  3. def create_user(self, email, date_of_birth, password=None):
  4. # create user here
  5. ...
  6. def create_superuser(self, email, date_of_birth, password=None):
  7. # create superuser here
  8. ...

重写权限

从AbstractUser的定义可以看到是继承了PermissionsMixin类的:

  1. class AbstractUser(AbstractBaseUser, PermissionsMixin):

所以重写权限就是重写PermissionsMixin的属性和方法,如get_user_permissions()、has_perm()等。

一个完整示例

我们把email作为USERNAME_FIELD,并且让date_of_birth必填。

models.py

  1. from django.db import models
  2. from django.contrib.auth.models import (
  3. BaseUserManager, AbstractBaseUser
  4. )
  5. class MyUserManager(BaseUserManager):
  6. def create_user(self, email, date_of_birth, password=None):
  7. """
  8. Creates and saves a User with the given email, date of
  9. birth and password.
  10. """
  11. if not email:
  12. raise ValueError('Users must have an email address')
  13. user = self.model(
  14. email=self.normalize_email(email),
  15. date_of_birth=date_of_birth,
  16. )
  17. user.set_password(password)
  18. user.save(using=self._db)
  19. return user
  20. def create_superuser(self, email, date_of_birth, password=None):
  21. """
  22. Creates and saves a superuser with the given email, date of
  23. birth and password.
  24. """
  25. user = self.create_user(
  26. email,
  27. password=password,
  28. date_of_birth=date_of_birth,
  29. )
  30. user.is_admin = True
  31. user.save(using=self._db)
  32. return user
  33. class MyUser(AbstractBaseUser):
  34. email = models.EmailField(
  35. verbose_name='email address',
  36. max_length=255,
  37. unique=True,
  38. )
  39. date_of_birth = models.DateField()
  40. is_active = models.BooleanField(default=True)
  41. is_admin = models.BooleanField(default=False)
  42. objects = MyUserManager()
  43. USERNAME_FIELD = 'email'
  44. REQUIRED_FIELDS = ['date_of_birth']
  45. def __str__(self):
  46. return self.email
  47. def has_perm(self, perm, obj=None):
  48. "Does the user have a specific permission?"
  49. # Simplest possible answer: Yes, always
  50. return True
  51. def has_module_perms(self, app_label):
  52. "Does the user have permissions to view the app `app_label`?"
  53. # Simplest possible answer: Yes, always
  54. return True
  55. @property
  56. def is_staff(self):
  57. "Is the user a member of staff?"
  58. # Simplest possible answer: All admins are staff
  59. return self.is_admin

不要忘了在settings.py中修改AUTH_USER_MODEL哦:

  1. AUTH_USER_MODEL = 'customauth.MyUser'

东方说

纯技术文太单调,不如来点小吐槽。写了这2篇关于Django认证系统的文章,明白了以前似懂非懂的技术细节。如果平时有需求想自己做个小网站,完全可以用Django来快速实现后端,开箱即用还是有点香。Template和Form不属于前后端分离的技术,在学习时可以选择性跳过。公众号后台回复“加群”,“Python互助讨论群”欢迎你。

参考资料:

https://docs.djangoproject.com/en/3.1/topics/auth/customizing/

自定义Django认证系统的技术方案的更多相关文章

  1. Django认证系统auth认证

    使用Django认证系统auth认证 auth认证系统可以处理范围非常广泛的任务,且具有一套细致的密码和权限实现.对于需要与默认配置不同需求的项目,Django支持扩展和自定义认证;会将用户信息写入到 ...

  2. django认证系统 Authentication

    Django自带一个用户认证系统,用于处理用户账户.群组.许可和基于cookie的用户会话. Django的认证系统包含了身份验证和权限管理两部分.简单地说,身份验证用于核实某个用户是否合法,权限管理 ...

  3. django 认证系统--3

    WEB request中的认证 django使用sessions和middleware和reqeust对象联系在一起 它们通过给每一个reqeust请求添加一个request.user属性来代表当前用 ...

  4. Django认证系统并不鸡肋反而很重要

    在使用django-admin startproject创建项目后,Django就默认安装了一个采用session实现的认证系统.这是Django相比于其他框架的一大特点:自带认证系统,开箱即用.有人 ...

  5. Django自定义用户认证系统之自定义用户模型

    参考文档:http://python.usyiyi.cn/django/topics/auth/customizing.html Django 自带的认证系统足够应付大多数情况,但你或许不打算使用现成 ...

  6. django自定义user认证系统

    第一种,彻底推翻django的user认证系统,重新定义我们想要的字段 from django.contrib.auth.models import AbstractBaseUser,Permissi ...

  7. django 认证系统--2

    使用django的认证系统 User 对象 User是认证系统的核心.典型代表是用户和你的站点进行交互还有限制访问.注册用户等等.django认证框架中,只存在一个User类,像'superuser' ...

  8. Django认证系统实现的web页面

    结合数据库.ajax.js.Djangoform表单和认证系统的web页面 一:数据模块 扩展了Django中的user表,增加了自定义的字段 from django.db import models ...

  9. django 认证系统--1

    django的认证系统提供认证和授权两种功能 认证系统包括如下部分: 1.Users 2.Permissions 主要是以 YES/NO 的形式反映一个用户是否能够做某事 3.Groups:就是对多个 ...

随机推荐

  1. 关于点击弹框外部区域弹框关闭的交互处理(前端JS)

    常见需求场景 前端在处理交互的时候,经常遇到这样的场景,点击一个按钮,出现一个弹框,点击外部区域,弹框关闭. 解决方法 思路说明: 1.给弹框的div父级都加个类名,如: 2.在document绑定一 ...

  2. 我要进大厂之大数据Hadoop HDFS知识点(2)

    01 我们一起学大数据 老刘继续分享出Hadoop中的HDFS模块的一些高级知识点,也算是对今天复习的HDFS内容进行一次总结,希望能够给想学大数据的同学一点帮助,也希望能够得到大佬们的批评和指点! ...

  3. BurpSuite 抓安卓app包

    认准夜神安卓模拟器,下了几个其他的模拟器都没有代理的功能 一.夜神安卓模拟器设置代理 1.点击右下角的设置 2.点击设置,进入到WIFI连接选项 3.进入列表后,鼠标点击wifi位置,长按左键,会出现 ...

  4. yum 方式安装mysql (完整记录)

    2016-04-07 学习笔记,源代码安装比较麻烦,还是要尝试一下yum安装和rpm方式安装 一.检查系统是否安装老版本,有的话干掉 #yum list installed | grep mysqlm ...

  5. overflow:scroll

    <div style="position: relative;"> <div class="container mycontent" styl ...

  6. BT下载器Folx标签功能怎么实现自动的资源分类

    很多经典的电影作品,比如魔戒三部曲.蜘蛛侠系列.漫威动画系列等,在一个系列中都会包含多个作品.如果使用Folx bt种子下载器自带的电影标签的话,会将这些系列电影都归为"电影"标签 ...

  7. 如何使用ABBYY FineReader 处理无法识别的字符?

    在识别PDF文档时,我们可能会遇到文档中存在多种语言.多种不同类型文字字符的情况.在ABBYY FineReader 15(Windows系统)OCR文字识别软件的默认语言数据下,可能无法识别PDF文 ...

  8. 5w 字 | 172 图 | 超级赛亚级 Spring Cloud 实战

    一.PassJava 项目简介 PassJava-Learning 项目是 PassJava(佳必过)项目的学习教程.对架构.业务.技术要点进行讲解. PassJava 是一款 Java 面试刷题 的 ...

  9. HarmonyOS Java UI之DependentLayout布局示例

    DependentLayout简介 DependentLayout意为相对位置布局,与DirectionalLayout相比较有更多的排布方式,每个组件可以指定相对于其他同级组件的位置,也可以指定相对 ...

  10. JDBC【2】-- JDBC工作原理以及简单封装

    目录 1. 工作原理 1.1 加载驱动 1.1.1 类加载相关知识 1.1.2 为什么JDK 1.6之后不需要显示加载了? 1.2 驱动加载完成了,然后呢? 2. 简单封装 1. 工作原理 一般我们主 ...