基于django开发的bbs博客系统[从0到1]

.

由于是我自己个人全程完成,所以内容上比较清晰

1
2
3
4
5
6
7
1. 需求分析-需求分析说明书
2. 概要设计-概要设计说明书
3. 详细设计-详细设计说明书
4. 编码-编码说明书
5. 测试-测试计划、测试用例等相关文档
6. 上线部署-上线部署说明书和操作手册
7. 维护和升级-维护手册和升级说明书等

这里安全漏洞的黑盒测试和白盒测试在上线部署和维护阶段,如图

imageb7c7053f910e40e8.png

另外就是维护和升级阶段内容我会和上线


需求分析

与客户沟通了解他们的需求并进行详细的需求分析,明确软件系统所需要具备的功能、性能、质量等方面的需求

【鉴于内容长度限制,故不单独写需求分析报告,数据流图与数据字典等】

我想开发一套类似于博客园的博客系统

1.引言

1.1 编写目的

本文主要介绍一款基于 Django 框架开发的博客系统的需求分析。该博客系统旨在提供一个简单、易用、高效的博客平台,为用户提供撰写、发布、分享和管理博客的功能。

1.2 项目背景

随着互联网技术的飞速发展,博客作为一种新型的网络媒体形式,已经成为人们进行自我表达、知识共享和交流的重要平台。博客的兴起,不仅极大地促进了信息传播和文化交流,也为个人及企业展示自我、树立品牌形象提供了广阔的空间和机会。

在此背景下,越来越多的人开始寻找一款简单、易用、高效的博客系统,以方便他们撰写、发布、分享和管理博客。然而,目前市面上的博客系统,要么功能过于繁琐复杂,要么界面设计不够优美、用户体验不佳,无法完全满足用户的需求。

因此,我们决定开发一款基于 Django 框架的博客系统,旨在提供一个简单、易用、高效、安全的博客平台,为用户提供撰写、发布、分享和管理博客的功能,并满足高并发、可扩展、稳定运行、数据安全、易维护等要求。该博客系统将吸收借鉴当前主流博客系统的优点,同时针对当前存在的问题进行改进和创新,力争成为一款优秀的、充满活力的博客平台。

2.功能需求

2.1 用户系统

  • 用户注册:用户可以通过邮箱或手机号码注册账号。
  • 用户登录:用户可以使用邮箱/手机号码和密码登录。
  • 密码重置:用户可以通过注册的邮箱或手机号码重置密码。

2.2 博客系统

  • 博客列表页:展示所有博客的标题、作者、发布时间和阅读量等信息,并支持分类和标签筛选。
  • 博客详情页:展示某篇博客的详细内容,并显示评论区。
  • 博客编辑页:用户可以编写新的博客或编辑已有的博客,支持富文本编辑器和 Markdown 编辑器。
  • 博客评论:用户可以在博客详情页下方发表评论或回复其他人的评论,支持图片上传和 Markdown 格式。
  • 博客搜索:用户可以通过关键词搜索博客的标题和内容。

2.3 管理后台

  • 登录认证:只有管理员才能登录到管理后台,需要进行身份验证。
  • 博客管理:管理员可以查看、编辑和删除所有已发布的博客,支持分类和标签管理。
  • 评论管理:管理员可以查看、审核和删除所有用户发表的评论。

2.4 其他功能

  • 友情链接:管理员可以添加、编辑和删除友情链接,并在博客页面展示。
  • 网站公告:管理员可以发布网站公告,并在博客页面展示。

3. 非功能需求

3.1 性能要求

  • 响应速度:系统需要保证在高并发访问时仍能保持较快的响应速度,页面加载时间不应超过 3 秒。
  • 可扩展性:系统需要支持水平扩展,能够适应不断增长的用户访问量。
  • 稳定性:系统需要保证稳定运行,不会因为意外错误或攻击而宕机或数据丢失。

