Django已經提供了開箱即用的認證系統,可是可能並不知足咱們的個性化需求。自定義認證系統須要知道哪些地方能夠擴展,哪些地方能夠替換。本文就來介紹自定義Django認證系統的相關技術細節。java
Django默認認證後端爲:python
['django.contrib.auth.backends.ModelBackend']
能夠在settings.py
中配置AUTHENTICATION_BACKENDS爲自定義的認證後端,其本質是Python class,在調用django.contrib.auth.authenticate()
時會進行遍歷:mysql
def authenticate(request=None, **credentials): """ If the given credentials are valid, return a User object. """ for backend, backend_path in _get_backends(return_tuples=True): backend_signature = inspect.signature(backend.authenticate) try: backend_signature.bind(request, **credentials) except TypeError: # This backend doesn't accept these credentials as arguments. Try the next one. continue try: user = backend.authenticate(request, **credentials) except PermissionDenied: # This backend says to stop in our tracks - this user should not be allowed in at all. break if user is None: continue # Annotate the user object with the path of the backend. user.backend = backend_path return user # The credentials supplied are invalid to all backends, fire signal user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
列表中的認證後端是有前後順序的,Django會依次進行認證,只要有後端認證成功,就會結束認證,若是有後端拋出PermissionDenied異常,也會中止認證。git
若是修改了認證後端,想要用戶從新認證,那麼須要調用
Session.objects.all().delete()
清除session數據,由於session中會緩存已認證過的認證後端。sql
先看看默認認證後端的源碼片斷:數據庫
class ModelBackend(BaseBackend): """ Authenticates against settings.AUTH_USER_MODEL. """ def authenticate(self, request, username=None, password=None, **kwargs): if username is None: username = kwargs.get(UserModel.USERNAME_FIELD) if username is None or password is None: return try: user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: # Run the default password hasher once to reduce the timing # difference between an existing and a nonexistent user (#20760). UserModel().set_password(password) else: if user.check_password(password) and self.user_can_authenticate(user): return user ... def get_user(self, user_id): try: user = UserModel._default_manager.get(pk=user_id) except UserModel.DoesNotExist: return None return user if self.user_can_authenticate(user) else None
總結一下:django
繼承BaseBackend。後端
實現了authenticate()
。(backend也有個authenticate方法,跟django.contrib.auth.authenticate()
不同哦)authenticate(request=None, **credentials)
方法的第一個入參是request
,可爲空,第二個入參是credentials(用戶憑證如用戶名、密碼),示例:緩存
from django.contrib.auth.backends import BaseBackend class MyBackend(BaseBackend): def authenticate(self, request, username=None, password=None): # Check the username/password and return a user. ...
用戶憑證也能夠是token:服務器
from django.contrib.auth.backends import BaseBackend class MyBackend(BaseBackend): def authenticate(self, request, token=None): # Check the token and return a user. ...
若是認證成功就返回User對象,若是認證失敗就返回None。
實現了get_user()
。get_user(user_id)
方法入參是user_id,能夠是username/數據庫ID等,必須是User的主鍵,返回值爲User對象或者None。
咱們試着來編寫一個認證後端,爲了演示效果,咱們不用客戶端服務器模式,而是在settings.py
文件中增長2個配置,而後用咱們自定義的認證後端進行認證,代碼以下:
from django.conf import settings from django.contrib.auth.backends import BaseBackend from django.contrib.auth.hashers import check_password from django.contrib.auth.models import User class SettingsBackend(BaseBackend): """ 認證settings中ADMIN_LOGIN和ADMIN_PASSWORD變量,好比: ADMIN_LOGIN = 'admin' ADMIN_PASSWORD = 'pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M=' """ def authenticate(self, request, username=None, password=None): login_valid = (settings.ADMIN_LOGIN == username) pwd_valid = check_password(password, settings.ADMIN_PASSWORD) if login_valid and pwd_valid: try: user = User.objects.get(username=username) except User.DoesNotExist: # 建立一個新用戶 user = User(username=username) user.is_staff = True user.is_superuser = True user.save() return user return None def get_user(self, user_id): try: return User.objects.get(pk=user_id) except User.DoesNotExist: return None
認證後端能夠重寫方法get_user_permissions()
, get_group_permissions()
, get_all_permissions()
, has_perm()
, has_module_perms()
, with_perm()
來實現受權。示例:
from django.contrib.auth.backends import BaseBackend class MagicAdminBackend(BaseBackend): def has_perm(self, user_obj, perm, obj=None): # 若是是超管,就會得到全部權限,由於無論perm是什麼,都返回True 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中配置
class BlogPost(models.Model): ... class Meta: permissions = ( ("can_publish", "Can Publish Posts"), )
方法2 使用create()
函數
from myapp.models import BlogPost from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType content_type = ContentType.objects.get_for_model(BlogPost) permission = Permission.objects.create( codename='can_publish', name='Can Publish Posts', content_type=content_type, )
在使用python manage.py migrate
命令後,就會建立這個新權限,接着就能夠在view中編寫代碼判斷用戶是否有這個權限來決定可否發表文章。
若是不須要修改表結構,只擴展行爲,那麼可使用代理模型。示例:
from django.contrib.auth.models import User class MyUser(User): class Meta: proxy = True def do_something(self): # ... pass
若是須要擴展字段,那麼可使用OneToOneField。示例:
from django.contrib.auth.models import User class Employee(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) department = models.CharField(max_length=100)
這樣會新增一張表:
CREATE TABLE `user_employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `department` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `user_id` int(11) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `user_id` (`user_id`), CONSTRAINT `user_employee_user_id_9b2edd10_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
在代碼中使用User也能訪問到Employee的屬性:
>>> u = User.objects.get(username='fsmith') >>> freds_department = u.employee.department
雖然這種方式能實現擴展,可是OneToOneField會增長數據庫查詢的複雜度,加劇數據庫處理負擔,並不建議採用。
新版Django的推薦作法是,若是不想用默認User模型,那麼就把它替換掉。Django除了User模型,還有2個抽象模型AbstractUser和AbstractBaseUser,從源碼中能夠看到它們的繼承關係:
class User(AbstractUser): class AbstractUser(AbstractBaseUser, PermissionsMixin): class AbstractBaseUser(models.Model):
爲何不用User模型,還要作2個抽象模型呢?這是由於通常繼承有2個用途,一是繼承父類的屬性和方法,並作出本身的改變或擴展,實現代碼重用。可是這種方式會致使子類也包含了父類的實現代碼,代碼強耦合,因此實踐中不會這麼作。而是採用第二種方式,把共性的內容抽象出來,只定義屬性和方法,不提供具體實現(如java中的接口類),而且只能被繼承,不能被實例化。AbstractUser和AbstractBaseUser就是對User的不一樣程度的抽象,AbstractUser是User的完整實現,可用於擴展User,AbstractBaseUser是高度抽象,可用於徹底自定義User。
除了代理模型和OneToOneField,擴展User的新方式是定義新的MyUser並繼承AbstractUser,把User替換掉,再添加額外信息。具體操做步驟咱們經過示例來了解:
替換User最好是建立項目後,首次
python manage.py migrate
前,就進行替換,不然數據庫的表已經生成,再中途替換,會有各類各樣的依賴問題,只能手動解決。
第一步,myapp.models中新建MyUser,繼承AbstractUser:
from django.contrib.auth.models import AbstractUser class MyUser(AbstractUser): pass
第二步,settings.py
中配置AUTH_USER_MODEL,指定新的用戶模型:
AUTH_USER_MODEL = 'myapp.MyUser'
第三步,settings.py
中配置INSTALLED_APPS:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'myapp.apps.MyappConfig' # 新增 ]
第四步(可選),若是須要使用Django自帶管理後臺,那麼要在admin.py
中註冊:
from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import MyUser admin.site.register(MyUser, UserAdmin)
咱們看下數據庫中的效果,提交數據遷移:
python manage.py makemigrations
執行數據遷移:
python manage.py migrate
從表能看出來,默認User已經替換爲MyUser了:
替換以後,就能夠進行擴展了。好比自定義表名:
from django.contrib.auth.models import AbstractUser class MyUser(AbstractUser): class Meta: db_table = "user" pass
替換User後,就不能直接引用
django.contrib.auth.models.User
了,可使用get_user_model()
函數或者settings.AUTH_USER_MODEL
。
繼承AbstractUser只能作擴展,若是咱們想徹底自定義用戶模型,那麼就須要繼承AbstractBaseUser,再重寫屬性和方法。
USERNAME_FIELD
USERNAME_FIELD是用戶模型的惟一標識符,不必定是username,也能夠是email、phone等。
惟一標識符是Django認證後端的要求,若是你實現了自定義認證後端,那麼也能夠用非惟一標識符做爲USERNAME_FIELD。
咱們能夠參考AbstractUser的實現:
username = models.CharField( _('username'), max_length=150, unique=True, help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), validators=[username_validator], error_messages={ 'unique': _("A user with that username already exists."), }, ) USERNAME_FIELD = 'username'
修改成自定義:
class MyUser(AbstractBaseUser): identifier = models.CharField(max_length=40, unique=True) ... USERNAME_FIELD = 'identifier'
EMAIL_FIELD
參考AbstractUser的實現:
email = models.EmailField(_('email address'), blank=True) EMAIL_FIELD = 'email'
REQUIRED_FIELDS
REQUIRED_FIELDS是指必填字段。參考AbstractUser的實現:
REQUIRED_FIELDS = ['email']
這表示email是必填的,在使用createsuperuser
命令時,會提示必須輸入。
修改成自定義:
class MyUser(AbstractBaseUser): ... date_of_birth = models.DateField() height = models.FloatField() ... REQUIRED_FIELDS = ['date_of_birth', 'height']
不須要再填USERNAME_FIELD和password,由於Django已經默認包含了,只須要填其餘字段便可。
is_active
能夠用來作軟刪(不刪除數據而是把is_active置爲False)。參考AbstractUser的實現:
is_active = models.BooleanField( _('active'), default=True, help_text=_( 'Designates whether this user should be treated as active. ' 'Unselect this instead of deleting accounts.' ), )
get_full_name()
參考AbstractUser的實現:
def get_full_name(self): """ Return the first_name plus the last_name, with a space in between. """ full_name = '%s %s' % (self.first_name, self.last_name) return full_name.strip()
get_short_name()
參考AbstractUser的實現:
def get_short_name(self): """Return the short name for the user.""" return self.first_name
更多屬性和方法請看源碼。
查看源碼的方法:在
from django.contrib.auth.models import AbstractBaseUser
代碼上,按住CTRL
點擊AbstractBaseUser
便可。
若是自定義用戶模型改變了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)
示例:
from django.contrib.auth.models import BaseUserManager class CustomUserManager(BaseUserManager): def create_user(self, email, date_of_birth, password=None): # create user here ... def create_superuser(self, email, date_of_birth, password=None): # create superuser here ...
從AbstractUser的定義能夠看到是繼承了PermissionsMixin類的:
class AbstractUser(AbstractBaseUser, PermissionsMixin):
因此重寫權限就是重寫PermissionsMixin的屬性和方法,如get_user_permissions()、has_perm()等。
咱們把email做爲USERNAME_FIELD,而且讓date_of_birth必填。
models.py
from django.db import models from django.contrib.auth.models import ( BaseUserManager, AbstractBaseUser ) class MyUserManager(BaseUserManager): def create_user(self, email, date_of_birth, password=None): """ Creates and saves a User with the given email, date of birth and password. """ if not email: raise ValueError('Users must have an email address') user = self.model( email=self.normalize_email(email), date_of_birth=date_of_birth, ) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, date_of_birth, password=None): """ Creates and saves a superuser with the given email, date of birth and password. """ user = self.create_user( email, password=password, date_of_birth=date_of_birth, ) user.is_admin = True user.save(using=self._db) return user class MyUser(AbstractBaseUser): email = models.EmailField( verbose_name='email address', max_length=255, unique=True, ) date_of_birth = models.DateField() is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) objects = MyUserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['date_of_birth'] def __str__(self): return self.email def has_perm(self, perm, obj=None): "Does the user have a specific permission?" # Simplest possible answer: Yes, always return True def has_module_perms(self, app_label): "Does the user have permissions to view the app `app_label`?" # Simplest possible answer: Yes, always return True @property def is_staff(self): "Is the user a member of staff?" # Simplest possible answer: All admins are staff return self.is_admin
不要忘了在settings.py中修改AUTH_USER_MODEL哦:
AUTH_USER_MODEL = 'customauth.MyUser'
純技術文太單調,不如來點小吐槽。寫了這2篇關於Django認證系統的文章,明白了之前似懂非懂的技術細節。若是平時有需求想本身作個小網站,徹底能夠用Django來快速實現後端,開箱即用仍是有點香。Template和Form不屬於先後端分離的技術,在學習時能夠選擇性跳過。公衆號後臺回覆「加羣」,「Python互助討論羣」歡迎你。
參考資料:
https://docs.djangoproject.com/en/3.1/topics/auth/customizing/