Django已经提供了开箱即用的认证系统,但是可能并不满足我们的个性化需求。自定义认证系统需要知道哪些地方可以扩展,哪些地方可以替换。本文就来介绍自定义Django认证系统的相关技术细节。
自定义认证后端AUTHENTICATION_BACKENDSDjango默认认证后端为:
['django.contrib.auth.backends.ModelBackend']
可以在settings.py
中配置AUTHENTICATION_BACKENDS为自定义的认证后端,其本质是Python class,在调用django.contrib.auth.authenticate()
时会进行遍历:
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异常,也会停止认证。
编写认证后端如果修改了认证后端,想要用户重新认证,那么需要调用
Session.objects.all().delete()
清除session数据,因为session中会缓存已认证过的认证后端。
先看看默认认证后端的源码片段:
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
总结一下:
继承BaseBackend。
实现了authenticate()
。(backend也有个authenticate方法,跟django.contrib.auth.authenticate()
不一样哦)authenticate(request=None,**credentials)
方法的第一个入参是request
,可为空,第二个入参是credentials(用户凭证如用户名、密码),示例:
from django.contrib.auth.backends import BaseBackendclass MyBackend(BaseBackend): def authenticate(self,password=None): # Check the username/password and return a user. ...
用户凭证也可以是token:
from django.contrib.auth.backends import BaseBackendclass MyBackend(BaseBackend): def authenticate(self,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 settingsfrom django.contrib.auth.backends import BaseBackendfrom django.contrib.auth.hashers import check_passwordfrom django.contrib.auth.models import Userclass SettingsBackend(BaseBackend): """ 认证settings中admin_LOGIN和admin_PASSWORD变量,比如: admin_LOGIN = 'admin' admin_PASSWORD = 'pbkdf2_sha256000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/OQ7fVOu1XAURIZYoOZ3iq8Dr4M=' """ def authenticate(self,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 BaseBackendclass 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 BlogPostfrom django.contrib.auth.models import Permissionfrom django.contrib.ContentTypes.models import ContentTypecontent_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 Userclass MyUser(User): class Meta: proxy = True def do_something(self): # ... pass
OnetoOneFIEld如果需要扩展字段,那么可以使用OnetoOneFIEld。示例:
from django.contrib.auth.models import Userclass 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
替换User模型虽然这种方式能实现扩展,但是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。
继承AbstractUser除了代理模型和OnetoOneFIEld,扩展User的新方式是定义新的MyUser并继承AbstractUser,把User替换掉,再添加额外信息。具体 *** 作步骤我们通过示例来了解:
替换User最好是创建项目后,首次
python manage.py migrate
前,就进行替换,否则数据库的表已经生成,再中途替换,会有各种各样的依赖问题,只能手动解决。
第一步,myapp.models中新建MyUser,继承AbstractUser:
from django.contrib.auth.models import AbstractUserclass 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 adminfrom django.contrib.auth.admin import Useradminfrom .models import MyUseradmin.site.register(MyUser,Useradmin)
我们看下数据库中的效果,提交数据迁移:
python manage.py makemigrations
执行数据迁移:
python manage.py migrate
从表能看出来,默认User已经替换为MyUser了:
替换之后,就可以进行扩展了。比如自定义表名:
from django.contrib.auth.models import AbstractUserclass MyUser(AbstractUser): class Meta: db_table = "user" pass
继承AbstractBaseUser替换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
更多属性和方法请看源码。
重写manager查看源码的方法:在
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,**other_fIElds)
create_user(username_fIEld,**other_fIElds)
示例:
from django.contrib.auth.models import BaseUserManagerclass CustomUserManager(BaseUserManager): def create_user(self,date_of_birth,password=None): # create user here ... def create_superuser(self,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 modelsfrom django.contrib.auth.models import ( BaseUserManager,AbstractBaseUser)class MyUserManager(BaseUserManager): def create_user(self,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,password=None): """ Creates and saves a superuser with the given email,date of birth and password. """ user = self.create_user( email,password=password,) user.is_admin = True user.save(using=self._db) return userclass MyUser(AbstractBaseUser): email = models.EmailFIEld( verbose_name='email address',max_length=255,) 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,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/
以上是内存溢出为你收集整理的自定义Django认证系统的技术方案全部内容,希望文章能够帮你解决自定义Django认证系统的技术方案所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)