一、添加视图函数

打开​​article/urls.py​​,并加入如下代码:

...

# 删除文章
def article_delete(request,id):
# 根据 id 获取需要删除的文章
article = ArticlePost.objects.get(id=id)
# 调用.delete()方法删除文章
article.delete()
# 完成删除后返回文章列表
return redirect("article:article_list")
  • 与查询文章类似,因为需要知道具体应该删除哪一篇文章,因此必须传入文章的​​id​​;
  • 紧接着调用​​.delete()​​函数删除数据库中这篇文章的条目;
  • 删除成功后返回文章列表;

二、添加路由地址

打开​​article/urls.py​​,并加入如下代码:

...

urlpatterns = [
...
# 删除文章
path('article-delete/<int:id>', views.article_delete, name='article_delete'),
]

三、添加删除文章入口

在文章详情的页面进行删除操作,因此修改模板​​detail.html​​:

<div class="container">
<div class="row">
...
<div class="col-12 alert alert-success">
作者:{{ article.author }} · <a href="#" onclick="confirm_delete()">删除文章</a>
</div>
...
</div>
</div>

四、增加弹窗

运行服务器查看,此时就已经可以删除文章了,但是这样存在弊端,删除文章没有提醒,如果不小心点到了,辛辛苦苦写的文章就白写了,所以,应该在删除文章前出现一个弹窗,询问用户是否删除文章。

在这里引入​​Layer​​弹窗组件,layer是一款非常好用的web弹窗组件,具备全方位的解决方案。

官网下载Layer插件:​​Layer​

下载完成之后将里面的​​layer​​​文件夹(含有​​layer.js​​​的)直接复制到项目的​​static​​文件夹下。

Django搭建个人博客--删除文章_html


Django搭建个人博客--删除文章_jquery_02


为了未来在所有页面都能使用Layer弹窗功能,在​​base.html​​中通过标签引入:

<body>
...
<!-- bootstrap.js依赖 jquery.js 和 popper.js ,因此在这里引入 -->
<script src="{% static 'jquery/jquery-3.3.1.js' %}"></script>
<!-- 引入layer.js -->
<script src="{% static 'layer/layer.js' %}"></script>
...
</body>

注意:​​layer​​​插件依赖​​jquery​​​才能正常工作,因此要在​​jquery​​​的后面引入​​layer​​;

改写模板​​detail.html​​:

{% block content %}
<!-- 文章详情 -->
<div class="container">
<div class="row">
...
<div class="col-12 alert alert-success">
作者:{{ article.author }} · <a href="#" onclick="confirm_delete()">删除文章</a>
</div>
...
</div>
</div>

<script>//删除文章的函数
function confirm_delete()
{
layer.open
({
//弹窗标题
title: "确认删除",
//正文
content: "确认删除这篇文章吗?",
//点击确定按钮后调用的回调函数
yes: function(index, layero)
{
//指定应当前往的url
location.href='{% url "article:article_delete" article.id %}'
},
})
}</script>
  • ​<a>​​​标签增加了​​onclick​​​属性,表示在点击链接时调用后面的​​confirm_delete()​​函数;
  • ​confirm_delete()​​​函数中调用了layer弹窗组件,对弹窗的标题、正文以及确定键进行了定义。​​location.href​​是点击确定键后应该前往的地址,即删除文章的url。
  • 通过​​onclick​​​实现了功能逻辑,因此​​href​​链接就不需要在跳转了;

Django搭建个人博客--删除文章_html_03


五、提升安全性

使用上面的方法实现删除文章功能虽然难度不大,但是上面的方法是存在安全隐患的。要继续深入探讨,就得提到跨域请求伪造攻击,也称​​CSRF​​攻击(Cross-site request forgery)。


5.1、CSRF攻击

CSRF攻击可以理解为:攻击者盗用了你的身份,以你的名义发送恶意请求。以删除文章为例:

  • 用户登录了博客网站A,浏览器记录下这次会话,并保持了登录状态;
  • 用户在没有退出登录的情况下,又非常不小心的打开了邪恶的攻击网站B
  • 攻击网站B在页面中植入恶意代码,悄无声息的向博客网站A发送删除文章的请求,此时浏览器误认为是用户在操作,从而顺利的执行了删除操作。

由于浏览器的同源策略,CSRF攻击者并不能得到你的登录数据实际内容,但是可以欺骗浏览器,让恶意请求附上正确的登录数据。

所以这里如果防范CSRF攻击的风险呢?采用的办法是删除文章时用POST方法,并且校验​​csrf​​令牌。


5.2、CSRF令牌

前面提到在Django中提交表单必须加​​csrf_token​​,这个就是CSRF令牌,它防范CSRF攻击的流程如下:

  • 当用户访问django站点时,django反馈给用户的表单中有一个隐含字段​​csrf_token​​,这个值是在服务器端随机生成的,每次都不一样;
  • 在后端处理POST请求前,django会校验请求的cookie里的​​csrf_token​​​和表单里的​​csrf_token​​是否一致。一致则请求合法,否则这个请求可能是来自CSRF攻击,返回406服务器禁止访问;

由于攻击者并不能得到用户的cookie内容(仅仅是考浏览器转发),所以通常情况下是无法构造出正确的​​csrf_token​​的,从而防范了此类攻击。


5.3、代码实现

修改删除文章的链接,以及点击它时调用的函数

打开​​templates/article/detail.html​​,做如下修改:

{% block content %}
<!-- 文章详情 -->
<div class="container">
<div class="row">
...
<div class="col-12 alert alert-success">
作者:{{ article.author }} · <a href="#" onclick="confirm_safe_delete()">删除文章</a>
</div>
<!-- 新增一个隐藏的表单 -->
<formstyle="display: none;"
id="safe_delete"
action="{% url 'article:article_safe_delete' article.id %}"
method="POST">
{% csrf_token %}
<button type="submit">发送</button>
</form>
...
</div>
</div>

<script>//删除文章的函数
function confirm_safe_delete()
{
layer.open
({
//弹窗标题
title: "确认删除",
//正文
content: "确认删除这篇文章吗?",
//点击确定按钮后调用的回调函数
yes: function(index, layero)
{
$('form#safe_delete button').click();
layer.close(index);
}
})
}</script>
  • 点击删除文章链接时,弹出layer弹窗;
  • 弹窗不在发起GET请求,而是通过Jquery选择器找到隐藏的表单,并点击发送按钮;
  • 表单发起POST请求,并携带了csrf令牌,从而避免了csrf攻击;

接着修改路由地址

打开​​article/urls.py​​,做如下修改:

...
urlpatterns = [
...
# 删除文章
path('article-delete/<int:id>', views.article_safe_delete, name='article_safe_delete'),
]

最后修改删除视图函数

打开​​article/views.py​​,做如下修改:

...
# 删除文章
def article_safe_delete(request,id):
if request.method == 'POST':
article = ArticlePost.objects.get(id=id)
article.delete()
return redirect("article:article_list")
else:
return HttpResponse("仅允许post请求")