通常而言,我們在用戶注冊成功,實際登陸之前,會發送一封電子郵件到對方的注冊郵箱中,表示歡迎。進一步的還可能要求用戶點擊郵件中的鏈接,進行注冊確認。
下面就讓我們先看看如何在Django中發送郵件吧。
一、在Django中發送郵件
其實在Python中已經內置了一個smtp郵件發送模塊,Django在此基礎上進行了簡單地封裝。
首先,我們需要在項目的settings文件中配置郵件發送參數,分別如下:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.sina.com' EMAIL_PORT = 25 EMAIL_HOST_USER = 'xxx@sina.com' EMAIL_HOST_PASSWORD = 'xxxxxxxxxxx'
- 第一行指定發送郵件的后端模塊,大多數情況下照抄!
- 第二行,不用說,發送方的smtp服務器地址,建議使用新浪家的;
- 第三行,smtp服務端口,默認為25;
- 第四行,你在發送服務器的用戶名;
- 第五行,對應用戶的密碼。
特別說明:
- 某些郵件公司可能不開放smtp服務
- 某些公司要求使用ssl安全機制
- 某些smtp服務對主機名格式有要求
這些都是前人踩過的坑。
配置好了參數,就可以先測試一下郵件功能了。
在項目根目錄下新建一個send_mail.py
文件,然后寫入下面的內容:
import os from django.core.mail import send_mail os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings' if __name__ == '__main__': send_mail( '來自www.cnblogs.com的測試郵件', '歡迎訪問,這里是博客園,本站專注於Python和Django技術的分享!', '1129719492@qq.com', ['1129719492@qq.com'], )
對於send_mail方法,第一個參數是郵件主題subject;第二個參數是郵件具體內容;第三個參數是郵件發送方,需要和你settings中的一致;第四個參數是接受方的郵件地址列表。請按你自己實際情況修改發送方和接收方的郵箱地址。
另外,由於我們是單獨運行send_mail.py
文件,所以無法使用Django環境,需要通過os模塊對環境變量進行設置,也就是:
os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'
運行send_mail.py
文件,注意不是運行Django服務器。然后到你的目的地郵箱查看郵件是否收到。
二、發送HTML格式的郵件
通常情況下,我們發送的郵件內容都是純文本格式。但是很多情況下,我們需要發送帶有HTML格式的內容,比如說超級鏈接。一般情況下,為了安全考慮,很多郵件服務提供商都會禁止使用HTML內容,幸運的是對於以http
和https
開頭的鏈接還是可以點擊的。
下面是發送HTML格式的郵件例子。刪除send_mail.py
文件內原來的所有內容,添加下面的代碼:
import os from django.core.mail import EmailMultiAlternatives os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings' if __name__ == '__main__': subject, from_email, to = '來自www.cnblogs.com的測試郵件', '1129719492@qq.com', '1129719492@qq.com' text_content = '歡迎訪問www.cnblogs.com,這里是劉江的博客和教程站點,專注於Python和Django技術的分享!' html_content = '<p>歡迎訪問<a href="http://www.cnblogs.com" target=blank>www.cnblogs.com</a>,這里是博客園,專注於Python和Django技術的分享!</p>' msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg.attach_alternative(html_content, "text/html") msg.send()
其中的text_content
是用於當HTML內容無效時的替代txt文本。
打開測試用的接收郵箱,可以看到鏈接能夠正常點擊,如下圖所示:
這個send_mail.py
文件只是一個測試腳本,可以從項目里刪除。
三、 創建郵件確認模型
既然要區分通過和未通過郵件確認的用戶,那么必須給用戶添加一個是否進行過郵件確認的屬性。
另外,我們要創建一張新表,用於保存用戶的確認碼以及注冊提交的時間。
全新、完整的/login/models.py
文件如下:
from django.db import models # Create your models here. class User(models.Model): gender = ( ('male', "男"), ('female', "女"), ) name = models.CharField(max_length=128, unique=True) password = models.CharField(max_length=256) email = models.EmailField(unique=True) sex = models.CharField(max_length=32, choices=gender, default="男") c_time = models.DateTimeField(auto_now_add=True) has_confirmed = models.BooleanField(default=False) def __str__(self): return self.name class Meta: ordering = ["-c_time"] verbose_name = "用戶" verbose_name_plural = "用戶" class ConfirmString(models.Model): code = models.CharField(max_length=256) user = models.OneToOneField('User',on_delete=models.CASCADE,) c_time = models.DateTimeField(auto_now_add=True) def __str__(self): return self.user.name + ": " + self.code class Meta: ordering = ["-c_time"] verbose_name = "確認碼" verbose_name_plural = "確認碼"
說明:
- User模型新增了
has_confirmed
字段,這是個布爾值,默認為False,也就是未進行郵件注冊; - ConfirmString模型保存了用戶和注冊碼之間的關系,一對一的形式;
- code字段是哈希后的注冊碼;
- user是關聯的一對一用戶;
c_time
是注冊的提交時間。
這里有個問題可以討論一下:是否需要創建ConfirmString新表,可否都放在User表里?我認為如果全都放在User中,不利於管理,查詢速度慢,創建新表有利於區分已確認和未確認的用戶。最終的選擇可以根據你的實際情況具體分析。
模型修改和創建完畢,需要執行migrate命令,一定不要忘了。
順便修改一下admin.py文件,方便我們在后台修改和觀察數據。
# login/admin.py from django.contrib import admin # Register your models here. from . import models admin.site.register(models.User) admin.site.register(models.ConfirmString)
四、修改視圖
首先,要修改我們的register()
視圖的邏輯:
def register(request): if request.session.get('is_login', None): # 登錄狀態不允許注冊。你可以修改這條原則! return redirect("/index/") if request.method == "POST": register_form = forms.RegisterForm(request.POST) message = "請檢查填寫的內容!" if register_form.is_valid(): # 獲取數據 username = register_form.cleaned_data['username'] password1 = register_form.cleaned_data['password1'] password2 = register_form.cleaned_data['password2'] email = register_form.cleaned_data['email'] sex = register_form.cleaned_data['sex'] if password1 != password2: # 判斷兩次密碼是否相同 message = "兩次輸入的密碼不同!" return render(request, 'login/register.html', locals()) else: same_name_user = models.User.objects.filter(name=username) if same_name_user: # 用戶名唯一 message = '用戶已經存在,請重新選擇用戶名!' return render(request, 'login/register.html', locals()) same_email_user = models.User.objects.filter(email=email) if same_email_user: # 郵箱地址唯一 message = '該郵箱地址已被注冊,請使用別的郵箱!' return render(request, 'login/register.html', locals()) # 當一切都OK的情況下,創建新用戶 new_user = models.User() new_user.name = username new_user.password = hash_code(password1) # 使用加密密碼 new_user.email = email new_user.sex = sex new_user.save() code = make_confirm_string(new_user) send_email(email, code) message = '請前往注冊郵箱,進行郵件確認!' return render(request, 'login/confirm.html', locals()) # 跳轉到等待郵件確認頁面。 register_form = forms.RegisterForm() return render(request, 'login/register.html', locals())
關鍵是多了下面兩行:
code = make_confirm_string(new_user) send_email(email, code)
make_confirm_string()
是創建確認碼對象的方法,代碼如下:
def make_confirm_string(user): now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") code = hash_code(user.name, now) models.ConfirmString.objects.create(code=code, user=user,) return code
在文件頂部要先導入datetime
模塊。
make_confirm_string()
方法接收一個用戶對象作為參數。首先利用datetime模塊生成一個當前時間的字符串now,再調用我們前面編寫的hash_code()
方法以用戶名為基礎,now為‘鹽’,生成一個獨一無二的哈希值,再調用ConfirmString模型的create()方法,生成並保存一個確認碼對象。最后返回這個哈希值。
send_email(email, code)
方法接收兩個參數,分別是注冊的郵箱和前面生成的哈希值,代碼如下:
def send_email(email, code): from django.core.mail import EmailMultiAlternatives subject = '來自www.cnblogs.com的注冊確認郵件' text_content = '''感謝注冊www.cnblogs.com,專注於Python和Django技術的分享!\ 如果你看到這條消息,說明你的郵箱服務器不提供HTML鏈接功能,請聯系管理員!''' html_content = ''' <p>感謝注冊<a href="http://{}/confirm/?code={}" target=blank>www.cnblogs.com</a>,\ 專注於Python和Django技術的分享!</p> <p>請點擊站點鏈接完成注冊確認!</p> <p>此鏈接有效期為{}天!</p> '''.format('127.0.0.1:8000', code, settings.CONFIRM_DAYS) msg = EmailMultiAlternatives(subject, text_content, settings.EMAIL_HOST_USER, [email]) msg.attach_alternative(html_content, "text/html") msg.send()
首先我們需要導入settings配置文件from django.conf import settings
。
郵件內容中的所有字符串都可以根據你的實際情況進行修改。其中關鍵在於<a href=''>
中鏈接地址的格式,我這里使用了硬編碼的'127.0.0.1:8000',請酌情修改,url里的參數名為code
,它保存了關鍵的注冊確認碼,最后的有效期天數為設置在settings中的CONFIRM_DAYS
。所有的這些都是可以定制的!
下面是郵件相關的settings配置:
# 郵件配置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.sina.com' EMAIL_PORT = 25 EMAIL_HOST_USER = 'xxx@sina.com' EMAIL_HOST_PASSWORD = 'xxxxxx' # 注冊有效期天數 CONFIRM_DAYS = 7
五、處理郵件確認請求
首先,在根目錄的urls.py
中添加一條url:
url(r'^confirm/$', views.user_confirm),
其次,在login/views.py
中添加一個user_confirm
視圖。
def user_confirm(request): code = request.GET.get('code', None) message = '' try: confirm = models.ConfirmString.objects.get(code=code) except: message = '無效的確認請求!' return render(request, 'login/confirm.html', locals()) c_time = confirm.c_time now = datetime.datetime.now() if now > c_time + datetime.timedelta(settings.CONFIRM_DAYS): confirm.user.delete() message = '您的郵件已經過期!請重新注冊!' return render(request, 'login/confirm.html', locals()) else: confirm.user.has_confirmed = True confirm.user.save() confirm.delete() message = '感謝確認,請使用賬戶登錄!' return render(request, 'login/confirm.html', locals())
說明:
- 通過
request.GET.get('code', None)
從請求的url地址中獲取確認碼; - 先去數據庫內查詢是否有對應的確認碼;
- 如果沒有,返回
confirm.html
頁面,並提示; - 如果有,獲取注冊的時間
c_time
,加上設置的過期天數,這里是7天,然后與現在時間點進行對比; - 如果時間已經超期,刪除注冊的用戶,同時注冊碼也會一並刪除,然后返回
confirm.html
頁面,並提示; - 如果未超期,修改用戶的
has_confirmed
字段為True,並保存,表示通過確認了。然后刪除注冊碼,但不刪除用戶本身。最后返回confirm.html
頁面,並提示。
這里需要一個confirm.html
頁面,我們將它創建在/login/templates/login/
下面:
{% extends 'base.html' %} {% block title %}注冊確認{% endblock %} {% block content %} <div class="row"> <h1 class="text-center">{{ message }}</h1> </div> <script> window.setTimeout("window.location='/login/'",2000); </script> {% endblock %}
頁面中通過JS代碼,設置2秒后自動跳轉到登錄頁面。
六、修改登錄規則
既然未進行郵件確認的用戶不能登錄,那么我們就必須修改登錄規則,如下所示:
def login(request): if request.session.get('is_login', None): return redirect("/index/") if request.method == "POST": login_form = forms.UserForm(request.POST) message = "請檢查填寫的內容!" if login_form.is_valid(): username = login_form.cleaned_data['username'] password = login_form.cleaned_data['password'] try: user = models.User.objects.get(name=username) if not user.has_confirmed: message = "該用戶還未通過郵件確認!" return render(request, 'login/login.html', locals()) if user.password == hash_code(password): # 哈希值和數據庫內的值進行比對 request.session['is_login'] = True request.session['user_id'] = user.id request.session['user_name'] = user.name return redirect('/index/') else: message = "密碼不正確!" except: message = "用戶不存在!" return render(request, 'login/login.html', locals()) login_form = forms.UserForm() return render(request, 'login/login.html', locals())
關鍵是下面的部分:
if not user.has_confirmed: message = "該用戶還未通過郵件確認!" return render(request, 'login/login.html', locals())
七、功能展示
首先,通過admin后台刪除原來所有的用戶。
進入注冊頁面,如下圖所示:
點擊提交后,跳轉到提示信息頁面,2秒后再跳轉到登錄頁面。
進入admin后台,查看剛才建立的用戶,可以看到其處於未確認狀態,嘗試登錄也不通過:
進入你的測試郵箱,查看注冊郵件:
點擊確認后再進來,ok了。