Django2 Web 實戰03-檔案上傳
作者:Hubery 時間:2018.10.31
接上文:接上文: ofollow,noindex">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 = 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.DEBUG
是 False
, static()
函式將返回一個空列表。
天星技術團QQ: 557247785
。
