随便写点东西吧。
Django的Class based view十分好用,也很灵活。其中DeleteView( https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-editing/#django.views.generic.edit.DeleteView )有点特别蛋疼的,他理想的流程是这样:点击删除 -> 跳转到确认页面 -> 点击确认删除 -> 删除对象 -> 跳转到success_url。
这样导致删除的流程特别麻烦,特别是这个"确认页面":
因为"删除"是一个敏感操作,所以一定要有CSRF防御,所以点击上图这个"是的,我确认"按钮以后,会发送一个POST请求到后端的。Django会自动检查CSRF TOKEN。但实际上,我们也可以操作JavaScript向后端发送POST请求,而无需确认页面里的这个表单。
所以,我理想中的删除逻辑应该是这样的:点击删除 -> Javascript弹出确认框 -> 用户点击确认 -> Javascript生成一个表单 -> 提交 -> 跳转到success_url。
我写了个JavaScript函数,专门用来执行删除等需要POST的逻辑:
function submit(action_url) {
if(!confirm('确认要执行这个操作?')) {
return false;
}
var f = $('<form method="post"></form>');
var xsrf = $("{% csrf_token %}");
f.append(xsrf);
f.prop('action', action_url);
f.submit();
}
使用的时候,只要传入后端url作为action_url即可。
所以,我这个逻辑里面是不需要GET请求的,也就是说不需要写"确认删除"的模板,所以我上网上搜了一下如何才能不要这个模板: http://stackoverflow.com/questions/17475324/django-deleteview-without-confirmation-template ,果然是有人问过的。所以其实有很多人和我遇到同样的问题。
但我一看回答......:
很无语,直接把get导向post了,这样做必然会存在CSRF。因为 django.middleware.csrf.CsrfViewMiddleware 是不检查GET方法的:
准确来说,是不检查GET、HEAD、OPTIONS、TRACE方法。所以说,如果按照stackoverflow上面的事例来写代码的话,就是存在漏洞的。
另外,关于检查的方法。让我想到flask,flask-wtf有个小缺陷,默认情况下,它只检查POST/PUT/PATCH三个方法 https://github.com/lepture/flask-wtf/blob/f306c360f74362be3aac89c43cdc7c37008764fb/flask_wtf/csrf.py#L151 ,不知道开发者怎么想的。正常开发中,ajax请求里会存在很多DELETE方法,所以DELETE一定要检查CSRF TOKEN,否则很容易出现漏洞。
那么回到Django。既然上述做法会引发CSRF漏洞,那么我们怎么办?
我们分析一下问题,现在问题是:我们只需要POST方法,但默认的DeleteView要求提供GET和POST两个方法,并且GET方法需要一个模板,也就是"确认删除"这个页面的模板。如果我们不提供模板,用户在以GET方法访问这个页面的时候就会报错,但要写个模板,感觉多此一举,很不优雅。
(当然你可以无视报错,反正不影响正常使用,但作为一个强迫症我忍不了)
我们先分析一下Class based view的原理。Django的一个基类View类,其中有一个dispatch方法,所有的请求经由dispatch方法,再根据请求的方法具体分发到get、post、delete这样的函数里。
那么,一个请求允许哪些方法,是在_allowed_methods函数里定义的:
这个函数的意思就是:根据子类中定义过的方法名确定允许哪些方法。
比如,Django提供的BaseUpdateView类中定义了两个方法get和post:
所以,继承这个类的View一定允许GET和POST两种请求。而Python是个多继承语言,当它还继承了其他辅助类(Mixin),还可能会允许其他请求。那么,一旦用户的请求不在允许的范围内,就会调用http_method_not_allowed,具体现象就是返回405错误:
回到上面的问题,所以现在解决问题的方法就很明显了:重写get函数,让"GET"请求返回self.http_method_not_allowed()
就可以了:
class LinkDeleteView(AdminPermissionMixin, DeleteView):
model = Link
success_url = reverse_lazy('management-link-list')
permission_required = 'archives.delete_link'
def get(self, request, *args, **kwargs):
return self.http_method_not_allowed(request, *args, **kwargs)
就是这么简单。
或者,换个更调皮、更简略的写法:
class LinkDeleteView(AdminPermissionMixin, DeleteView):
model = Link
success_url = reverse_lazy('management-link-list')
permission_required = 'archives.delete_link'
get = DeleteView.http_method_not_allowed
想想为什么可以这么写,不懂的话可以看看Django源码,答案就在里面。