前言
最近又要用到django去搭建網站,之前用的東西需要花一些時間去恢復記憶,同時還需要學習一些新的知識,進一步提升自己使用django的能力,因此寫這篇博客持續更新使用過程中積累的一些技巧,知識和用法等。
目前我自己寫了一個認證系統,包括登入登出,註冊激活,密碼找回等功能,放在了github倉庫中,有使用django-registration-redux不適合的,或者需要個性化定製認證功能的,可以考慮使用和優化這個認證系統。以下的代碼大部分來自於該項目。
發送郵件設置
要使用django發送郵件,事先需要做一些設置,這裏我以騰訊企業郵箱爲例。
- 打開郵箱的smtp發送郵件服務,一般在設置中的客戶端設置中
- 然後在yourapp/settings中設置如下內容
EMAIL_HOST = 'smtp.exmail.qq.com'
EMAIL_PORT = '465'
EMAIL_HOST_USER = '[email protected]'
EMAIL_HOST_PASSWORD = 'xxxx'
DEFAULT_FROM_EMAIL = '[email protected]'
EMAIL_USE_SSL = True
- 之後使用django.core.mail中的send_mail發送郵件即可
from django.core.mail import send_mail
send_mail(subject,message,from_email,recipient_list)
#subject:發送主題
#message:發送內容
#from_email:對應settings中的DEFAULT_FROM_EMAIL
#recipient_list 發送人列表,例如[[email protected], [email protected]]
保存用戶密碼
實際上,無論是django.forms.Form,還是django.forms.ModelForm,都僅僅是發揮表單提交和表單驗證的功能,用戶的密碼在存到數據庫的時候是不會加密的,因此,需要手動加密,一般的方法,這裏引用django基礎教程中的方法,先保存form模型到數據庫,然後再根據用戶名提取到user對象,之後再使用user.set_password()的方法給密碼加密,例子如下:
if request.method == 'POST':
form = Registration_Form(request.POST)
if form.is_valid():
userform = form.save(commit=False)
userform.is_active = False #初始用戶未激活
userform.save()
username = form.cleaned_data['username']
email = form.cleaned_data['email']
user = User.objects.get(username=username)
user.set_password(user.password) #form保存時不會自動加密,因此這裏需要手動加密
user.userauth = UserAuth(user=user)
user.save()
user.userauth.send_activation_email(request,purpose='registration')
return render(request,'user_auth/registration_complete.html',{'username':username,
'email':email})
else:
print(form.errors)
return render(request,'user_auth/registration.html',{'form':form})
form = Registration_Form()
return render(request,'user_auth/registration.html',{'form':form})
自定義form表單驗證
要對錶單的一些字段或者內容進行自定義驗證的話,可以通過在form 類中添加clean()和clean_yourfield()方法來增加驗證內容,如下的例子,自定義方法驗證郵箱的唯一性和兩次輸入的密碼是否相同的form類,需要注意的是django默認的User中郵箱是可以重複的,因此我們需要自己去檢查郵箱的唯一性。另外,除去User模型中有的字段,我們可以額外添加一些字段或者重寫其中的字段,而Meta類中的field是有順序的,如果你在模板中使用for循環來遍歷字段的話,它將決定最終字段的顯示的順序。
class Registration_Form(forms.ModelForm):
username = forms.CharField(max_length=64,label='用戶名',
widget=forms.TextInput(attrs={'placeholder':'用戶名'}),
# error_messages={'required':'不能爲空','max_length':'要求64個字符以內'}
)
confirm_password = forms.CharField(label='重新輸入密碼',max_length=64,widget=forms.PasswordInput(attrs={'class':'form-control',
'placeholder':'重複輸入密碼',
}))
password = forms.CharField(label='登陸密碼',max_length=64,widget=forms.PasswordInput(attrs={'class':'form-control',
'placeholder':'登陸密碼',
}))
email = forms.EmailField(label='用戶郵箱',widget=forms.EmailInput(attrs={'placeholder':'用戶郵箱'}))
class Meta:
model = User
fields = ['username','password','confirm_password','email'] #添加confirm_password以調整順序
def clean(self):
if self.cleaned_data['password'] != self.cleaned_data['confirm_password']:
raise forms.ValidationError('兩次密碼輸入不一致,請檢查!',code='password error')
return self.cleaned_data
def clean_email(self): #函數名是嚴格限制的 clean+下劃線+字段
"""
用於驗證郵箱的唯一性
"""
if User.objects.filter(email__exact=self.cleaned_data['email']):
raise forms.ValidationError('該Email已經註冊過了,請檢查!',code='invalid email')
return self.cleaned_data['email']
可以看到,這裏還使用了forms.ValidationError來自定義錯誤說明,它們最終將被添加到form.errors中。
這裏特別說明的是在ModelForm中的字段,最終是先驗證ModelForm中的字段,然後再驗證自定義的字段,因此如果我這裏想寫一個clean_password(self)的方法來使用self.cleaned_data[‘confirm_password’]就會KeyError的的錯誤,因此最好的額辦法是寫一個clean_confirm_password(self)的方法中引用self.cleaned_data[‘password’],因爲此時的password的key已經存在了!
模板中有多個參數的url寫法
{% url 'user_auth:registration_resend_email' username email %}
這裏的username和email都是即將傳遞給名爲registration_resend_email的URL的參數,最終找到的URL如下:
'registration/resend_email/<username>/<email>/'
激活碼的生成
通過查看django-registration-redux的源碼,學習到了如何將用戶激活的功能集成到User的關聯模型中,我們只需要創建一個與User 有OneToOneField()的Userauth模型,然後在該模型下寫認證功能,可以直接引用self.user和self.save()以及self.yourfield來構建激活碼的生成及驗證,以下是代碼:
from datetime import datetime,timedelta
import hashlib
import string
import os
from django.db import models
from django.contrib.auth.models import User
from django.core.mail import send_mail
from django.utils.crypto import get_random_string
from django.conf import settings
class UserAuth(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE)
activation_time = models.DateTimeField(default=datetime.now())
activation_key = models.CharField(max_length=64,blank=True)
activation_valid = models.BooleanField(default=False)
def __str__(self):
return self.user
def generate_activation_key(self,save=True):
"""
生成64位隨機激活碼
"""
random_string = get_random_string(length=32,allowed_chars=string.printable)
self.activation_key = hashlib.sha256(random_string.encode('utf-8')).hexdigest()
if save:
self.save()
return self.activation_key
def send_activation_email(self,request,purpose='reset_password'):
"""
用於給用戶發送註冊確認郵件或者密碼重置激活郵件
purpose = [ reset_password,registration ]
這裏特別需要注意的是activation_url的構建
purpose的設置是以urls.py中設置的路徑關聯的
因爲在urls.py中設置了激活URL爲:
resent_password/activation/(P<activation_key>[\w-]+)
registration/activation/(P<activation_key>[\w-]+)
因此,這裏我拼了一個這樣的激活路徑,如果後續修改了url,那麼
這裏的activation_url也必須要修改以適應新的激活URL
"""
activation_key = self.generate_activation_key(save=True)
activation_url = 'http://' + "/".join([request.META['HTTP_HOST'],
'accounts',purpose,'activation',activation_key])
if purpose == 'registration':
subject = getattr(settings,'REGISTRATION_SUBJECT')
message = getattr(settings,'REGISTRATION_MESSAGE')
elif purpose == 'reset_password':
subject = getattr(settings,'RESET_PASSWORD_SUBJECT')
message = getattr(settings,'RESET_PASSWORD_MESSAGE')
if subject and message:
message = message.format(
username=self.user.username,
activation_url=activation_url,
sender=getattr(settings,'EMAIL_HOST_USER'),
time=datetime.now())
from_email = getattr(settings,'DEFAULT_FROM_EMAIL')
recipient_list = [ self.user.email ]
send_mail(subject,message,from_email,recipient_list)
#保存發送激活碼時間
self.activation_time = datetime.now()
self.activation_valid = True
self.save()
return True
def confirm_activation_key(self):
"""
確認激活碼是否有效且驗證時間未過期
這裏特別需要說明的是activation_valid值
它主要是用來保證用戶激活及密碼重置以後
原來的激活或者密碼重置界面將無法使用
默認情況下是false
"""
expired_days = getattr(settings,'EXPIRED_DAYS') or 1
is_not_expired = self.activation_time + timedelta(expired_days) > datetime.now()
is_valid = self.activation_valid
return is_valid and is_not_expired