3.2 安全要求

  • 用户安全:用户密码需要经过加密处理存储,账号需要进行验证才能注册成功。
  • 数据安全:系统需要进行数据备份和恢复,以避免数据丢失或损坏造成的影响。
  • 防止攻击:系统需要采取一定的安全措施,如防火墙、DDoS 攻击防护等,以保证系统的安全性。

3.3 易用性要求

  • 用户友好:系统需要提供简洁、明了的界面,使用户能够轻松地撰写博客、发布评论等。
  • 高效性:系统需要具备高效的操作速度和快捷的操作方式,以提升用户的使用体验。
  • 可访问性:系统需要考虑到残障人士的特殊需求,如视觉障碍者需要支持屏幕阅读器。

概要设计

概述

该博客系统旨在提供一个用户友好且易于使用的博客平台,使用户能够创建、编辑和分享自己的博客文章。系统将采用 Python 的 Django 框架进行开发,并将使用MySQL 数据库存储数据。

功能需求

用户管理

  • 支持注册新用户,并验证其输入的信息(如用户名、密码、电子邮件地址等)是否符合规范。
  • 支持登录和注销功能,并为已登陆用户提供个人资料页面以及修改密码和电子邮件地址的功能。

博客文章管理

  • 支持用户创建、编辑和删除自己的博客文章。
  • 支持用户发布、修改和删除评论。
  • 支持用户对文章进行分类和标签,以便于浏览和搜索。

网站管理

  • 支持管理员登录和注销功能,并提供管理后台。
  • 支持管理员对用户、文章、评论进行管理,包括禁言用户、删除文章和评论等。

技术实现

前端

  • 使用 HTML、CSS 和 JavaScript 实现前端界面。
  • 使用 Bootstrap 框架简化前端开发过程。
  • 使用 jQuery 简化 JavaScript 开发。

后端

  • 使用 Django 框架开发后端代码。
  • 使用 SQLite 或 MySQL 存储数据。
  • 使用 Django REST framework 提供 API 接口。

安全

  • 使用 HTTPS 加密用户与服务器之间的通信。
  • 对用户输入数据进行验证和过滤,避免 SQL 注入、XSS 等攻击。
  • 编写安全性强的代码,并定期对代码进行审查和测试。

部署方式

  • 使用 Nginx 或 Apache 作为反向代理服务器。
  • 使用 Gunicorn 或 uWSGI 将 Django 应用程序部署到生产环境中。
  • 将静态文件上传到 Amazon S3 或 CDN 上,以提高网站加载速度。
  • 定期备份数据,确保数据安全性。

详细设计

数据库表设计

用户表(auth_user)

该表用于存储所有用户的信息,包括用户名、密码、电子邮件地址等。Django 自带了该表格,因此无需手动创建。

列名 类型 描述
id int 用户 ID
password varchar 加密后的用户密码
last_login datetime 上次登录时间,可为空
is_superuser bool 是否是超级用户
username varchar 用户名
first_name varchar 用户名字,可为空
last_name varchar 用户姓氏,可为空
email varchar 用户电子邮件地址
is_staff bool 是否是工作人员(管理员)
is_active bool 用户账号是否激活
date_joined datetime 用户注册时间

外键字段
一对一个人站点表

个人站点表

image6b361be1395097f5.png

每个人都有自己的个人主页,所以有个人站点表

    site_name 站点名称
    site_title      站点标题
    site_theme    站点样式
列名 类型 描述
site_name varchar 站点名称
site_title varchar 站点标题
site_theme varchar 站点样式

文章标签表

用来创建标签,每个标签叫什么名字

image442a7b8b05e0e3d1.png

列名 类型 描述
name varchar 标签名

文章分类表

image46509a0e3820dc8f.png

列名 类型 描述
name varchar 标签名

文章表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
title	文章标题
desc 文章简介
content 文章内容
create_time 发布时间

数据库字段设计优化(******)
(虽然下述的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效率)
up_num 点赞数
down_num 点踩数
comment_num 评论数

