一、csrf攻击
1.1 csrf攻击(跨站请求伪造)
【csrf攻击即】:通过第3方网站,伪造请求(前提条件是你已经登录正常网站,并保存了session或cookie登录信息且没有退出),第三方网站即可通过你的session或cookie直接修改正常网站的用户名密码。
- 首先做一个登录页,让用户输入用户名和密码进行登录,登录成功之后跳转的修改密码页面。在修改密码页面输入新密码,点击确认按钮完成密码修改。
- 登录页需要一个模板文件login.html.修改密码页面也需要一个模板文件change_pwd.html.
- 显示登录页的视图login,验证登录的视图login_check,显示发帖页的视图change_pwd,处理修改密码的视图change_pwd_action.
- 加功能:
a)只有用户登录之后才可以进行修改密码操作。 - 登录装饰器函数(app2/views.py):
1
2
3
4
5
6
7
8
9
10
11def login_required(view_func): #参数即调用它的函数
'''登录判断装饰器'''
def wrapper(request, *view_args, **view_kwargs): # 内部函数,包装一下
# 判断用户是否登录
if request.session.has_key('islogin'): #如果登录了,就返回真正页面(调用此装饰器函数的函数)
# 用户已登录,调用对应的视图
return view_func(request, *view_args, **view_kwargs)
else:
# 否则,用户未登录,则跳转到登录页
return redirect('/login')
return wrapper - 登录装饰器调用,比如一个页面必须要登录才能操作,否则跳转到登录页
【app2/views.py】@login_required
1
2
3
4
5# /change_pwd
def change_pwd(request):
'''显示修改密码页面'''
return render(request, 'booktest/change_pwd.html')【csrf攻击改密码实例】(必须登录后才能操作):
1) project2/settings.py 注释掉csrf
1
2
3
4
5
6
7
8
9MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]2)templates/change_pwd.html
1
2
3
4
5
6
7
8
9
10
11
12
13<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>修改密码页面</title>
</head>
<body>
<form method="post" action="/change_pwd_action/">
新密码:<input type="password" name="pwd">
<input type="submit" value="确认修改">
</form>
</body>
</html>3) app2/views.py【登录装饰器】
【1】登录装饰器:用户已登录,调用对应的视图;用户未登录,跳转到登录页
【2】需要登录才能操作的页面调用登录装饰器
【3】需要登录才能操作的页面调用登录装饰器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90from django.shortcuts import render,redirect
from django.template import loader,RequestContext
from django.http import HttpResponse,JsonResponse
from app2.models import BookInfo
def login(request):
'''登录页'''
# 判断用户是否登录,用户已登录, 直接跳转到图书列表
if request.session.has_key('islogin'):
return redirect('/change_pwd/')
else:
#如果用户名密码已经在cookie中,则取到它,并做为参数返回给渲染页面
if 'username' in request.COOKIES:
#获取cookie中的用户名、密码
username=request.COOKIES['username']
#password=request.COOKIES['password']
else:
username=''
password=''
return render(request,'app2/login.html',{'username':username,'password':password})
def login_check(request):
'''登录校验'''
#1.获取用户名密码
username=request.POST.get('username')
password=request.POST.get('password')
remember=request.POST.get('remember') #接收remeber
#2.进行校验,并返回json数据
if username=='jim' and password=='123':
#return redirect('/books')
response = JsonResponse({'res':1}) #正确返回1,重写
#如果remember==on,则把用户名,密码设置cookie到cookie
if remember=='on':
#response.set_cookie('username',username,max_age=7*24*3600)
#response.set_cookie('password',password,max_age=7*24*3600)
# 记住用户登录状态把用户名设置到session
request.session['username'] = username
# 返回应答
# 如果用户勾选了remember的条件下,设置session,记住用户登录状态
request.session['islogin'] = True # 只有session中有islogin,就认为用户已登录
return response #不要忘记返回response
else:
#return redirect('/login')
return JsonResponse({'res':0}) #错误返回0
#【1】登录装饰器:用户已登录,调用对应的视图;用户未登录,跳转到登录页
def login_required(view_func):
'''登录判断装饰器'''
def wrapper(request, *view_args, **view_kwargs):
# 判断用户是否登录
if request.session.has_key('islogin'):
# 用户已登录,调用对应的视图
return view_func(request, *view_args, **view_kwargs)
else:
# 用户未登录,跳转到登录页
return redirect('/login')
return wrapper
# /change_pwd
def change_pwd(request):
'''显示修改密码页面'''
# # 进行用户是否登录的判断
# if not request.session.has_key('islogin'):
# # 用户未登录,跳转到登录
# return redirect('/login')
return render(request, 'app2/change_pwd.html')
# /change_pwd_action
def change_pwd_action(request):
'''模拟修改密码处理'''
# # 进行用户是否登录的判断
# if not request.session.has_key('islogin'):
# # 用户未登录,跳转到登录
# return redirect('/login')
# 1.获取新密码
pwd = request.POST.get('pwd')
# 获取用户名
username = request.session.get('username')
# 2.实际开发的时候: 修改对应数据库中的内容...
# 3.返回一个应答
return HttpResponse('%s修改密码为:%s'%(username,pwd))4)app2/urls.py
1
2
3
4
5path('login/',views.login),#登录页
path('login_check',views.login_check),#登录检测
path('change_pwd/', views.change_pwd), # 修改密码页面显示
path('change_pwd_action/', views.change_pwd_action), # 修改密码处理5 ) templates/login.html(ajax登录技术)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<!-- 【0】引入jquery -->
<script src="/static/js/jquery-1.12.4.min.js"></script>
<title>登录页面</title>
</head>
<script>
// 写ajax处理函数
$(function () {
$('#btnLogin').click(function () {
//1.获取用户名、密码、是否记住用户名
username=$('#username').val()
password=$('#password').val()
remember=$('#remember').val() //【2】是否记住用户名
//2.发起ajax--post(username,password)请求验证,地址:/login_check
$.ajax({
'url':'/login_check',//验证地址
'type':'post',//请求类型
'data':{'username':username,'password':password,'remember':remember},//【3】发送数据,加上remember
'dataType':'json',//希望返回数据类型
}).success(function(data){
//成功返回{'res':1},失败{'res':0}
if(data.res===0){
$('#msg').show().html('用户名或密码错误请重试!')//登录失败则显示msg,并在里写入信息
}else{//成功跳转到books页面
location.href='/change_pwd'
}
})
})
})
</script>
<style>
/* 信息提示样式 */
#msg{
display: none;
color:red;
}
</style>
<body>
<!-- 原form删除,input的name变id,方便jquery操作 -->
<!-- 【4】把views页的login()函数传过来的用户名,密码赋值给对应处 -->
用户名:<input type="text" id="username" value="{{username}}"><br/>
密码:<input type="password" id="password" value="{{password}}"><br/>
<!-- 加入一个信息提示框,用于密码等错误提示 -->
<div id="msg"></div>
<!-- 【1】记住用户名,设置cookie用,如果勾选则其value=on -->
<input type="checkbox" id="remember">记住用户名<br/>
<!-- 按钮type改button,加一个id方便jquery操作 -->
<input type="button" id="btnLogin" value="登录">
</body>
</html>6)效果:http://127.0.0.1:8000/login/
用户名或密码错误:
不登录直接访问修改密码页面:http://127.0.0.1:8000/change_pwd 跳回登录页
u/p正确(jim,123)跳转到:http://127.0.0.1:8000/change_pwd
然后提示:jim修改密码为:4567)通过第3方网站修改密码具体操作过程如下:
- 首先查看访问服务器的局域网IP及公网IP
1
2
3
4
5ipconfig /all
找到:
192.168.1.4
公网直接百度IP,查看即可 - 创建服务,以下操作2选1
1
2
3
4
5如果在局域网创建服务:
py manage.py runserver 192.168.1.4:8000
如果在【虚拟机】或【真正服务器】创建服务:
py manage.py runserver 公网IP:8000 - 配置settings.py
1
2DEBUG = False #True
ALLOWED_HOSTS = ['*'] - 在局域网另一台电脑访问:192.168.1.4:8000/login 即可访问网站,输入(jim,123)登录成功。
- 【创建伪造页】在访问电脑自建一个页面如下:
【csrf-test.html】1
2
3
4
5
6
7
8
9
10
11
12
13
<html lang="en">
<head>
<meta charset="UTF-8">
<title>修改密码页面</title>
</head>
<body>
<form method="post" action="http://192.168.1.4:8000/change_pwd_action/">
<input type="hidden" name="pwd" value='789'><!--【1】用了隐藏input,页面只能看到一个按钮,并给新密码定为789-->
<input type="submit" value="点我有惊喜!">
</form>
</body>
</html> - 打开第5步的页面,点按键即可修改成功对应网站密码:
- 跨站伪造post请求修改用户密码成功!
1
jim修改密码为:789
1.1.1 django防止csrf的方式:
1) 默认打开csrf中间件(project2/settings.py)。
【1】django默认打开csrf防护,它只对post提交有效
1 | MIDDLEWARE = [ |
2) 表单post提交数据时加上{ % csrf_token % }
标签
(对应模板本例:change_pwd.html)。
【注意】:需要有post页面提交的页面必须加上{ % csrf_token % }
,否则即使本站页面也会访问失败。
1 | <!DOCTYPE html> |
3)templates/app2/login.html是Ajax页,所以不能用普通写法
【参考】:https://code.ziqiangxuetang.com/django/django-csrf.html?bd_source_light=4317393
- 在视图中使用 render(而不要使用 render_to_response)
- 在ajax中加:
1
2
3$.ajaxPost({
data: {csrfmiddlewaretoken: '{{ csrf_token }}' },
}); - 详细代码:【4】csrf防护
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<!-- 【0】引入jquery -->
<script src="/static/js/jquery-1.12.4.min.js"></script>
<title>登录页面</title>
</head>
<script>
// 写ajax处理函数
$(function () {
$('#btnLogin').click(function () {
//1.获取用户名、密码、是否记住用户名
username=$('#username').val()
password=$('#password').val()
remember=$('#remember').val() //【2】是否记住用户名
//2.发起ajax--post(username,password)请求验证,地址:/login_check
$.ajax({
'url':'/login_check',//验证地址
'type':'post',//请求类型
'data':{//【3】发送数据,加上remember
'username':username,
'password':password,
'remember':remember,
csrfmiddlewaretoken: '{{ csrf_token }}',//【4】csrf防护
},
'dataType':'json',//希望返回数据类型
}).success(function(data){
//成功返回{'res':1},失败{'res':0}
if(data.res===0){
$('#msg').show().html('用户名或密码错误请重试!')//登录失败则显示msg,并在里写入信息
}else{//成功跳转到books页面
location.href='/change_pwd'
}
})
})
})
</script>
<style>
/* 信息提示样式 */
#msg{
display: none;
color:red;
}
</style>
<body>
<!-- 原form删除,input的name变id,方便jquery操作 -->
<!-- 【4】把views页的login()函数传过来的用户名,密码赋值给对应处 -->
用户名:<input type="text" id="username" value="{{username}}"><br/>
密码:<input type="password" id="password" value="{{password}}"><br/>
<!-- 加入一个信息提示框,用于密码等错误提示 -->
<div id="msg"></div>
<!-- 【1】记住用户名,设置cookie用,如果勾选则其value=on -->
<input type="checkbox" id="remember">记住用户名<br/>
<!-- 按钮type改button,加一个id方便jquery操作 -->
<input type="button" id="btnLogin" value="登录">
</body>
</html>
再跨站修改(7.6):
1 | <p style="background-color:rgb(255, 255, 204)"> |
1.1.2 防御原理:
1) 渲染模板文件时在页面生成一个名字叫做csrfmiddlewaretoken的隐藏域。
在网站右键查看源码,可看到这个隐藏域:name=csrfmiddlewaretoken 的input
1 | <form method="post" action="/change_pwd_action/"> |
把上例的隐藏域的value传给服务器。
1 | <input type="hidden" name="csrfmiddlewaretoken" value="O6lfnrybooqSP9Je0bZyCOr8qGObvK0jgzV7fMUW259X117OkpAm8OjtCsadu9tk"> |
2) 服务器同时交给浏览器保存一个名字为csrftoken的cookie信息。
3) 提交表单时,两个值都会发给服务器,服务器进行比对,如果一样,则csrf验证通过,否则失败。
二、验证码
- 在用户注册、登录页面,为了防止暴力请求(穷举法破解用户名密码、自动注册大量账号等),可以加入验证码功能,如果验证码错误,则不需要继续处理,可以减轻业务服务器、数据库服务器的压力。
- 【其它生成验证码参考】:https://blog.csdn.net/ding_312/article/details/82258442
1)生成验证码函数 app2/views.py
【1】存入session,用于做进一步验证
【2】将图片保存在内存中,文件类型为png
【3】将内存中的图片数据返回给客户端,MIME类型为图片png1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47from PIL import Image, ImageDraw, ImageFont
import io
# /verify_code
def verify_code(request):
# 引入随机函数模块
import random
# 定义变量,用于画面的背景色、宽、高 RGB
bgcolor = (random.randrange(20, 100), random.randrange(20, 100), 255)
width = 100
height = 25
# 创建画面对象
im = Image.new('RGB', (width, height), bgcolor)
# 创建画笔对象
draw = ImageDraw.Draw(im)
# 调用画笔的point()函数绘制噪点
for i in range(0, 100):
xy = (random.randrange(0, width), random.randrange(0, height))
fill = (random.randrange(0, 255), 255, random.randrange(0, 255))
draw.point(xy, fill=fill)
# 定义验证码的备选值
str1 = 'ABCD123EFGHIJK456LMNOPQRS789TUVWXYZ0' #qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789
# 随机选取4个值作为验证码
rand_str = ''
for i in range(0, 4):
rand_str += str1[random.randrange(0, len(str1))]
# 构造字体对象,ubuntu的字体路径为“/usr/share/fonts/truetype/freefont”
font = ImageFont.truetype('AdobeFanHeitiStd-Bold.otf', 23)
# 构造字体颜色
fontcolor = (255, random.randrange(0, 255), random.randrange(0, 255))
# 绘制4个字
draw.text((5, 2), rand_str[0], font=font, fill=fontcolor)
draw.text((25, 2), rand_str[1], font=font, fill=fontcolor)
draw.text((50, 2), rand_str[2], font=font, fill=fontcolor)
draw.text((75, 2), rand_str[3], font=font, fill=fontcolor)
# 释放画笔
del draw
# 【1】存入session,用于做进一步验证
request.session['verifycode'] = rand_str
# 内存文件操作
buf = io.BytesIO()
# 【2】将图片保存在内存中,文件类型为png
im.save(buf, 'png')
# 【3】将内存中的图片数据返回给客户端,MIME类型为图片png
return HttpResponse(buf.getvalue(), 'image/png')2)app2/urls.py
1
path('verify_code',views.verify_code),#生成验证码
3)效果:http://127.0.0.1:8000/verify_code
4)在templates/app2/login.html加验证码(这次用表单提交)
【1】添加验证码,直接把验证码地址写在src内1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form method="post" action="/login_check">
{% csrf_token %}
用户名:<input type="text" name="username" value="{{ username }}"><br/>
密码:<input type="password" name="password"><br/>
<input type="checkbox" name="remember">记住用户名<br/>
<!--【1】添加验证码,直接把地址写在src内-->
<img src="/verify_code"><br/>
<input type="text" name="vcode">输入验证码<br/>
<input type="submit" value="登录">
</form>
</body>
</html>5)app2/views.py验证函数修改
【1】获取用户输入验证码
【2】获取session中保存的验证码(在第1步)
【3】进行验证码校验,如果验证码输入不对直接返回登录页面,下面的密码验证就不再进行操作了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50def login(request):
'''登录页'''
# 判断用户是否登录,用户已登录, 直接跳转到图书列表
if request.session.has_key('islogin'):
return redirect('/change_pwd/')
else:
#如果用户名密码已经在cookie中,则取到它,并做为参数返回给渲染页面
if 'username' in request.COOKIES:
#获取cookie中的用户名、密码
username=request.COOKIES['username']
#password=request.COOKIES['password']
else:
username=''
#password=''
return render(request,'app2/login.html',{'username':username}) #,'password':password
def login_check(request):
'''登录校验'''
#1.获取用户名密码
username=request.POST.get('username')
password=request.POST.get('password')
remember=request.POST.get('remember') #接收remeber
vcode1 = request.POST.get('vcode') # 【1】获取用户输入验证码
vcode2 = request.session.get('verifycode') # 【2】获取session中保存的验证码
# 【3】进行验证码校验,如果验证码输入不对直接返回登录页面,下面的密码验证就不再进行操作了
if vcode1 != vcode2:
# 验证码错误
return redirect('/login')
#2.进行校验,并返回json数据
if username=='jim' and password=='123':
#return redirect('/books')
response = JsonResponse({'res':1,'msg':'login success!'}) #密码正确,登录成功,返回1
#如果remember==on,则把用户名,密码设置cookie到cookie
if remember=='on':
response.set_cookie('username',username,max_age=7*24*3600)
#response.set_cookie('password',password,max_age=7*24*3600)
# 记住用户登录状态把用户名设置到session
request.session['username'] = username
# 返回应答
# 如果用户勾选了remember的条件下,设置session,记住用户登录状态
request.session['islogin'] = True # 只有session中有islogin,就认为用户已登录
return response #不要忘记返回response
else:
#return redirect('/login')
return JsonResponse({'res':0,'msg':'login faild'}) #密码错误返回06)效果:http://127.0.0.1:8000/login/
- 用户名、密码、验证码、都正确返回:1 –登录成功
- 有一个错误返回 :0– 登录失败
三、反向解析
- 当某一个url配置的地址发生变化时,页面上使用反向解析生成地址的位置不需要发生变化。
- 根据url 正则表达式的配置动态的生成url。
- 在项目urls中包含具体应用的urls文件时指定namespace;
1)project2/urls.py配置namespace【重点1】
1-1)Django2.0之后写法
【参考】https://blog.csdn.net/weixin_43883625/article/details/100545439
【1】配置namespace,注意写法:re_path(r'^',include(('app2.urls','booktest'),namespace="booktest"))
1 | from django.contrib import admin |
1-2)Django2.0之前写法:project2/urls.py配置namespace
【1】2.0之前写法:url(r'^', include('app2.urls', namespace='booktest')),
1 | """ |
2)app2/urls.py配置 【重点2】
【1】 配置name反向解析配置path('index2/', views.index, name='index')
1 | from django.contrib import admin |
3)app2/views.py写url_reverse函数
1 | # /url_reverse |
4)模板引用templates/app2/url_reverse.html【重点3】
【1】反向解析模板写法 <a href="{ % url 'booktest:index' % }">反向解析</a>
1 | <!DOCTYPE html> |
5)效果:http://127.0.0.1:8000/url_reverse/
写死地址 http://127.0.0.1:8000/index
反向解析 http://127.0.0.1:8000/index
- 当2步的
path('index/', views.index, name='index')
变path('index2/', views.index, name='index')
时。 - 写死地址的连接不会变,反向解析会自动变为:http://127.0.0.1:8000/index2
3.2 反向解析+动态传参
1)app2/views.py
1 | from django.shortcuts import render,redirect |
2)【重点1:反向解析+传参配置url】app2/urls.py
【1】反向解析+分组传参数
【2】反向解析+字典传参数(以下两种写法都可)
1 | from django.contrib import admin |
3)【重点2:反向解析+传参+模板写法】templates/app2/url_reverse.html
【1】反向解析+分组传参:href="{ % url 'booktest:show_args' 1 2 % }"
【2】反向解析+字典传参:href="{ % url 'booktest:show_kwargs' c=3 d=4 % }"
1 | <!DOCTYPE html> |
4)效果:http://127.0.0.1:8000/url_reverse/
【1】写死+分组传参:/show_args/1/2
【2】反向解析+分组传参:/show_args/1/2
【3】写死+字典传参:/show_kwargs/3/4
【4】反向解析+字典传参:/show_kwargs/3/4
- 当2)步的【show_args】变【show_args2】时,【2】【4】都可正确更新链接,【1】【2】不行
1
2
3
4
5
6
7urlpatterns = [
re_path(r'^show_args2/(\d+)/(\d+)', views.show_args,name='show_args'),#【1】反向解析+分组传参数
#【2】反向解析+字典传参数(以下两种写法都可)
#path(r'show_kwargs/<str:c>/<str:d>',views.show_kwargs,name='show_kwargs'),
path(r'show_kwargs2/<c>/<d>',views.show_kwargs,name='show_kwargs'),
]3.3 views函数里调用反向解析+传参
1)app2/views.py【重点1】
redirect('/index')
reverse('booktest:index')
reverse('booktest:show_args', args=(1,2))
reverse('booktest:show_kwargs', kwargs={'c':3, 'd':4})
- 记得返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# from django.core.urlresolvers import reverse #【0-1】2.0之前导入reverse
from django.urls import reverse #【0-2】2.0之后导入reverse
# /test_redirect
def test_redirect(request):
# 【0写死】重定向到/index
# return redirect('/index')
# 【1反向解析】
# url = reverse('booktest:index')
# 【2反向解析-分组传参】重定向到/show_args/1/2
url = reverse('booktest:show_args', args=(1,2))
# 【3反向解析-字典(关键字)传参】重定向到/show_kwargs/3/4
#url = reverse('booktest:show_kwargs', kwargs={'c':3, 'd':4})
return redirect(url)2)app2/urls.py
1
path('test_redirect/',views.test_redirect),
3)其它配置见3.2(重点2)
效果:把1)步的【0-3】多选1解除注释查看效果
访问:http://127.0.0.1:8000/test_redirect 会动态反向解析到对应的网址。3.2切换也没办法