作者: IT策士
系列: 《Django 從 0 到 1 打造完整電商平臺(tái)》第 6 篇
標(biāo)簽: Django, 用戶認(rèn)證, 短信驗(yàn)證碼, 郵箱激活, 電商開發(fā)
前言
上一篇我們把靜態(tài)文件、模板骨架全部搞定,項(xiàng)目已經(jīng)可以呈現(xiàn)出漂亮的頁(yè)面。從今天開始,我們正式踏入業(yè)務(wù)邏輯開發(fā)的第一站——用戶注冊(cè)。
用戶注冊(cè)看似簡(jiǎn)單,但在電商項(xiàng)目里,它涉及表單驗(yàn)證、手機(jī)號(hào)唯一性校驗(yàn)、驗(yàn)證碼發(fā)送、郵箱激活、密碼加密存儲(chǔ)等一大堆細(xì)節(jié)。今天我會(huì)帶著大家一步一步寫出完整的注冊(cè)流程,并且讓手機(jī)驗(yàn)證碼和郵箱激活都跑通。
一、需求分析
我們的注冊(cè)功能需要支持兩種方式:
| 注冊(cè)方式 | 流程 |
|---|---|
| 手機(jī)號(hào)注冊(cè) | 輸入手機(jī)號(hào) → 獲取短信驗(yàn)證碼 → 填寫驗(yàn)證碼 + 密碼 → 完成注冊(cè) |
| 郵箱注冊(cè) | 輸入郵箱 + 密碼 → 注冊(cè)成功 → 發(fā)送激活郵件 → 點(diǎn)擊鏈接激活賬號(hào) |
開發(fā)環(huán)境說明: 由于沒有真實(shí)短信通道,我們采用控制臺(tái)模擬發(fā)送短信驗(yàn)證碼(生產(chǎn)環(huán)境換成阿里云/騰訊云 SDK 即可)。郵件方面,Django 提供了
console.EmailBackend,激活郵件會(huì)直接打印在終端里,非常方便調(diào)試。
二、配置郵件后端(開發(fā)環(huán)境)
在 django_ecommerce/settings.py 中添加郵件配置:
# 開發(fā)環(huán)境:將郵件打印到控制臺(tái)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# 生產(chǎn)環(huán)境才需要下面的真實(shí) SMTP 配置,現(xiàn)在注釋掉
# EMAIL_HOST = 'smtp.example.com'
# EMAIL_PORT = 587
# EMAIL_HOST_USER = 'your_email@example.com'
# EMAIL_HOST_PASSWORD = 'your_password'
# EMAIL_USE_TLS = True
# DEFAULT_FROM_EMAIL = '電商平臺(tái) <noreply@example.com>'這樣所有發(fā)出的郵件都會(huì)顯示在 runserver 的終端輸出中,注冊(cè)后去終端復(fù)制激活鏈接即可。
三、編寫注冊(cè)表單
Django 的 Form 組件能幫我們處理前端數(shù)據(jù)校驗(yàn)。我們?cè)?apps/users/forms.py 中創(chuàng)建自定義注冊(cè)表單,支持手機(jī)號(hào)或郵箱兩種方式:
from django import forms
from django.core.validators import RegexValidator
from .models import User
class RegisterForm(forms.Form):
# 手機(jī)號(hào)(可選,如果用郵箱注冊(cè)則留空)
phone = forms.CharField(
max_length=11,
min_length=11,
required=False,
validators=[
RegexValidator(r'^1[3-9]\d{9}$', message='請(qǐng)輸入有效的手機(jī)號(hào)')
],
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '手機(jī)號(hào)(選填)'
})
)
# 郵箱
email = forms.EmailField(
required=False,
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': '郵箱(選填)'
})
)
# 密碼
password = forms.CharField(
min_length=6,
max_length=20,
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': '密碼(至少6位)'
})
)
password2 = forms.CharField(
label='確認(rèn)密碼',
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': '再次輸入密碼'
})
)
# 手機(jī)驗(yàn)證碼(如果用手機(jī)注冊(cè)時(shí)必填)
sms_code = forms.CharField(
max_length=6,
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '短信驗(yàn)證碼'
})
)
def clean(self):
cleaned_data = super().clean()
phone = cleaned_data.get('phone')
email = cleaned_data.get('email')
# 必須提供手機(jī)號(hào)或郵箱之一
if not phone and not email:
raise forms.ValidationError('請(qǐng)至少填寫手機(jī)號(hào)或郵箱')
return cleaned_data
def clean_phone(self):
phone = self.cleaned_data.get('phone')
if phone and User.objects.filter(phone=phone).exists():
raise forms.ValidationError('該手機(jī)號(hào)已被注冊(cè)')
return phone
def clean_email(self):
email = self.cleaned_data.get('email')
if email and User.objects.filter(email=email).exists():
raise forms.ValidationError('該郵箱已被注冊(cè)')
return email
def clean_password2(self):
pwd = self.cleaned_data.get('password')
pwd2 = self.cleaned_data.get('password2')
if pwd and pwd2 and pwd != pwd2:
raise forms.ValidationError('兩次密碼不一致')
return pwd2設(shè)計(jì)要點(diǎn)
手機(jī)號(hào)和郵箱均設(shè)為
required=False,但通過clean()方法確保至少填一個(gè)自定義手機(jī)號(hào)正則校驗(yàn):
^1[3-9]\d{9}$匹配中國(guó)大陸手機(jī)號(hào)格式唯一性校驗(yàn):在
clean_phone和clean_email中檢查是否已被注冊(cè)密碼一致性校驗(yàn):在
clean_password2中比對(duì)兩次輸入
四、編寫注冊(cè)視圖
在 apps/users/views.py 中實(shí)現(xiàn)注冊(cè)邏輯:
import random
from django.shortcuts import render, redirect
from django.contrib import messages
from django.core.mail import send_mail
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .forms import RegisterForm
from .models import User
def register(request):
if request.method == 'POST':
form = RegisterForm(request.POST)
if form.is_valid():
phone = form.cleaned_data.get('phone')
email = form.cleaned_data.get('email')
password = form.cleaned_data.get('password')
# 驗(yàn)證手機(jī)驗(yàn)證碼(如果使用手機(jī)注冊(cè))
if phone:
sms_code_input = form.cleaned_data.get('sms_code')
sms_code_session = request.session.get('sms_code')
if not sms_code_session or sms_code_input != sms_code_session:
form.add_error('sms_code', '驗(yàn)證碼錯(cuò)誤或已過期')
return render(request, 'users/register.html', {'form': form})
# 清空 session 中的驗(yàn)證碼
request.session.pop('sms_code', None)
# 創(chuàng)建用戶
user = User.objects.create_user(
username=phone or email.split('@')[0], # 用手機(jī)號(hào)或郵箱前綴當(dāng)用戶名
password=password,
phone=phone if phone else None,
email=email if email else None,
)
# 如果用郵箱注冊(cè),發(fā)送激活郵件
if email:
# 生成簡(jiǎn)單的 token(生產(chǎn)環(huán)境建議用 itsdangerous)
token = str(random.randint(100000, 999999))
request.session[f'email_token_{user.id}'] = token
activate_url = request.build_absolute_uri(
f'/users/activate/{user.id}/{token}/'
)
send_mail(
subject='激活你的電商賬號(hào)',
message=f'點(diǎn)擊鏈接激活賬號(hào):{activate_url}',
from_email='noreply@example.com',
recipient_list=[email],
)
messages.success(request, '注冊(cè)成功!激活郵件已發(fā)送,請(qǐng)前往郵箱查收(查看終端輸出)。')
else:
messages.success(request, '注冊(cè)成功!您現(xiàn)在可以登錄了。')
return redirect('home') # 后續(xù)改為登錄頁(yè)
else:
form = RegisterForm()
return render(request, 'users/register.html', {'form': form})
@require_POST
def send_sms_code(request):
"""發(fā)送短信驗(yàn)證碼(模擬)"""
phone = request.POST.get('phone')
if not phone:
return JsonResponse({'ok': False, 'msg': '手機(jī)號(hào)不能為空'}, status=400)
# 生成 6 位隨機(jī)驗(yàn)證碼
code = str(random.randint(100000, 999999))
# 存入 session
request.session['sms_code'] = code
request.session.set_expiry(300) # 5分鐘有效
# 控制臺(tái)模擬發(fā)送
print(f"\n{'='*40}")
print(f"【模擬短信】驗(yàn)證碼:{code},發(fā)送至手機(jī)號(hào):{phone}")
print(f"{'='*40}\n")
return JsonResponse({'ok': True, 'msg': '驗(yàn)證碼已發(fā)送'})關(guān)鍵點(diǎn)說明
| 功能 | 實(shí)現(xiàn)方式 |
|---|---|
| 驗(yàn)證碼校驗(yàn) | 手機(jī)注冊(cè)時(shí),比對(duì)用戶輸入與 request.session['sms_code'],通過后立即清空 |
| 用戶創(chuàng)建 | 使用 User.objects.create_user(),自動(dòng)處理密碼加密;用戶名取手機(jī)號(hào)或郵箱前綴 |
| 郵箱激活 | 生成 6 位隨機(jī) token 存入 session,構(gòu)建激活鏈接并通過 send_mail() 發(fā)送 |
| 短信模擬 |
send_sms_code 是獨(dú)立 AJAX 視圖,驗(yàn)證碼存 session 并設(shè)置 5 分鐘過期,同時(shí)在控制臺(tái)打印 |
五、URL 路由配置
5.1 應(yīng)用級(jí)路由:apps/users/urls.py
from django.urls import path
from . import views
app_name = 'users'
urlpatterns = [
path('register/', views.register, name='register'),
path('send_sms/', views.send_sms_code, name='send_sms'),
]5.2 項(xiàng)目級(jí)路由:django_ecommerce/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.views.generic import TemplateView
urlpatterns = [
path('admin/', admin.site.urls),
path('', TemplateView.as_view(template_name='home.html'), name='home'),
path('users/', include('apps.users.urls')), # 注意路徑前綴
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)六、注冊(cè)頁(yè)面模板
創(chuàng)建 apps/users/templates/users/register.html:
{% extends 'base.html' %}
{% block title %}用戶注冊(cè){% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-sm">
<div class="card-body p-4">
<h3 class="text-center mb-4">?? 創(chuàng)建賬號(hào)</h3>
<form method="post" novalidate>
{% csrf_token %}
<!-- 手機(jī)號(hào) -->
<div class="mb-3">
<label class="form-label">手機(jī)號(hào)</label>
{{ form.phone }}
{% if form.phone.errors %}
<div class="text-danger small">{{ form.phone.errors.0 }}</div>
{% endif %}
</div>
<!-- 驗(yàn)證碼(僅手機(jī)注冊(cè)時(shí)顯示) -->
<div class="mb-3" id="sms-code-group" style="display:none;">
<label class="form-label">驗(yàn)證碼</label>
<div class="input-group">
{{ form.sms_code }}
<button class="btn btn-outline-secondary" type="button" id="get-sms-btn">
獲取驗(yàn)證碼
</button>
</div>
{% if form.sms_code.errors %}
<div class="text-danger small">{{ form.sms_code.errors.0 }}</div>
{% endif %}
</div>
<!-- 郵箱 -->
<div class="mb-3">
<label class="form-label">郵箱</label>
{{ form.email }}
{% if form.email.errors %}
<div class="text-danger small">{{ form.email.errors.0 }}</div>
{% endif %}
</div>
<!-- 密碼 -->
<div class="mb-3">
<label class="form-label">密碼</label>
{{ form.password }}
{% if form.password.errors %}
<div class="text-danger small">{{ form.password.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">確認(rèn)密碼</label>
{{ form.password2 }}
{% if form.password2.errors %}
<div class="text-danger small">{{ form.password2.errors.0 }}</div>
{% endif %}
</div>
<!-- 全局錯(cuò)誤(如未填手機(jī)號(hào)或郵箱) -->
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors.0 }}
</div>
{% endif %}
<button type="submit" class="btn btn-primary w-100">注冊(cè)</button>
</form>
<p class="text-center mt-3">
已有賬號(hào)?<<a href="#">立即登錄</a>
</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const phoneInput = document.querySelector('#id_phone');
const emailInput = document.querySelector('#id_email');
const smsGroup = document.querySelector('#sms-code-group');
const getSmsBtn = document.querySelector('#get-sms-btn');
// 根據(jù)手機(jī)號(hào)輸入框是否有內(nèi)容來顯示/隱藏驗(yàn)證碼區(qū)域
function toggleSmsGroup() {
if (phoneInput.value.trim().length > 0) {
smsGroup.style.display = 'block';
} else {
smsGroup.style.display = 'none';
}
}
phoneInput.addEventListener('input', toggleSmsGroup);
// 頁(yè)面初始化
toggleSmsGroup();
// 獲取驗(yàn)證碼
getSmsBtn.addEventListener('click', function() {
const phone = phoneInput.value.trim();
if (!phone) {
alert('請(qǐng)先輸入手機(jī)號(hào)');
return;
}
// 簡(jiǎn)單的前端倒計(jì)時(shí)
let countdown = 60;
getSmsBtn.disabled = true;
getSmsBtn.textContent = countdown + '秒后重試';
const timer = setInterval(() => {
countdown--;
getSmsBtn.textContent = countdown + '秒后重試';
if (countdown <= 0) {
clearInterval(timer);
getSmsBtn.disabled = false;
getSmsBtn.textContent = '獲取驗(yàn)證碼';
}
}, 1000);
// 發(fā)送請(qǐng)求
fetch('{% url "users:send_sms" %}', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'phone=' + encodeURIComponent(phone)
})
.then(response => response.json())
.then(data => {
if (!data.ok) {
alert(data.msg);
}
});
});
</script>
{% endblock %}前端交互亮點(diǎn)
動(dòng)態(tài)顯示驗(yàn)證碼區(qū)域:監(jiān)聽手機(jī)號(hào)輸入框,有內(nèi)容時(shí)自動(dòng)顯示驗(yàn)證碼輸入框
60 秒倒計(jì)時(shí)防重復(fù)點(diǎn)擊:點(diǎn)擊"獲取驗(yàn)證碼"后按鈕禁用并倒計(jì)時(shí)
AJAX 發(fā)送驗(yàn)證碼:使用
fetch()發(fā)送 POST 請(qǐng)求,自動(dòng)攜帶 CSRF Token
七、郵箱激活功能
為了完成完整的郵箱注冊(cè)流程,我們?cè)偬砑右粋€(gè)激活視圖。
7.1 激活視圖:apps/users/views.py
from django.http import Http404
from django.shortcuts import get_object_or_404
def activate_email(request, user_id, token):
user = get_object_or_404(User, id=user_id)
session_token = request.session.get(f'email_token_{user.id}')
if not session_token or session_token != token:
raise Http404('激活鏈接無(wú)效或已過期')
user.email_active = True
user.save(update_fields=['email_active'])
# 激活成功后清理 token
request.session.pop(f'email_token_{user.id}', None)
messages.success(request, '郵箱激活成功!現(xiàn)在可以登錄了。')
return redirect('home') # 后續(xù)改為登錄頁(yè)7.2 添加激活路由:apps/users/urls.py
path('activate/<int:user_id>/<str:token>/', views.activate_email, name='activate_email'),八、測(cè)試完整流程
啟動(dòng)開發(fā)服務(wù)器:
python manage.py runserver8.1 手機(jī)號(hào)注冊(cè)測(cè)試
訪問
http://127.0.0.1:8000/users/register/輸入手機(jī)號(hào)
13800138000,此時(shí)驗(yàn)證碼輸入框會(huì)出現(xiàn)點(diǎn)擊"獲取驗(yàn)證碼",查看終端輸出:
[20/May/2026 14:35:22] "POST /users/send_sms/ HTTP/1.1" 200 27
========================================
【模擬短信】驗(yàn)證碼:384729,發(fā)送至手機(jī)號(hào):13800138000
========================================輸入收到的驗(yàn)證碼(如
384729),設(shè)置密碼,提交注冊(cè)注冊(cè)成功,跳轉(zhuǎn)到首頁(yè)并顯示提示
驗(yàn)證數(shù)據(jù)庫(kù):
SELECT id, username, phone, is_active FROM tb_users;結(jié)果示例:
1|admin|13800138001|1 ← 超級(jí)管理員(之前創(chuàng)建)
2|13800138000|13800138000|1 ← 剛注冊(cè)的用戶8.2 郵箱注冊(cè)測(cè)試
填寫郵箱
test@example.com,密碼,確認(rèn)密碼,提交終端會(huì)輸出激活郵件內(nèi)容:
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: 激活你的電商賬號(hào)
From: noreply@example.com
To: test@example.com
Date: Wed, 20 May 2026 14:40:00 -0000
Message-ID: <...>
X-Mailer: Django Mailer
點(diǎn)擊鏈接激活賬號(hào):http://127.0.0.1:8000/users/activate/3/123456/復(fù)制終端中的鏈接(注意你的 token 和用戶 ID 可能不同),用瀏覽器打開
提示"郵箱激活成功!",用戶
email_active變?yōu)?True
九、當(dāng)前注冊(cè)的不足與改進(jìn)方向
| 問題 | 現(xiàn)狀 | 改進(jìn)方案 |
|---|---|---|
| 手機(jī)驗(yàn)證碼防刷 | 沒有對(duì)同一手機(jī)號(hào)頻繁發(fā)送做限制 | 后續(xù)使用 Redis 緩存記錄發(fā)送頻率 |
| 激活 Token 安全性 | 使用 6 位隨機(jī)數(shù),易被暴力破解 | 生產(chǎn)環(huán)境使用 itsdangerous 或 Django 自帶的 PasswordResetTokenGenerator 生成有時(shí)效的簽名 token |
| 用戶名沖突 | 郵箱前綴可能重復(fù) | 后續(xù)在 clean 中增加唯一性校驗(yàn),或改用 UUID 作為 username |
這些優(yōu)化會(huì)在后續(xù)篇(第 24~26 篇)中逐步完善。
十、總結(jié)與下集預(yù)告
今天我們實(shí)現(xiàn)了用戶注冊(cè)的核心功能,涵蓋:
? 自定義注冊(cè)表單,支持手機(jī)號(hào)或郵箱雙通道注冊(cè)
? 模擬短信驗(yàn)證碼的發(fā)送與驗(yàn)證,使用 session 存儲(chǔ)驗(yàn)證碼
? 郵箱注冊(cè)的激活流程,通過控制臺(tái)郵件后端完成激活
? 前端動(dòng)態(tài)顯示驗(yàn)證碼輸入?yún)^(qū)域,AJAX 請(qǐng)求發(fā)送驗(yàn)證碼
注冊(cè)搞定了,登錄自然是下一步。 第 7 篇,我將帶大家實(shí)現(xiàn)登錄與登出功能,包括 Django 內(nèi)置認(rèn)證系統(tǒng)的使用、登錄裝飾器、記住我功能,以及登錄后導(dǎo)航欄的狀態(tài)變化。
?? 想了解更多? 去公眾號(hào)、今日頭條搜索「IT策士」,一起升級(jí) IT 思維!
本文為《Django 從 0 到 1 打造完整電商平臺(tái)》系列第 6 篇,作者:IT策士,未經(jīng)授權(quán)禁止轉(zhuǎn)載。