Django中CSRF原理及應用詳解
Web開發中十分重要的一項內容就是Web安全,在我們進行服務端構建的時候,最常見的幾種Web攻擊無非是下面的這幾種:
1.注入(SQL注入)
2.跨站指令碼攻擊(XSS)
3.跨站請求偽造(CSRF)
4.開放重定向
今天主要就CSRF做一個簡單的總結,結合Python Web框架Django 做一個應用
一、CSRF是什麼
跨站請求偽造(CSRF)與跨站請求指令碼正好相反。跨站請求指令碼的問題在於,客戶端信任伺服器端傳送的資料。跨站請求偽造的問題在於,伺服器信任來自客戶端的資料。
二、無CSRF時存在的隱患
跨站請求偽造是指攻擊者通過HTTP請求江資料傳送到伺服器,從而盜取回話的cookie。盜取回話cookie之後,攻擊者不僅可以獲取使用者的資訊,還可以修改該cookie關聯的賬戶資訊。
三、Form提交(CSRF)
那麼在Django中CSRF驗證大體是一個什麼樣的原理呢?下面通過一個小例子來簡單說明一下:
我們把Django中CSRF中介軟體開啟(在settings.py中)
'django.middleware.csrf.CsrfViewMiddleware'
在app01的views.py中寫一個簡單的後臺
from django.shortcuts import render,redirect,HttpResponse # Create your views here. def login(request): if request.method == "GET": return render(request,'login.html') elif request.method == "POST": user = request.POST.get('user') pwd = request.POST.get('pwd') if user == 'root' and pwd == "123123": #生成隨機字串 #寫到使用者瀏覽器cookie #儲存在服務端session中 #在隨機字串對應的字典中設定相關內容 request.session['username'] = user request.session['islogin'] = True if request.POST.get('rmb',None) == '1': #認為設定超時時間 request.session.set_expiry(10) return redirect('/index/') else: return render(request,'login.html') def index(request): #獲取當前隨機字串 #根據隨機字串獲取對應的資訊 if request.session.get('islogin', None): return render(request,'index.html',{'username':request.session['username']}) else: return HttpResponse('please login ') def logout(request): del request.session['username'] request.session.clear() return redirect('/login/')
在templates中寫一個簡單的登陸建立兩個檔案(login.html,index.html)登陸成功跳轉index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login/" method="post"> <input type="text" name="user" /> <input type="text" name="pwd" /> <input type="checkbox" name="rmb" value="1" /> 10s免登入 <input type="submit" value="提交" /> </form> </body> </html>
這是瀏覽器中的樣式 :
那麼如果這個時候,我們點選登陸提交,django會因為無法通過csrf驗證返回一個403:
而csrf驗證其實是對http請求中一段隨機字串的驗證,那麼這段隨機字串從何而來呢?這個時候我們嘗試把login.html做一個修改新增一句 {% csrf_token %}:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login/" method="post">
{% csrf_token %}
<input type="text" name="user" />
<input type="text" name="pwd" />
<input type="checkbox" name="rmb" value="1" /> 10s免登入
<input type="submit" value="提交" />
</form>
</body>
</html>
這個時候我們再通過瀏覽器元素審查,就會發現一段新的程式碼:
Django在html中建立一個基於input框value值的隨機字串 ,這個時候我們再次輸入後臺設定的賬號密碼,進行提交,會發現提交成功,進入了我們的index.html介面當中:
這就是csrf的基本原理,如果沒有這樣一段隨機字串做驗證,我們只要在另一個站點,寫一個表單,提交到這個地址下,是一樣可以傳送資料的,這樣就造成了極大的安全隱患。而我們新新增的csrf_token就是在我們自己的站點中,設定的隱藏引數,用來進行csrf驗證。
四、Ajax提交 (CSRF)
上面是一個基於form表單提交的小例子,csrf中介軟體要驗證的隨機字串存放在了form表單當中,傳送到了後臺,那麼如果我們使用的是ajax非同步請求,是不是也要傳送這樣一個類似的字串呢?答案是肯定的,但這個字串又該來自哪裡?其實它來自cookie,在上面的那個小例子當中,我們開啟F12元素審查,會發現,在cookie中也存放這樣一個csrftoken:
所以下面呢我們就再聊一下ajax請求中如何進行。
首先我們引入兩個js檔案放在工程專案的static當中,這兩個檔案是jquery的庫檔案,方便我們進行請求操作:
然後我們把之前的login.html做一個修改:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login/" method="post">
{% csrf_token %}
<input type="text" name="user" />
<input type="text" name="pwd" />
<input type="checkbox" name="rmb" value="1" /> 10s免登入
<input type="submit" value="提交" />
<input id="btn" type="button" value="按鈕">
</form>
<script src="/static/jquery-1.12.4.js"></script>
<script src="/static/jquery.cookie.js"></script>
<script>
$(function () {
$('#btn').click(function () {
$.ajax({
url:'/login/',
type:"POST",
data:{'username':'root','pwd':'123123'},
success:function (arg) {
}
})
})
})
</script>
</body>
</html>
這個時候我們開啟介面,點選按鈕,會發現http請求傳送403錯誤,很明顯我們直接傳送請求是不合適的,並沒有帶隨機字串過去:
所以,我們應該先從cookie中獲取到這個隨機字串,這個隨機字串的名字,我們可以通過之前的驗證得出是“csrftoken”:
var csrftoken = $.cookie('csrftoken');
這樣變數csrftoken取到的就是我們的隨機字串;但是如果後臺想要去接收這個隨機字串,也應該需要一個key,那這個key是什麼?我們可以通過查詢配置檔案,通過控制檯輸出的方式驗證這個key:
from django.conf import settings
print(settings.CSRF_HEADER_NAME)
最後輸出的是:
HTTP_X_CSRFTOKEN
但這裡需要注意的是,HTTP_X_CSRFTOKEN並不是請求頭中傳送給django真正拿到的欄位名,前端發過去真正的欄位名是:
X-CSRFtoken
那為什麼從Django的控制檯輸出會得到HTTP_X_CSRFTOKEN呢?其實我們前端的請求頭X-CSRFtoken傳送到後臺之後,django會做一個名字處理,在原來的欄位名前家一個HTTP_,並且將原來的小寫字元變成大寫的,“-”會處理成下劃線“_”,所以會有這兩個欄位的不一樣。但本質上他們指向的都是同一個字串。知道這一點之後,那麼我們前端就可以真正地發起含有CSRF請求頭的資料請求了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login/" method="post">
{% csrf_token %}
<input type="text" name="user" />
<input type="text" name="pwd" />
<input type="checkbox" name="rmb" value="1" /> 10s免登入
<input type="submit" value="提交" />
<input id="btn" type="button" value="按鈕">
</form>
<script src="/static/jquery-1.12.4.js"></script>
<script src="/static/jquery.cookie.js"></script>
<script>
var csrftoken = $.cookie('csrftoken');
$(function () {
$('#btn').click(function () {
$.ajax({
url:'/login/',
type:"POST",
data:{'username':'root','pwd':'123123'},
header:{'X-CSRFtoken':csrftoken},
success:function (arg) {
}
})
})
})
</script>
</body>
</html>
在頁面中點選按鈕之後,會發現請求成功!
那麼這個時候有人會問,難道所有的ajax請求,都需要這樣獲取一次寫進去嗎,會不會很麻煩。針對這一點,jquery的ajax請求中為我們封裝了一個方法:ajaxSetup,它可以為我們所有的ajax請求做一個集體配置,所以我們可以進行如下改造,這樣不管你的ajax請求有多少,都可以很方便地進行csrf驗證了:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login/" method="POST">
{% csrf_token %}
<input type="text" name="user" />
<input type="text" name="pwd" />
<input type="checkbox" name="rmb" value="1" /> 10s免登入
<input type="submit" value="提交" />
<input id="btn" type="button" value="按鈕">
</form>
<script src="/static/jquery-1.12.4.js"></script>
<script src="/static/jquery.cookie.js"></script>
<script>
var csrftoken = $.cookie('csrftoken');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
$(function () {
$('#btn').click(function () {
$.ajax({
url:'/login/',
type:"POST",
data:{'username':'root','pwd':'123123'},
success:function (arg) {
}
})
})
});
</script>
</body>
</html>
四、裝飾器配置
在講完基本的csrf驗證操作之後,我們還有一個可說的地方。在平時的產品需求當中,並不一定所有的介面驗證都需要進行csrf驗證,而我們之前採用的是在settings.py中介軟體配置進行全域性配置,那麼如果遇到了不需要開啟csrf的時候該怎麼辦呢?
from django.views.decorators.csrf import csrf_exempt,csrf_protect
@csrf_protect
def index(request):
.....
@csrf_exempt
def login(request):
.....
@csrf_protect 是 開啟csrf驗證的裝飾器,@csrf_exempt是關閉csrf驗證的裝飾器。
五、前後端分離專案手動將csrftoken寫入cookie的方法
1. 手動設定,在view 中新增(經常失效,建議採用2,3,4種方法,親測有效)
request.META["CSRF_COOKIE_USED"] = True
2. 手動呼叫 csrf 中的 get_token(request) 或 rotate_token(request) 方法。
from django.middleware.csrf import get_token ,rotate_token
def server(request):
# get_token(request) // 兩者選一
# rotate_token(request) // 此方法每次設定新的cookies
return render(request, ‘server.html‘)
3. 在HTML模板中新增 {% csrf_token %}
4. 在需要設定cookie的檢視上加裝飾器 ensure_csrf_cookie()
from django.views.decorators.csrf import ensure_csrf_cookie
@ensure_csrf_cookie
def server(request):
return render(request,'server.html')