写在前面:
由于作者仅仅是自学,且是独自做这个项目,代码中出现不少漏洞、错误、累赘或常识问题是难免的,作者也在不断努力学习中,请各位看官多提意见,轻喷~
基本信息
1. 项目环境
Python 3.10.7
Django 4.2
编辑器:VSCode
操作系统:Windows 10
2. 项目背景
正好近一段时间在自学 Django,突发奇想不妨试着做个个人网站练练手吧~
3. 项目构思
暂定为:登录系统 + 个人主页 + 个人博客 + 个人作品
项目代码
今天的任务是做完登录系统的路由及一半的视图~
1. 配置 URL
共有如下登录系统路由 :
- login/:登录 URL
- register/:注册 URL
- logout/:登出 URL
- reset/:重置密码 URL(负责邮箱验证)
- resetpassword/:重置密码 URL(负责修改密码)
- confirm/:邮箱验证 URL
- index/:用户主页 URL
- upload_image/:上传用户头像 URL
首先在 LZLBlog/login/ 目录下创建 urls.py
创建后目录如下
在 urls.py 中输入如下内容
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.login),
path('register/', views.register),
path('logout/', views.logout),
path('reset/', views.reset),
path('confirm/', views.user_confirm),
path('resetpassword/', views.resetpassword),
path('index/<slug:name>/', views.index),
path('upload_image/', views.upload_image),
]
代码注释:
1. 每个 path() 函数都代表一条路由,引号内的字符串则是具体的 URL
2. view.xxx 表示与 urls.py 在同一目录下的 views.py 内的视图函数,稍后会处理
3. <slug:name> 表示在 URL 中会提供一个名为 name 的参数到 views.py 内的视图函数
其次进入根 URLconf,即 /LZLBlog/LZLBlog/urls.py,配置一下路由转发
首先导入路由转发函数 include
from django.urls import include
其次在 urlpatterns 列表中添加
path('login/', include('login.urls')),
此时根 URLconf 配置完毕,代码如下:
"""LZLBlog URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/dev/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import include
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', include('login.urls')),
]
2. 视图中所有用到的模块
在这里提前将所有用到的模块写一下,后面就不再写了
用到的模块如下:
from django.conf import settings
from django.shortcuts import render
from django.shortcuts import redirect
from django.utils.html import conditional_escape
from django.views.decorators.csrf import csrf_exempt, requires_csrf_token
from django.http import JsonResponse
from django.utils import timezone
from . import models
import hashlib, datetime, random, pytz
注意:这里的 pytz 模块要提前下载~
3. 登录视图
打开 LZLBlog/login/views.py, 以下的代码均在此文件中编写
首先,用户密码等需要加密,因此需要写一个加密函数,代码如下
import hashlib
def hashcode(s, salt="LZLBlog"):
h = hashlib.sha256()
s += salt
h.update(s.encode())
return h.hexdigest()
代码注释:
1. hashlib 为 python 标准库,无须另外安装
2. 此处使用 sha256 算法加密
3. salt 为加密时的盐,用于提高安全性
其次来编写登录视图
需要获得的信息:
- 前端输入的用户名
- 前端输入的密码
- 前端是否勾选“记住我”标记
大体思路如下:
- 判断用户是否已经登录,因为不能重复登录,如果已登录,则直接跳转
- 为防止恶意发送大量请求导致崩溃,应每几次访问后就进行验证,所以需要记录访问次数
- 实现 “记住我” 功能,即下次自动登录
- 接收前端传回的用户名与密码,进行比对,判断是否登录成功
登录视图函数代码如下:
def login(request):
if request.session.get("is_login", ""):
return redirect("/index/index/")
visitnum = int(conditional_escape(request.session.get('visit_num', 0)))
if visitnum <= 3:
request.session['visit_num'] = visitnum+1
else:
turn_visit_num = conditional_escape(request.POST.get('turn_visit_num',0))
if turn_visit_num:
request.session['visit_num'] = 1
try:
remember_signature = request.get_signed_cookie(key=hashcode('LZLBlog'), salt=hashcode('LZLBlog'), max_age=7*24*3600)
if remember_signature == hashcode(hashcode('LZLBlog')):
href = conditional_escape(request.COOKIES.get('redirect_href',None))
if href:
return redirect(href)
else:
username = conditional_escape(request.get_signed_cookie(key=hashcode('username'), salt=hashcode(hashcode('username')), max_age=7*24*3600))
return redirect('/index/index/')
except:
pass
if request.method == "POST":
username = conditional_escape(request.POST.get("username",''))
password = conditional_escape(request.POST.get("password",''))
message = '请检查填写的内容格式是否正确!'
if username.strip() and password:
try:
user = models.User.objects.get(name=username)
except:
try:
user = models.User.objects.get(email=username)
except:
message = '用户名或密码不正确!'
return render(request, 'login/login.html', {'message':message})
if user.password == hashcode(password):
if not(user.has_confirmed):
message = '用户未经过邮件确认!'
return render(request, 'login/login.html', {'message':message})
request.session['is_login'] = 1
request.session['user_id'] = user.id
request.session['user_name'] = user.name
response = redirect('/index/index/')
remember = request.POST.get('remember', '')
href = conditional_escape(request.COOKIES.get('redirect_href',''))
if remember:
response.set_signed_cookie(key=hashcode('LZLBlog'), value=hashcode(hashcode('LZLBlog')), salt=hashcode('LZLBlog'), max_age=7*24*3600)
response.set_signed_cookie(key=hashcode('username'), value=hashcode(user.name), salt=hashcode(hashcode('username')), max_age=7*24*3600)
if href:
return redirect(href)
else:
return response
else:
message = '用户名或密码不正确!'
return render(request, 'login/login.html', {'message':message})
else:
message = ''
return render(request, 'login/login.html', {'message':message})
代码注释:
if request.session.get("is_login", ""):
return redirect("/index/index/")
- 这里使用 Django 的 session 会话 记录登录状态,在登录成功后设置 is_login 为 True,相当于打个标记
- 此处为若已登录则直接跳转至 /index/index/ 界面,这个界面后面编写个人主页时处理
visitnum = int(conditional_escape(request.session.get('visit_num', 0)))
if visitnum <= 3:
request.session['visit_num'] = visitnum+1
else:
turn_visit_num = conditional_escape(request.POST.get('turn_visit_num',0))
if turn_visit_num:
request.session['visit_num'] = 1
- visitnum 表示登录页面已被连续访问的次数,这里仍然使用 session 会话 记录
- 当访问次数小于等于3,则正常计数
- 当访问次数大于3,则从前端获取 POST 的 turn_visit_num 标记,这个标记代表是否通过前端验证
- 如果通过验证,则重新开始计数
try:
remember_signature = request.get_signed_cookie(key=hashcode('LZLBlog'), salt=hashcode('LZLBlog'), max_age=7*24*3600)
if remember_signature == hashcode(hashcode('LZLBlog')):
href = conditional_escape(request.COOKIES.get('redirect_href',None))
if href:
return redirect(href)
else:
username = conditional_escape(request.get_signed_cookie(key=hashcode('username'), salt=hashcode(hashcode('username')), max_age=7*24*3600))
return redirect('/index/index/')
except:
pass
- remember_signature 是上次“记住我”的标记,通过加密的 Cookie 获得,保存时间为7天
- remember_signature 的 key 和 salt 和 value 都是
瞎起的 - 如果有标记(上次在前端被勾选了),那么直接跳转
- 这里的 redirect_href 的 Cookie 是规定登录成功后跳转到哪里,默认到 /index/index/
if request.method == "POST":
username = conditional_escape(request.POST.get("username",''))
password = conditional_escape(request.POST.get("password",''))
message = '请检查填写的内容格式是否正确!'
if username.strip() and password:
try:
user = models.User.objects.get(name=username)
except:
try:
user = models.User.objects.get(email=username)
except:
message = '用户名或密码不正确!'
return render(request, 'login/login.html', {'message':message})
if user.password == hashcode(password):
if not(user.has_confirmed):
message = '用户未经过邮件确认!'
return render(request, 'login/login.html', {'message':message})
request.session['is_login'] = 1
request.session['user_id'] = user.id
request.session['user_name'] = user.name
response = redirect('/index/index/')
remember = request.POST.get('remember', '')
href = conditional_escape(request.COOKIES.get('redirect_href',''))
if remember:
response.set_signed_cookie(key=hashcode('LZLBlog'), value=hashcode(hashcode('LZLBlog')), salt=hashcode('LZLBlog'), max_age=7*24*3600)
response.set_signed_cookie(key=hashcode('username'), value=hashcode(user.name), salt=hashcode(hashcode('username')), max_age=7*24*3600)
if href:
return redirect(href)
else:
return response
else:
message = '用户名或密码不正确!'
return render(request, 'login/login.html', {'message':message})
else:
message = ''
大体思路:
- 检查填写的用户名与密码格式是否正确
- 检查用户名是否存在
- 检查密码是否正确
- 检查用户是否经过邮件确认
- 如果都通过了,设置登录相关状态,获取“记住我”信息,决定是否设置“记住我”状态
- 重新渲染页面并提示错误(验证未通过,登陆失败)或跳转(验证通过,登录成功)
4. 注册视图
编写注册视图,这次要实现的功能只有一个 —— 接收前端数据,比对是否注册成功
需要获得的信息:
- 用户名
- 密码
- 再次确认的密码
- 邮箱
比对流程思路:
- 检查格式是否正确
- 检查两次输入的密码是否相同
- 检查用户名是否已经存在
- 检查邮箱是否已经存在
- 检查密码是否合法(强度是否足够高)
- 检查邮箱是否合法
- 如果有检查未通过的地方,重新渲染页面并提示错误,若没有,创建新用户并跳转至邮箱验证页面
注册视图函数代码如下:
def register(request):
if request.method == 'POST':
username = conditional_escape(request.POST.get('username',''))
password = conditional_escape(request.POST.get('password',''))
password_repeat = conditional_escape(request.POST.get('password_again',''))
email = conditional_escape(request.POST.get('email',''))
message = "请检查填写的内容格式是否正确!"
if username.strip() and password and password_repeat and email:
if password != password_repeat:
message = "两次输入的密码不同!"
return render(request, 'login/register.html', {'message':message})
else:
user_list = models.User.objects.filter(name=username)
if user_list:
message = "该用户名已经存在!"
return render(request, 'login/register.html', {'message':message})
email_list = models.User.objects.filter(email=email)
if email_list:
message = "该邮箱已经被注册了!"
return render(request, 'login/register.html', {'message':message})
if password_verification(password):
message = password_verification(password)
return render(request, 'login/register.html', {'message':message})
if email_verification(email):
message = email_verification(email)
return render(request, 'login/register.html', {'message':message})
new_user = models.User()
new_user.name = username
new_user.password = hashcode(password_repeat)
new_user.email = email
new_user.save()
code,tag = make_confirm_string(new_user)
send_email(email, code)
request.session['tag'] = tag
return redirect('/login/confirm/')
else:
return render(request, 'login/register.html', {'message':message})
return render(request, 'login/register.html')
这里的代码注释我就不写了,应该比较明显,看前面的思路就可以
在注册视图中出现的一些小函数:
1. 密码验证函数
作用:验证密码是否合法且强度是否足够
def password_verification(password):
message = ''
if len(password) < 8:
message = '密码位数必须至少有 8 位!'
return message
for chars in password:
if '\u4e00' <= chars and chars <= '\u9fff':
message = '密码中不可以包含中文字符!'
return message
en = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
capital_en = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
number = ['1','2','3','4','5','6','7','8','9']
flag = 0
for letter in en:
if letter in password:
flag = flag + 1
break
for letter in capital_en:
if letter in password:
flag = flag + 1
break
for num in number:
if num in password:
flag = flag+1
break
if flag < 2:
message = '密码必须至少包含小写字母、大写字母和数字中的两种!'
return message
return message
代码注释:
1. '\u4e00' ~ '\u9fff' 是中文字符的范围
2. 要求密码至少包含小写字母、大写字母和数字中的两种
2. 邮箱验证函数
作用:验证邮箱是否合法
def email_verification(email):
message = ''
emaillist = list(email)
if emaillist.count('@') != 1:
message = '请检查邮箱格式是否正确!'
return message
if emaillist.index('@') == 0 or emaillist.index('@') == len(emaillist)-1:
message = '请检查邮箱格式是否正确!'
return message
return message
代码注释:
有且仅有一个 “@”,并且这个 “@” 不在字符串的第一位或最后一位就能过
3. 创建邮箱验证码函数
作用:在注册验证通过后创建邮箱验证码,为接下来的邮箱验证做准备
def make_confirm_string(user):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
code = hashcode(user.name, now)
tag = hashcode(user.name, "tag")
get_str = ''
for x in range(6):
num = random.randint(0,len(code)-1)
get_str = get_str + code[num]
models.ConfirmString.objects.create(code=get_str, user=user, tag=tag)
return (get_str,tag)
代码注释:
1. 使用用户名加密,用户创建的时间(精确到秒)作为盐,确保验证码的值是独一无二的
2. 使用用户名加密,"tag" 为盐,得到鉴别验证码的 “标签”
3. 循环六次,每次任意取验证码的一位拼成一个字符串,组成最终发送的六位邮箱验证码
4. 发送邮件函数
作用:发送带有验证码的邮件到用户刚刚注册的邮箱地址
def send_email(email, code):
from django.core.mail import EmailMultiAlternatives
subject = "来自 www.LZLBlog.com 的注册确认邮件"
text_content = '''感谢注册 www.LZLBlog.com, 欢迎阅读作者的博客并发表评论与改进意见,
如果您看到这条信息,说明您的邮箱服务器不支持 HTML 链接功能,请检查或升级系统以解决问题!'''
html_content = '''
<p>感谢注册<a href="http://{}/index/index/" target=_blank> www.LZLBlog.com </a>,
欢迎阅读作者的博客并发表评论与改进意见!</p>
<p>您的验证码是:</p><h1> {} </h1>
<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()
假装自己有域名
代码注释:
1. 这里需要有个邮箱,并且开通 smtp 服务
2. 需要在 settings.py 中进行配置,这里暂时不写,后面和 media 的配置一块说
5. 邮箱验证视图
编写邮箱验证视图
需要获得的信息:
- 获得邮箱验证码的标签
- 前端输入的邮箱验证码
大体思路如下:
- 检查标签所对应的邮箱验证码是否存在
- 检查邮件是否过期
- 检查邮箱验证码是否正确
- 如果正确,跳转至登录页面并删除邮箱验证码,若不正确,重新渲染页面并提示错误
邮箱验证视图代码如下:
def user_confirm(request):
message = ''
if request.method == "POST":
tag = conditional_escape(request.session.get('tag', ''))
email_password = conditional_escape(request.POST.get('email_password',''))
try:
confirm = models.ConfirmString.objects.get(tag=tag)
except:
return render(request, 'login/confirm.html', {'message':message})
c_time = confirm.c_time
now = datetime.datetime.now()
now = now.replace(tzinfo=pytz.timezone('UTC'))
if now > c_time + datetime.timedelta(settings.CONFIRM_DAYS):
confirm.user.delete()
del request.session['tag']
message = '您的邮件已经过期!请重新注册!'
return render(request, 'login/confirm.html', {'message':message})
if email_password == confirm.code:
confirm.user.has_confirmed = True
confirm.user.save()
confirm.delete()
del request.session['tag']
return redirect('/login/login/')
else:
message = '邮箱验证码不正确!'
return render(request, 'login/confirm.html', {'message':message})
return render(request, 'login/confirm.html', {'message':message})
这里的代码注释我就不写了,应该比较明显,看前面的思路就可以
6. 全部代码
from django.conf import settings
from django.shortcuts import render
from django.shortcuts import redirect
from django.utils.html import conditional_escape
from django.views.decorators.csrf import csrf_exempt, requires_csrf_token
from django.http import JsonResponse
from django.utils import timezone
from . import models
import hashlib, datetime, random, pytz
# Create your views here.
def hashcode(s, salt="LZLBlog"):
h = hashlib.sha256()
s += salt
h.update(s.encode())
return h.hexdigest()
def login(request):
if request.session.get("is_login", ""):
return redirect("/index/index/")
visitnum = int(conditional_escape(request.session.get('visit_num', 0)))
if visitnum <= 3:
request.session['visit_num'] = visitnum+1
else:
turn_visit_num = conditional_escape(request.POST.get('turn_visit_num',0))
if turn_visit_num:
request.session['visit_num'] = 1
try:
remember_signature = request.get_signed_cookie(key=hashcode('LZLBlog'), salt=hashcode('LZLBlog'), max_age=7*24*3600)
if remember_signature == hashcode(hashcode('LZLBlog')):
href = conditional_escape(request.COOKIES.get('redirect_href',None))
if href:
return redirect(href)
else:
username = conditional_escape(request.get_signed_cookie(key=hashcode('username'), salt=hashcode(hashcode('username')), max_age=7*24*3600))
return redirect('/index/index/')
except:
pass
if request.method == "POST":
username = conditional_escape(request.POST.get("username",''))
password = conditional_escape(request.POST.get("password",''))
message = '请检查填写的内容格式是否正确!'
if username.strip() and password:
try:
user = models.User.objects.get(name=username)
except:
try:
user = models.User.objects.get(email=username)
except:
message = '用户名或密码不正确!'
return render(request, 'login/login.html', {'message':message})
if user.password == hashcode(password):
if not(user.has_confirmed):
message = '用户未经过邮件确认!'
return render(request, 'login/login.html', {'message':message})
request.session['is_login'] = 1
request.session['user_id'] = user.id
request.session['user_name'] = user.name
response = redirect('/index/index/')
remember = request.POST.get('remember', '')
href = conditional_escape(request.COOKIES.get('redirect_href',''))
if remember:
response.set_signed_cookie(key=hashcode('LZLBlog'), value=hashcode(hashcode('LZLBlog')), salt=hashcode('LZLBlog'), max_age=7*24*3600)
response.set_signed_cookie(key=hashcode('username'), value=hashcode(user.name), salt=hashcode(hashcode('username')), max_age=7*24*3600)
if href:
return redirect(href)
else:
return response
else:
message = '用户名或密码不正确!'
return render(request, 'login/login.html', {'message':message})
else:
message = ''
return render(request, 'login/login.html', {'message':message})
def password_verification(password):
message = ''
if len(password) < 8:
message = '密码位数必须至少有 8 位!'
return message
for chars in password:
if '\u4e00' <= chars and chars <= '\u9fff':
message = '密码中不可以包含中文字符!'
return message
en = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
capital_en = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
number = ['1','2','3','4','5','6','7','8','9']
flag = 0
for letter in en:
if letter in password:
flag = flag + 1
break
for letter in capital_en:
if letter in password:
flag = flag + 1
break
for num in number:
if num in password:
flag = flag+1
break
if flag < 2:
message = '密码必须至少包含小写字母、大写字母和数字中的两种!'
return message
return message
def email_verification(email):
message = ''
emaillist = list(email)
if emaillist.count('@') != 1:
message = '请检查邮箱格式是否正确!'
return message
if emaillist.index('@') == 0 or emaillist.index('@') == len(emaillist)-1:
message = '请检查邮箱格式是否正确!'
return message
return message
def make_confirm_string(user):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
code = hashcode(user.name, now)
tag = hashcode(user.name, "tag")
get_str = ''
for x in range(6):
num = random.randint(0,len(code)-1)
get_str = get_str + code[num]
models.ConfirmString.objects.create(code=get_str, user=user, tag=tag)
return (get_str,tag)
def send_email(email, code):
from django.core.mail import EmailMultiAlternatives
subject = "来自 www.LZLBlog.com 的注册确认邮件"
text_content = '''感谢注册 www.LZLBlog.com, 欢迎阅读作者的博客并发表评论与改进意见,
如果您看到这条信息,说明您的邮箱服务器不支持 HTML 链接功能,请检查或升级系统以解决问题!'''
html_content = '''
<p>感谢注册<a href="http://{}/index/index/" target=_blank> www.LZLBlog.com </a>,
欢迎阅读作者的博客并发表评论与改进意见!</p>
<p>您的验证码是:</p><h1> {} </h1>
<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()
def register(request):
if request.method == 'POST':
username = conditional_escape(request.POST.get('username',''))
password = conditional_escape(request.POST.get('password',''))
password_repeat = conditional_escape(request.POST.get('password_again',''))
email = conditional_escape(request.POST.get('email',''))
message = "请检查填写的内容格式是否正确!"
if username.strip() and password and password_repeat and email:
if password != password_repeat:
message = "两次输入的密码不同!"
return render(request, 'login/register.html', {'message':message})
else:
user_list = models.User.objects.filter(name=username)
if user_list:
message = "该用户名已经存在!"
return render(request, 'login/register.html', {'message':message})
email_list = models.User.objects.filter(email=email)
if email_list:
message = "该邮箱已经被注册了!"
return render(request, 'login/register.html', {'message':message})
if password_verification(password):
message = password_verification(password)
return render(request, 'login/register.html', {'message':message})
if email_verification(email):
message = email_verification(email)
return render(request, 'login/register.html', {'message':message})
new_user = models.User()
new_user.name = username
new_user.password = hashcode(password_repeat)
new_user.email = email
new_user.save()
code,tag = make_confirm_string(new_user)
send_email(email, code)
request.session['tag'] = tag
return redirect('/login/confirm/')
else:
return render(request, 'login/register.html', {'message':message})
return render(request, 'login/register.html')
def user_confirm(request):
message = ''
if request.method == "POST":
tag = conditional_escape(request.session.get('tag', ''))
email_password = conditional_escape(request.POST.get('email_password',''))
try:
confirm = models.ConfirmString.objects.get(tag=tag)
except:
return render(request, 'login/confirm.html', {'message':message})
c_time = confirm.c_time
now = datetime.datetime.now()
now = now.replace(tzinfo=pytz.timezone('UTC'))
if now > c_time + datetime.timedelta(settings.CONFIRM_DAYS):
confirm.user.delete()
del request.session['tag']
message = '您的邮件已经过期!请重新注册!'
return render(request, 'login/confirm.html', {'message':message})
if email_password == confirm.code:
confirm.user.has_confirmed = True
confirm.user.save()
confirm.delete()
del request.session['tag']
return redirect('/login/login/')
else:
message = '邮箱验证码不正确!'
return render(request, 'login/confirm.html', {'message':message})
return render(request, 'login/confirm.html', {'message':message})
至此,今天的内容就完结啦~
下篇继续做登录视图~