外键字段
一对多个人站点
多对多文章标签
一对多文章分类

点赞点踩表

1
2
3
4
5
6
7
8
9
记录哪个用户给哪篇文章点了赞还是点了踩
user ForeignKey(to="User")
article ForeignKey(to="Article")
is_up BooleanField()

1 1 1
1 2 1
1 3 0
2 1 1

评论表

1
2
3
4
5
6
7
8
9
10
11
12
13
	记录哪个用户给哪篇文章写了哪些评论内容
user ForeignKey(to="User")
article ForeignKey(to="Article")
content CharField()
comment_time DateField()
# 自关联
parent ForeignKey(to="Comment",null=True)
# ORM专门提供的自关联写法
parent ForeignKey(to="self",null=True)

id user_id article_id parent_id
1 1 1
2 2 1 1

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from django.db import models

# Create your models here.
"""
先写普通字段
之后再写外键字段
"""
from django.contrib.auth.models import AbstractUser


class UserInfo(AbstractUser):
phone = models.BigIntegerField(verbose_name='手机号',null=True)
# 头像
avatar = models.FileField(upload_to='avatar/',default='avatar/default.png',verbose_name='用户头像')
"""
给avatar字段传文件对象 该文件会自动存储到avatar文件下 然后avatar字段只保存文件路径avatar/default.png
"""
create_time = models.DateField(auto_now_add=True)

blog = models.OneToOneField(to='Blog',null=True, on_delete=models.CASCADE)


class Blog(models.Model):
site_name = models.CharField(verbose_name='站点名称',max_length=32)
site_title = models.CharField(verbose_name='站点标题',max_length=32)
# 简单模拟 带你认识样式内部原理的操作
site_theme = models.CharField(verbose_name='站点样式',max_length=64) # 存css/js的文件路径


class Category(models.Model):
name = models.CharField(verbose_name='文章分类',max_length=32)
blog = models.ForeignKey(to='Blog',null=True, on_delete=models.CASCADE)


class Tag(models.Model):
name = models.CharField(verbose_name='文章标签',max_length=32)
blog = models.ForeignKey(to='Blog', null=True, on_delete=models.CASCADE)


class Article(models.Model):
title = models.CharField(verbose_name='文章标题',max_length=64)
desc = models.CharField(verbose_name='文章简介',max_length=255)
# 文章内容有很多 一般情况下都是使用TextField
content = models.TextField(verbose_name='文章内容')
create_time = models.DateField(auto_now_add=True)

# 数据库字段设计优化
up_num = models.BigIntegerField(verbose_name='点赞数',default=0)
down_num = models.BigIntegerField(verbose_name='点踩数',default=0)
comment_num = models.BigIntegerField(verbose_name='评论数',default=0)

# 外键字段
blog = models.ForeignKey(to='Blog', null=True, on_delete=models.CASCADE)
category = models.ForeignKey(to='Category',null=True, on_delete=models.CASCADE)
tags = models.ManyToManyField(to='Tag',
through='Article2Tag',
through_fields=('article','tag')
)


class Article2Tag(models.Model):
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
tag = models.ForeignKey(to='Tag', on_delete=models.CASCADE)


class UpAndDown(models.Model):
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
is_up = models.BooleanField() # 传布尔值 存0/1


class Comment(models.Model):
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
content = models.CharField(verbose_name='评论内容',max_length=255)
comment_time = models.DateTimeField(verbose_name='评论时间',auto_now_add=True)
# 自关联
parent = models.ForeignKey(to='self', null=True, on_delete=models.CASCADE) # 有些评论就是根评论

新建数据库bbs14

配置django数据库连接

settings.py

1
2
3
4
5
6
7
8
9
10
11
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": 'bbs14',
"USER": 'root',
"PASSWORD": 'root',
"HOST": '127.0.0.1',
"PORT": '3306',
"CHARSET": 'utf8',
}
}

init.py

1
2
import pymysql
pymysql.install_as_MySQLdb()

