.
由于是我自己个人全程完成,所以内容上比较清晰
1 2 3 4 5 6 7 1. 需求分析-需求分析说明书 2. 概要设计-概要设计说明书 3. 详细设计-详细设计说明书 4. 编码-编码说明书 5. 测试-测试计划、测试用例等相关文档 6. 上线部署-上线部署说明书和操作手册 7. 维护和升级-维护手册和升级说明书等
这里安全漏洞的黑盒测试和白盒测试在上线部署和维护阶段,如图
另外就是维护和升级阶段内容我会和上线
需求分析
与客户沟通了解他们的需求并进行详细的需求分析,明确软件系统所需要具备的功能、性能、质量等方面的需求
【鉴于内容长度限制,故不单独写需求分析报告,数据流图与数据字典等】
我想开发一套类似于博客园的博客系统
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
用户注册时间
外键字段 一对一个人站点表
个人站点表
每个人都有自己的个人主页,所以有个人站点表
site_name 站点名称
site_title 站点标题
site_theme 站点样式
列名
类型
描述
site_name
varchar
站点名称
site_title
varchar
站点标题
site_theme
varchar
站点样式
文章标签表 用来创建标签,每个标签叫什么名字
列名
类型
描述
name
varchar
标签名
文章分类表
列名
类型
描述
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 ) parent ForeignKey(to="self" ,null=True ) id user_id article_id parent_id1 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""" 先写普通字段 之后再写外键字段 """ from django.contrib.auth.models import AbstractUserclass 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 ) 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 ) 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() 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 pymysqlpymysql.install_as_MySQLdb()
在项目中init.py中这个报错原因,python 3.5以上版本不支持这种方式,指定版本
1 2 3 import pymysqlpymysql.version_info=(1 ,4 ,3 ,"final" ,0 ) 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(): clean_data = form_obj.cleaned_data clean_data.pop('confirm_password' ) 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 from django import formsfrom app01 import modelsclass MyRegForm (forms.Form): username = forms.CharField(label='用户名' , min_length=3 , max_length=8 , error_messages={ 'required' : '用户名不能为空' , 'min_length' : "用户名最少3位" , 'max_length' : "用户名最大8位" }, 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位" }, 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位" }, 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" > {% 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请求
登陆功能 urls.py
1 re_path(r'^login/' ,views.login,name='login' ),
图片验证码 1 2 3 4 img标签的src属性 1.图片路径 2.url 3.图片的二进制数据
xxxx
编码和测试省略 上线部署
黑盒渗透测试和白盒代码审计
1)任意文件上传
利用的kindeditor编辑器进行文件上传
定位到upload_image方法
这段内容接收名字是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 , } if request.method == "POST" : 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) 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 , } if request.method == "POST" : 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() if not os.path.isdir(file_dir): os.mkdir(file_dir) 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)
.
2)如果登陆失败,验证码并不刷新,这意味着攻击者可以爆破密码
如图,验证码并不刷新,
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' ) if request.session.get('code' ).upper() == code.upper(): 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' )
修复方案:
这里在验证码错误和账户密码错误时,应当刷新
但是如果你点击图片,会刷新,是因为点击图片时触发了一个点击事件的ajax请求
1 2 3 4 5 $("#id_img" ).click (function ( ) { 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 $("#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 ( ) { 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/