Django2 Web 實戰03-文件上傳

做者:Hubery 時間:2018.10.31html

接上文:接上文:Django2 Web 實戰02-用戶註冊登陸退出python

視頻是一種可視化媒介,所以視頻數據庫至少應該存儲圖像。讓用戶上傳文件是個很大的隱患,所以接下來會討論這倆話題:文件上傳,安全隱患。數據庫

  • 新增一個文件上傳函數,讓用戶給movie上傳圖片
  • 檢查OWASP列舉的前10項安全隱患

咱們會檢查文件上傳的安全隱患。能夠看下Django幫咱們作了什麼,以及什麼地方咱們應該作出謹慎的決策。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等,這些會在後續的部分討論。bash

接下來,建立media_root目錄: 命令行至:與咱們的項目最外層目錄平級cookie

mkdir media_root
ls 
複製代碼

1.2 建立MovieImage模型

MovieImage模型用一個新的字段ImageField來存儲文件,同時也會驗證該文件是不是圖片。儘管ImageField會驗證該字段,但僅僅靠阻止那些製造惡意文件的用戶是不夠的(但會幫助意外點擊.zip文件的用戶,而不是.png的用戶)。 Django用Pillow庫來作驗證,因此先添加Pillow庫到環境中:session

pip install Pillow
複製代碼

默認在命令行中直接pip install Pillow,安裝的是最新版本; 另外提供一種更優雅的命令行安裝方式:app

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)
複製代碼

ImageFieldFileField的一個特殊字段,用Pillow來確認一個文件是不是圖片。ImageFieldFileField使用Django的文件存儲API來工做(提供了一種讀取文件的方式),同時能夠進行文件的讀寫。 Django自帶了FileSystemStorage,實現了存儲API將文件數據存儲到本地文件系統上。這對開發來講足夠了,但後續咱們會考慮替代方案。

咱們用ImageFieldupload_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 = MovieImage
        fields = ('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 = MovieImageForm

    def get_initial(self):
        initial = super().get_initial()
        initial['user'] = self.request.user.id
        initial['movie'] = self.kwargs['movie_id']
        return initial

    def 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 path

from core import views

app_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 static

from django.contrib import admin
from django.urls import path, include

import core.urls
import user.urls

MEDIA_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.DEBUGFalsestatic()函數將返回一個空列表。

天星技術團QQ:557247785

歡迎來擾
相關文章
相關標籤/搜索