在项目中init.py中这个报错原因,python 3.5以上版本不支持这种方式,指定版本

1
2
3
import pymysql
pymysql.version_info=(1,4,3,"final",0) # 指定了pymysql的版本:1.4.3,按照你版本修改
pymysql.install_as_MySQLdb()

创建app01项目

1
python manage.py startapp app01

在models.py里写字段

在settings.py里面修改配置

1
AUTH_USER_MODEL = 'app01.UserInfo'

这里定义外键的时候需要加上 on_delete=;

1
, on_delete=models.CASCADE

数据库迁移

1
2
D:\pycharm\python\python.exe manage.py makemigrations
D:\pycharm\python\python.exe manage.py migrate

注册功能

注册功能这里是这样安排的,现在urls.py里写好路由之后

1
re_path(r'^register/',views.register,name='reg')

在views.py里面写好对应的方法

这里我并不是直接在register方法内部写好处理性的代码,将post收到的内容交给form组件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def register(request):
form_obj = MyRegForm()
if request.method == 'POST':
back_dic = {"code": 1000, 'msg': ''}
# 校验数据是否合法
form_obj = MyRegForm(request.POST)
# 判断数据是否合法
if form_obj.is_valid():
# print(form_obj.cleaned_data) # {'username': 'jason', 'password': '123', 'confirm_password': '123', 'email': '123@qq.com'}
clean_data = form_obj.cleaned_data # 将校验通过的数据字典赋值给一个变量
# 将字典里面的confirm_password键值对删除
clean_data.pop('confirm_password') # {'username': 'jason', 'password': '123', 'email': '123@qq.com'}
# 用户头像
file_obj = request.FILES.get('avatar')
"""针对用户头像一定要判断是否传值 不能直接添加到字典里面去"""
if file_obj:
clean_data['avatar'] = file_obj
# 直接操作数据库保存数据
models.UserInfo.objects.create_user(**clean_data)
back_dic['url'] = '/login/'
else:
back_dic['code'] = 2000
back_dic['msg'] = form_obj.errors
return JsonResponse(back_dic)
return render(request, 'register.html', locals())

form组件—myforms.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 书写针对用户表的forms组件代码
from django import forms
from app01 import models


class MyRegForm(forms.Form):
username = forms.CharField(label='用户名', min_length=3, max_length=8,
error_messages={
'required': '用户名不能为空',
'min_length': "用户名最少3位",
'max_length': "用户名最大8位"
},
# 还需要让标签有bootstrap样式
widget=forms.widgets.TextInput(attrs={'class': 'form-control'})
)

password = forms.CharField(label='密码', min_length=3, max_length=8,
error_messages={
'required': '密码不能为空',
'min_length': "密码最少3位",
'max_length': "密码最大8位"
},
# 还需要让标签有bootstrap样式
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)

confirm_password = forms.CharField(label='确认密码', min_length=3, max_length=8,
error_messages={
'required': '确认密码不能为空',
'min_length': "确认密码最少3位",
'max_length': "确认密码最大8位"
},
# 还需要让标签有bootstrap样式
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱',
error_messages={
'required': '邮箱不能为空',
'invalid': '邮箱格式不正确'
},
widget=forms.widgets.EmailInput(attrs={'class': 'form-control'})
)

# 钩子函数
# 局部钩子:校验用户名是否已存在
def clean_username(self):
username = self.cleaned_data.get('username')
# 去数据库中校验
is_exist = models.UserInfo.objects.filter(username=username)
if is_exist:
# 提示信息
self.add_error('username', '用户名已存在')
return username

# 全局钩子:校验两次是否一致
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password == confirm_password:
self.add_error('confirm_password', '两次密码不一致')
return self.cleaned_data

register.html

