項目:開發一個簡單的BBS論壇
需求:
- 整體參考“抽屜新熱榜” + “虎嗅網”
- 實現不同論壇版塊
- 帖子列表展示
- 帖子評論數、點贊數展示
- 在線用戶展示
- 允許登錄用戶發貼、評論、點贊
- 允許上傳文件
- 帖子可被置頂
- 可進行多級評論
- 就先這些吧。。。
知識必備:
- Django
- HTML\CSS\JS
- BootStrap
- Jquery
設計表結構
1 # -*- coding:utf-8 -*- 2 from django.db import models 3 from django.contrib.auth.models import User 4 from django.core.exceptions import ValidationError 5 import datetime 6 7 class Article(models.Model): 8 ''' 文章帖子 ''' 9 title = models.CharField(u'標題', max_length=255) 10 brief = models.CharField(u'簡介', max_length=255, blank=True, null=True) 11 category = models.ForeignKey('Category', verbose_name=u'板塊') 12 content = models.TextField(u'內容') 13 author = models.ForeignKey('UserProfile', verbose_name=u'作者') 14 # auto_now_add 創建生成的時間, auto_now 修改時的時間 15 pub_date = models.DateTimeField(u'發布時間', blank=True, null=True) 16 last_modify = models.DateTimeField(u'修改時間', auto_now=True) 17 priority = models.IntegerField(u'優先級', default=1000) 18 status_choices = ( 19 ('draft', u'草稿'), 20 ('published', u'發布'), 21 ('hidden', u'隱藏'), 22 ) 23 status = models.CharField(u'狀態', max_length=64, choices=status_choices, 24 default='published') 25 # upload_to='uploads' 設置存放圖片路徑,默認存在跟目錄下 26 head_img = models.ImageField(u'圖片', upload_to='uploads') 27 28 def clean(self): 29 if self.status == 'draft' and self.pub_date is not None: 30 raise ValidationError(u'草稿是沒有發布日期的!') 31 # Set the pub_date for published items if it hasn't been set already. 32 if self.status == 'published' and self.pub_date is None: 33 self.pub_date = datetime.date.today() 34 35 def __str__(self): 36 return self.title 37 38 class Meta: 39 verbose_name = u'文章' 40 verbose_name_plural = u'文章' 41 42 class Comment(models.Model): 43 ''' 評論、點贊 ''' 44 article = models.ForeignKey('Article', verbose_name='所屬文章') 45 parent_comment = models.ForeignKey('self', related_name='my_children', 46 blank=True, null=True, verbose_name=u'父評論') 47 comment_choices = ((1, u'評論'), (2, u'點贊'),) 48 comment_type = models.IntegerField(u'類型', choices=comment_choices, 49 default=1) 50 user = models.ForeignKey('UserProfile', verbose_name=u'評論者') 51 content = models.TextField(u'內容', blank=True, null=True) 52 date = models.DateTimeField(u'時間', auto_now_add=True) 53 54 def clean(self): 55 if self.comment_type == 1 and len(self.content) == 0: 56 raise ValidationError(u'評論的內容不能為空!') 57 58 def __str__(self): 59 return self.content 60 61 62 class Meta: 63 verbose_name = u'評論' 64 verbose_name_plural = u'評論' 65 66 class Category(models.Model): 67 ''' 板塊 ''' 68 name = models.CharField(u'板塊名字', max_length=64, unique=True) 69 brief = models.CharField(u'簡介', max_length=255, blank=True, null=True) 70 set_as_top_menu = models.BooleanField(u'頂級菜單', default=False) 71 position_index = models.SmallIntegerField(u'位置索引') 72 admins = models.ManyToManyField('UserProfile', blank=True, 73 verbose_name=u'版主') 74 75 def __str__(self): 76 return self.name 77 78 class Meta: 79 verbose_name = u'板塊' 80 verbose_name_plural = u'板塊' 81 82 class UserProfile(models.Model): 83 ''' 用戶 ''' 84 user = models.OneToOneField(User, verbose_name=u'用戶') 85 name = models.CharField(u'昵稱', max_length=32) 86 signature = models.CharField(u'簽名', max_length=255, blank=True, null=True) 87 head_img = models.ImageField(u'頭像',height_field=150, width_field=150, 88 blank=True, null=True) 89 90 91 def __str__(self): 92 return self.name 93 94 class Meta: 95 verbose_name = u'用戶權限' 96 verbose_name_plural = u'用戶權限'
CSRF(Cross Site Request Forgery, 跨站域請求偽造)
CSRF 背景與介紹
CSRF(Cross Site Request Forgery, 跨站域請求偽造)是一種網絡的攻擊方式,它在 2007 年曾被列為互聯網 20 大安全隱患之一。其他安全隱患,比如 SQL 腳本注入,跨站域腳本攻擊等在近年來已經逐漸為眾人熟知,很多網站也都針對他們進行了防御。然而,對於大多數人來說,CSRF 卻依然是一個陌生的概念。即便是大名鼎鼎的 Gmail, 在 2007 年底也存在着 CSRF 漏洞,從而被黑客攻擊而使 Gmail 的用戶造成巨大的損失。
CSRF 攻擊實例
CSRF 攻擊可以在受害者毫不知情的情況下以受害者名義偽造請求發送給受攻擊站點,從而在並未授權的情況下執行在權限保護之下的操作。比如說,受害者 Bob 在銀行有一筆存款,通過對銀行的網站發送請求 http://bank.example/withdraw?account=bob&amount=1000000&for=bob2 可以使 Bob 把 1000000 的存款轉到 bob2 的賬號下。通常情況下,該請求發送到網站后,服務器會先驗證該請求是否來自一個合法的 session,並且該 session 的用戶 Bob 已經成功登陸。黑客 Mallory 自己在該銀行也有賬戶,他知道上文中的 URL 可以把錢進行轉帳操作。Mallory 可以自己發送一個請求給銀行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory。但是這個請求來自 Mallory 而非 Bob,他不能通過安全認證,因此該請求不會起作用。這時,Mallory 想到使用 CSRF 的攻擊方式,他先自己做一個網站,在網站中放入如下代碼: src=”http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory ”,並且通過廣告等誘使 Bob 來訪問他的網站。當 Bob 訪問該網站時,上述 url 就會從 Bob 的瀏覽器發向銀行,而這個請求會附帶 Bob 瀏覽器中的 cookie 一起發向銀行服務器。大多數情況下,該請求會失敗,因為他要求 Bob 的認證信息。但是,如果 Bob 當時恰巧剛訪問他的銀行后不久,他的瀏覽器與銀行網站之間的 session 尚未過期,瀏覽器的 cookie 之中含有 Bob 的認證信息。這時,悲劇發生了,這個 url 請求就會得到響應,錢將從 Bob 的賬號轉移到 Mallory 的賬號,而 Bob 當時毫不知情。等以后 Bob 發現賬戶錢少了,即使他去銀行查詢日志,他也只能發現確實有一個來自於他本人的合法請求轉移了資金,沒有任何被攻擊的痕跡。而 Mallory 則可以拿到錢后逍遙法外。
CSRF 攻擊的對象
在討論如何抵御 CSRF 之前,先要明確 CSRF 攻擊的對象,也就是要保護的對象。從以上的例子可知,CSRF 攻擊是黑客借助受害者的 cookie 騙取服務器的信任,但是黑客並不能拿到 cookie,也看不到 cookie 的內容。另外,對於服務器返回的結果,由於瀏覽器同源策略的限制,黑客也無法進行解析。因此,黑客無法從返回的結果中得到任何東西,他所能做的就是給服務器發送請求,以執行請求中所描述的命令,在服務器端直接改變數據的值,而非竊取服務器中的數據。所以,我們要保護的對象是那些可以直接產生數據改變的服務,而對於讀取數據的服務,則不需要進行 CSRF 的保護。比如銀行系統中轉賬的請求會直接改變賬戶的金額,會遭到 CSRF 攻擊,需要保護。而查詢余額是對金額的讀取操作,不會改變數據,CSRF 攻擊無法解析服務器返回的結果,無需保護。
防御策略:在請求地址中添加 token 並驗證
CSRF 攻擊之所以能夠成功,是因為黑客可以完全偽造用戶的請求,該請求中所有的用戶驗證信息都是存在於 cookie 中,因此黑客可以在不知道這些驗證信息的情況下直接利用用戶自己的 cookie 來通過安全驗證。要抵御 CSRF,關鍵在於在請求中放入黑客所不能偽造的信息,並且該信息不存在於 cookie 之中。可以在 HTTP 請求中以參數的形式加入一個隨機產生的 token,並在服務器端建立一個攔截器來驗證這個 token,如果請求中沒有 token 或者 token 內容不正確,則認為可能是 CSRF 攻擊而拒絕該請求。
token 可以在用戶登陸后產生並放於 session 之中,然后在每次請求時把 token 從 session 中拿出,與請求中的 token 進行比對,但這種方法的難點在於如何把 token 以參數的形式加入請求。對於 GET 請求,token 將附在請求地址之后,這樣 URL 就變成 http://url?csrftoken=tokenvalue。 而對於 POST 請求來說,要在 form 的最后加上 <input type=”hidden” name=”csrftoken” value=”tokenvalue”/>,這樣就把 token 以參數的形式加入請求了。但是,在一個網站中,可以接受請求的地方非常多,要對於每一個請求都加上 token 是很麻煩的,並且很容易漏掉,通常使用的方法就是在每次頁面加載時,使用 javascript 遍歷整個 dom 樹,對於 dom 中所有的 a 和 form 標簽后加入 token。這樣可以解決大部分的請求,但是對於在頁面加載之后動態生成的 html 代碼,這種方法就沒有作用,還需要程序員在編碼時手動添加 token。
Django 中使用CSRF
1 // using jQuery 2 function getCookie(name) { 3 var cookieValue = null; 4 if (document.cookie && document.cookie !== '') { 5 var cookies = document.cookie.split(';'); 6 for (var i = 0; i < cookies.length; i++) { 7 var cookie = jQuery.trim(cookies[i]); 8 // Does this cookie string begin with the name we want? 9 if (cookie.substring(0, name.length + 1) === (name + '=')) { 10 cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 11 break; 12 } 13 } 14 } 15 return cookieValue; 16} 17 18 19 function csrfSafeMethod(method) { 20 // these HTTP methods do not require CSRF protection 21 return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 22} 23 var csrftoken = getCookie('csrftoken'); 24 $.ajaxSetup({ 25 beforeSend: function(xhr, settings) { 26 if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 27 xhr.setRequestHeader("X-CSRFToken", csrftoken); 28 } 29 } 30 }); 31// end ajax
上傳文件
template form表單
1 <form enctype="multipart/form-data" action="{% url 'new_article' %}" method="post">{% csrf_token %} 2 3 上傳標題圖片:<input type="file" name="head_img" > 4 5 <button type="submit" class="btn btn-success pull-right">提交</button> 6 7 </form>
多級評論
用戶可以直接對貼子進行評論,其它用戶也可以對別的用戶的評論再進行評論,也就是所謂的壘樓,如下圖:
所有的評論都存在一張表中, 評論與評論之前又有從屬關系,如何在前端 頁面上把這種層級關系體現出來?
先把評論簡化成一個這樣的模型:
1 數據庫里評論之前的關系大概如下 2 data = [ 3 ('a',None), 4 ('b', 'a'), 5 ('c', None), 6 ('d', 'a'), 7 ('e', 'a'), 8 ('g', 'b'), 9 ('h', 'g'), 10 ('j', None), 11 ('f', 'j'), 12 ] 13 14 15 ''' 16 完整的層級關系如下: 17 a -> b -> g ->h 18 a -> d 19 a -> e 20 21 ''' 22 23 #轉成字典后的關系如下 24 { 25 'a':{ 26 'b':{ 27 'g':{ 28 'h':{} 29 } 30 }, 31 'd':{}, 32 'e':{} 33 }, 34 'j':{ 35 'f':{} 36 } 37 }
接下來其實直接用遞歸的方法去迭代一遍字典就行啦。
注意, 你不能直接把字典格式返回給前端的template, 前端的template在對這個字典進行的遍歷的時候必須采用遞歸的方法,但是template里沒有遞歸的語法支持, 所以怎么辦呢? 只能用自定義template tag啦。
自定義template tag模塊
1 #!/usr/bin/env python3 2 # -*- coding:utf-8 -*- 3 # Version:Python3.5.0 4 5 from django import template 6 from django.utils.html import format_html 7 8 register = template.Library() 9 10 @register.filter 11 def truncate_url(img_obj): 12 # img_obj是一個圖片對象,img_obj.name或者img_obj.url都是獲取圖片路徑 13 # split('/', maxsplit=1)[-1] 分割一次,去最后的值 14 # print(img_obj.name,img_obj.url) # uploads/1.jpg 15 return img_obj.name.split('/', maxsplit=1)[-1] 16 17 @register.simple_tag 18 def filter_comment(article_obj): 19 ''' 過濾點贊、評論''' 20 # comment_set.select.related() 分組 21 query_set = article_obj.comment_set.select_related() 22 comments = { 23 'comment_count': query_set.filter(comment_type=1).count(), 24 'thumb_count': query_set.filter(comment_type=2).count(), 25 } 26 print(comments) 27 return comments