通常而言,我們在用戶注冊成功,實際登陸之前,會發送一封電子郵件到對方的注冊郵箱中,表示歡迎。進一步的還可能要求用戶點擊郵件中的鏈接,進行注冊確認。
下面就讓我們先看看如何在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了。