这里我们不用form表单提交数据,用ajax提交数据,设置csrftoken

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h1 class="text-center">注册</h1>
<form id="myform"> <!--这里我们不用form表单提交数据 知识单纯的用一下form标签而已-->
{% csrf_token %}
{% for form in form_obj %}
<div class="form-group">
<label for="{{ form.auto_id }}">{{ form.label }}</label>
{{ form }}
<span style="color: red" class="pull-right"></span>
</div>
{% endfor %}
<div class="form-group">
<label for="myfile">头像
{% load static %}
<img src="{% static 'img/default.png' %}" id='myimg' alt="" width="100" style="margin-left: 10px">
</label>
<input type="file" id="myfile" name="avatar" style="display: none" >
</div>

<input type="button" class="btn btn-primary pull-right" value="注册" id="id_commit">
</form>
</div>
</div>
</div>

<script>
$("#myfile").change(function () {
// 文件阅读器对象
// 1 先生成一个文件阅读器对象
let myFileReaderObj = new FileReader();
// 2 获取用户上传的头像文件
let fileObj = $(this)[0].files[0];
// 3 将文件对象交给阅读器对象读取
myFileReaderObj.readAsDataURL(fileObj) // 异步操作 IO操作
// 4 利用文件阅读器将文件展示到前端页面 修改src属性
// 等待文件阅读器加载完毕之后再执行
myFileReaderObj.onload = function(){
$('#myimg').attr('src',myFileReaderObj.result)
}
})

