从零开始做个人网站(2)

写在前面:

由于作者仅仅是自学,且是独自做这个项目,代码中出现不少漏洞、错误、累赘或常识问题是难免的,作者也在不断努力学习中,请各位看官多提意见,轻喷~


基本信息

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 内的视图函数

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

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 为加密时的盐,用于提高安全性

其次来编写登录视图

需要获得的信息:

  1. 前端输入的用户名
  2. 前端输入的密码
  3. 前端是否勾选“记住我”标记

大体思路如下:

  1. 判断用户是否已经登录,因为不能重复登录,如果已登录,则直接跳转
  2. 为防止恶意发送大量请求导致崩溃,应每几次访问后就进行验证,所以需要记录访问次数
  3. 实现 “记住我” 功能,即下次自动登录
  4. 接收前端传回的用户名与密码,进行比对,判断是否登录成功

登录视图函数代码如下:

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 = ''

大体思路:

  1. 检查填写的用户名与密码格式是否正确
  2. 检查用户名是否存在
  3. 检查密码是否正确
  4. 检查用户是否经过邮件确认
  5. 如果都通过了,设置登录相关状态,获取“记住我”信息,决定是否设置“记住我”状态
  6. 重新渲染页面并提示错误(验证未通过,登陆失败)或跳转(验证通过,登录成功)

4. 注册视图

编写注册视图,这次要实现的功能只有一个 —— 接收前端数据,比对是否注册成功

需要获得的信息:

  1. 用户名
  2. 密码
  3. 再次确认的密码
  4. 邮箱

比对流程思路:

  1. 检查格式是否正确
  2. 检查两次输入的密码是否相同
  3. 检查用户名是否已经存在
  4. 检查邮箱是否已经存在
  5. 检查密码是否合法(强度是否足够高)
  6. 检查邮箱是否合法
  7. 如果有检查未通过的地方,重新渲染页面并提示错误,若没有,创建新用户并跳转至邮箱验证页面

注册视图函数代码如下:

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. 邮箱验证视图

编写邮箱验证视图

需要获得的信息:

  1. 获得邮箱验证码的标签
  2. 前端输入的邮箱验证码

大体思路如下:

  1. 检查标签所对应的邮箱验证码是否存在
  2. 检查邮件是否过期
  3. 检查邮箱验证码是否正确
  4. 如果正确,跳转至登录页面并删除邮箱验证码,若不正确,重新渲染页面并提示错误

邮箱验证视图代码如下:

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})

至此,今天的内容就完结啦~

下篇继续做登录视图~