作者:Hubery 时间:2018.10.31
接上文:接上文:Django2 Web 实战02-用户注册登录退出
视频是一种可视化媒介,因此视频数据库至少应该存储图像。让用户上传文件是个很大的隐患,因此接下来会讨论这俩话题:文件上传,安全隐患。
- 新增一个文件上传函数,让用户给movie上传图片
- 检查OWASP列举的前10项安全隐患
我们会检查文件上传的安全隐患。可以看下Django帮我们做了什么,以及什么地方我们应该做出谨慎的决策。
1. 文件上传
这里,我们会创建一个model,展示和管理要上传到网站上的文件;然后,创建一个form和视图来验证和处理上传过程。
1.1 准备文件上传配置项
开始着手文件上传之前,我们需要知道,文件上传取决于一系列的设置,且这些设置在开发环境和生产环境上是不同的。这些设置会影响文件的存储方式和访问方式。 Django有两套文件配置:STATIC_* 和MEDIA_*。 Static
文件是我们项目的一部分,比如(CSS,JS)。 Media
文件是用户上传到我们系统中的文件。Media文件不应被信任,切不能执行。 我们将会在settings.py
文件中设置这两个地方:
MEDIA_URL = '/uploaded'
MEDIA_ROOT = os.path.join(BASE_DIR, '../media_root')
复制代码
MEDIA_URL
, 是用来给上传的文件服务的URL。 开发环境
中,这个值无关紧要,同样不会与我们视图中的URL冲突。 生产环境
中,上传的文件应该给一个与我们工程中任何app不同的域URL,同时还不能是子域。 用户的浏览器被欺骗执行它从同一域(或子域)中请求来的文件,因为我们的app将信任该与我们用户cookie(包括session ID)相同的文件。 所有浏览器的默认策略是:同源策略(Same Origin Policy)。 MEDIA_ROOT
是Django保存代码目录的路径。 我们应该确保该目录不在我们的工程代码目录下,这样就不会意外的将该目录加入版本控制范围,或者意外的授予该目录文件一些特定的权限,如执行。 在生产环境中,还有其他的配置项需要配置,如限制请求body等,这些会在后续的部分讨论。
接下来,创建media_root目录: 命令行至:与我们的项目最外层目录平级
mkdir media_root
ls
复制代码
1.2 创建MovieImage模型
MovieImage模型用一个新的字段ImageField来存储文件,同时也会验证该文件是否是图片。尽管ImageField会验证该字段,但仅仅靠阻止那些制造恶意文件的用户是不够的(但会帮助意外点击.zip文件的用户,而不是.png的用户)。 Django用Pillow
库来做验证,所以先添加Pillow库到环境中:
pip install Pillow
复制代码
默认在命令行中直接pip install Pillow,安装的是最新版本; 另外提供一种更优雅的命令行安装方式:
touch requirements.dev.txt //创建文件
vi requirements.dev.txt // 编辑文件
// 输入版本号 Pillow<4.4.0 然后保存
pip install -r requirements.dev.txt // 执行py库安装
复制代码
接下来开始创建model: core/models.py
def movie_directory_path_with_uuid(instance, filename):return '{}/{}'.format(instance.movie_id, uuid4())class MovieImage(models.Model):image = models.ImageField(upload_to=movie_directory_path_with_uuid)uploaded = models.DateTimeField(auto_now_add=True)movie = models.ForeignKey('Movie', on_delete=models.CASCADE)user = models.ForeignKey(settings.AUTH_PASSWORD_VALIDATORS,on_delete=models.CASCADE)
复制代码
ImageField
是FileField
的一个特殊字段,用Pillow
来确认一个文件是否是图片。ImageField
和FileField
使用Django的文件存储API
来工作(提供了一种读取文件的方式),同时可以进行文件的读写。 Django自带了FileSystemStorage
,实现了存储API将文件数据存储到本地文件系统上。这对开发来说足够了,但后续我们会考虑替代方案。
我们用ImageField
的upload_to
参数来指定一个方法,用来生成上传文件的名字。我们不希望用户可以在我们的系统中指定文件的名字,因为他们可能会滥用一些用户信任的名字,从而使我们难堪。鉴于此,我们使用一个函数将指定的movie的所有图片存储在同一目录中,同时用uuid4
为每个文件生成一个通用
的名字(这也避免了名字冲突
和处理文件之间的相互覆盖
问题)。
我们同时会记录是谁上传的文件,这样如果我们发现一个坏的文件,相当于提供了一种如何找到其他坏文件的线索。
模型创建完,更新数据库:
python manage.py makemigrations core
复制代码
有了模型,就可以创建其他部分,如表单和视图。
1.3 创建和使用MovieImageForm
MovieImageForm和之前的VoteForm相似,它会隐藏和禁用模型所需的movie和user字段,这很难取得客户的信任。
编辑core/forms.py
# 添加文件上传form
class MovieImageForm(forms.ModelForm):movie = forms.ModelChoiceField(widget=forms.HiddenInput,queryset=Movie.objects.all(),disabled=True,)user = forms.ModelChoiceField(widget=forms.HiddenInput,queryset=get_user_model().objects.all(),disabled=True,)class Meta:model = MovieImagefields = ('image', 'user', 'movie')
复制代码
表单ModelForm中,我们没有重写MovieImage的image字段,因为ModelForm会自动提供一正确的文件选择框:<input type="file">。
现在我们在视图MovieDetail中使用这个表单, core/views.py:
# movie详情 视图
class MovieDetail(DetailView):queryset = Movie.objects.all_with_related_persons_and_score()def get_context_data(self, **kwargs):ctx = super().get_context_data(**kwargs)# 配置图片上传表单ctx['image_form'] = self.movie_image_form()# 其他 略# 添加图片上传表单def movie_image_form(self):if self.request.user.is_authenticated:return MovieImageForm()return None
复制代码
这里的上传代码比较简单,只能上传新图片,没有其他操作,一只提供一个空表单。然而通过这种方式我们不能显示错误信息。实践中,丢失error信息不是很好的做法。
1.4 更新模版movie_detail.html显示和上传图片
我们需要对movie_detail.html模版进行两次更新。
- 需要更新main模版的block新增一个图片列表。
- 需要更新sidebar模版的block包含我们新建的上传表单。
编辑core/templates/core/movie_detail.html
{% extends 'base.html' %}{% block title %}{{ object.title }} - {{ block.super }}
{% endblock %}{% block main %}<h1>{{ object }}</h1><p class="lead">{{ object.plot }}</p>{# 展示电影图片列表 #}<div class="col"><h1>{{ object }}</h1><p class="lead"> {{ object.plot }}</p></div><ul>{% for i in object.movieimage_set.all %}<li class="list-inline-item"><img src="{{ i.image.url }}"></li>{% endfor %}</ul><p>由 {{ object.director }} 执导。</p>
{% endblock %}{% block sidebar %}{# 电影排名部分 #}<div>这个电影排名:<span class="badge badge-primary">{{ object.get_rating_display }}</span></div><div><h2>该片得分:{{ object.score|default_if_none:"TBD-暂无得分" }}</h2></div>{# 文件上传部分 #}{% if image_form %}<div><h2>上传新图片</h2><form method="post"enctype="multipart/form-data"action="{% url 'core:MovieImageUpload' movie_id=object.id %}">{% csrf_token %}{{ image_form.as_p }}<p><button class="but btn-primary">上传</button></p></form></div>{% endif %}{# 投票部分 #}{% if vote_form %}<form method="post" action="{{ vote_form_url }}">{% csrf_token %}{{ vote_form.as_p }}<button class="btn btn-primary">投票</button></form>{% else %}<p> 先登录,再给此电影投票</p>{% endif %}
{% endblock %}
复制代码
更新movie_detail.html的main和sidebar部分。 main block
中,用image
字段的url
属性,返回MEDIA_URL
中设置的URL,再与计算的名字相拼接,然后我们可以通过tag找到正确的图片。 sidebar block
中,form tag中一定要引入enctype属性,以便可以让上传的文件与请求的属性相关联。
模版升级完成,可以开始创建保存上传文件的视图了:MovieImageUpload。
1.5 创建MovieImageUpload视图
编辑core/views.py文件
# 创建图片上传视图
class MovieImageUpload(LoginRequiredMixin, CreateView):form_class = MovieImageFormdef get_initial(self):initial = super().get_initial()initial['user'] = self.request.user.idinitial['movie'] = self.kwargs['movie_id']return initialdef render_to_response(self, context, **response_kwargs):movie_id = self.kwargs['movie_id']movie_detail_url = reverse('core:MovieDetail',kwargs={'pk': movie_id})return redirect(to=movie_detail_url)def get_success_url(self):movie_id = self.kwargs['movie_id']movie_detail_url = reverse('core:MovieDetail', kwargs={'pk': movie_id})return movie_detail_url
复制代码
视图再一次做了验证和保存模型的所有工作。我们从请求的user属性中获取user.id属性,从URL中获取movie ID,当MovieImageForm的user和movie字段不可用时(忽略请求body体中的参数值),将user和movie ID当作初始参数传给form。 Django的ImageField会对文件改名和存储。
1.6 将请求关联到视图和文件上
将文件上传视图MovieImageUpload关联到URLConf中。 编辑core/urls.py
from django.conf.urls import url
from django.urls import pathfrom core import viewsapp_name = 'core'urlpatterns = [# 省略其他路径# 配置path('movie/<int:movie_id>/image/upload',views.MovieImageUpload.as_view(),name='MovieImageUpload'),
]
复制代码
像往常一样,我们添加一个path()函数,确保传入一个movie_id参数。 现在Django就知道如何找到我们新增的文件上传视图,只是它还不知道如何对外提供这个上传的文件。 在开发环境中,为了对外提供该上传的文件,更新下urls.py文件: MyMovie/urls.py
from django.conf import settings
from django.conf.urls.static import staticfrom django.contrib import admin
from django.urls import path, includeimport core.urls
import user.urlsMEDIA_FILE_PATHS = static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)urlpatterns = [path('admin/', admin.site.urls),path('user/', include(user.urls, namespace='user')),path('', include(core.urls, namespace='core')),
] + MEDIA_FILE_PATHS
复制代码
Django提供了static()
函数,返回一个包含单路径对象的列表,该对象将以字符串MEDIA_URL
开头的任何请求路由到document_root
中的文件。 开发环境中,这给我们提供了一种上传图片文件的方式。这种方式不适合生产环境,如果settings.DEBUG
是False
,static()
函数将返回一个空列表。
天星技术团QQ:557247785
。