$('#id_commit').click(function () {
// 发送ajax请求 我们发送的数据中即包含普通的键值也包含文件
let formDataObj = new FormData();
// 1.添加普通的键值对
{#console.log($('#myform').serializeArray()) // [{},{},{},{},{}] 只包含普通键值对#}
$.each($('#myform').serializeArray(),function (index,obj) {
{#console.log(index,obj)#} // obj = {}
formDataObj.append(obj.name,obj.value)
});
// 2.添加文件数据
formDataObj.append('avatar',$('#myfile')[0].files[0]);

// 3.发送ajax请求
$.ajax({
url:"",
type:'post',
data:formDataObj,

// 需要指定两个关键性的参数
contentType:false,
processData:false,

success:function (args) {
if (args.code==1000){
// 跳转到登陆页面
window.location.href = args.url
}else{
// 如何将对应的错误提示展示到对应的input框下面
// forms组件渲染的标签的id值都是 id_字段名
$.each(args.msg,function (index,obj) {
{#console.log(index,obj) // username ["用户名不能为空"]#}
let targetId = '#id_' + index;
$(targetId).next().text(obj[0]).parent().addClass('has-error')
})
}
}
})
})
// 给所有的input框绑定获取焦点事件
$('input').focus(function () {
// 将input下面的span标签和input外面的div标签修改内容及属性
$(this).next().text('').parent().removeClass('has-error')
})
</script>
</body>
</html>

利用这段js实现将上传之后的图片展示,再利用ajax提交post请求

image.png

登陆功能

urls.py

1
re_path(r'^login/',views.login,name='login'),

图片验证码

1
2
3
4
img标签的src属性
1.图片路径
2.url
3.图片的二进制数据

xxxx

编码和测试省略

上线部署

黑盒渗透测试和白盒代码审计

1)任意文件上传

image.png

利用的kindeditor编辑器进行文件上传

image999a65d93dcefe9d.png

定位到upload_image方法

image0ae12adc3e5b224d.png

这段内容接收名字是imgFile的文件上传,后缀和文件名没有经过任何过滤就拼接后上传(好在不能跨目录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def upload_image(request):
back_dic = {'error': 0, } # 先提前定义返回给编辑器的数据格式
# 用户写文章上传的图片 也算静态资源 也应该防盗media文件夹下
if request.method == "POST":
# 获取用户上传的图片对象
# print(request.FILES) # 打印看到了健固定叫imgFile
file_obj = request.FILES.get('imgFile')
# 手动拼接存储文件的路径
file_dir = os.path.join(settings.BASE_DIR,'media','article_img')
# 优化操作 先判断当前文件夹是否存在 不存在 自动创建
if not os.path.isdir(file_dir):
os.mkdir(file_dir) # 创建一层目录结构 article_img
# 拼接图片的完整路径
file_path = os.path.join(file_dir,file_obj.name)
with open(file_path,'wb') as f:
for line in file_obj:
f.write(line)
back_dic['url'] = '/media/article_img/%s'%file_obj.name

return JsonResponse(back_dic)

修复代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'}

def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


def upload_image(request):
back_dic = {'error': 0, } # 先提前定义返回给编辑器的数据格式
# 用户写文章上传的图片 也算静态资源 也应该防盗media文件夹下
if request.method == "POST":
# 获取用户上传的图片对象
# print(request.FILES) # 打印看到了健固定叫imgFile
file_obj = request.FILES.get('imgFile')

if file_obj and allowed_file(file_obj.name):
m = hashlib.md5()
m.update(str(time.time()).encode('utf-8'))
file_dir = m.hexdigest() + '.' + file_obj.name.rsplit('.', 1)[1].lower()

#
#
# # 手动拼接存储文件的路径
# file_dir = os.path.join(settings.BASE_DIR,'media','article_img')
# 优化操作 先判断当前文件夹是否存在 不存在 自动创建
if not os.path.isdir(file_dir):
os.mkdir(file_dir) # 创建一层目录结构 article_img
# 拼接图片的完整路径
file_path = os.path.join(file_dir,file_obj.name)
with open(file_path,'wb') as f:
for line in file_obj:
f.write(line)
back_dic['url'] = '/media/article_img/%s'%file_obj.name

return JsonResponse(back_dic)

image36d5a58e4dda12fe.pngimage03f8b138dae7d266.png

.

2)如果登陆失败,验证码并不刷新,这意味着攻击者可以爆破密码

image10d5afe2b63b7595.png

如图,验证码并不刷新,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def login(request):
if request.method == 'POST':
back_dic = {'code':1000,'msg':''}
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
# 1 先校验验证码是否正确 自己决定是否忽略 统一转大写或者小写再比较
if request.session.get('code').upper() == code.upper():
# 2 校验用户名和密码是否正确
user_obj = auth.authenticate(request,username=username,password=password)
if user_obj:
# 保存用户状态
auth.login(request,user_obj)
back_dic['url'] = '/home/'
else:
back_dic['code'] = 2000
back_dic['msg'] = '用户名或密码错误'
else:
back_dic['code'] = 3000
back_dic['msg'] = '验证码错误'
return JsonResponse(back_dic)
return render(request,'login.html')

修复方案:

这里在验证码错误和账户密码错误时,应当刷新

image0d6e2c40823751bc.png

但是如果你点击图片,会刷新,是因为点击图片时触发了一个点击事件的ajax请求

1
2
3
4
5
$("#id_img").click(function () {
// 1 先获取标签之前的src
let oldVal = $(this).attr('src');
$(this).attr('src',oldVal += '?')
})

就是将urlget_code变成了get_code/?,导致图片刷新

那么在我们点击登陆发送ajax请求之后,根据返回内容,如果失败就刷新url,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 点击按钮发送ajax请求
$("#id_commit").click(function () {
$.ajax({
url:'',
type:'post',
data:{
'username':$('#username').val(),
'password':$('#password').val(),
'code':$('#id_code').val(),
// 自己结合自己需求 合理选择
'csrfmiddlewaretoken':'{{ csrf_token }}'
},
success:function (args) {
if (args.code == 1000){
// 跳转到首页
window.location.href = args.url
}else{
// 渲染错误信息
$('#error').text(args.msg)
$("#id_img").click(function () {
// 1 先获取标签之前的src
let oldVal = $(this).attr('src');
$(this).attr('src',oldVal += '?')
})
}
}
})
}

登陆失败就刷新

效果展示:https://batmanfuture.cn/2023/05/15/bbs%E5%8D%9A%E5%AE%A2%E7%B3%BB%E7%BB%9F%E5%B1%95%E7%A4%BA/