作者: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
。