跨域请求伪造
一、csrf跨站请求伪造详解
- CSRF(Cross-Site Request Forgery)跨站请求伪造是一种常见的网络攻击方式。
- 攻击者通过诱导受害者访问恶意网站或点击恶意链接
- 将恶意请求发送到目标网站上
- 利用受害者在目标网站中已登录的身份来执行某些操作
- 从而达到攻击的目的。
- 举个例子
- 假设受害者在一家网银网站上登录账户,然后继续浏览其他网页。
- 同时,攻击者通过电子邮件等方式向受害者发送了一封包含恶意链接的邮件。
- 当受害者点击该链接时,潜在的威胁就会变得非常现实。
- 该链接指向一个由攻击者操纵的网站,该网站上的恶意代码会自动向网银网站发送一个请求,请求转账到攻击者的账户。
- 由于受害者在网银网站中已经登录,所以该请求会被认为是合法的,这样攻击者就可以成功地进行转账操作。
- 要保护自己免受CSRF攻击,网站开发者可以采取以下措施:
- 使用CSRF令牌:
- 在用户的请求中添加随机生成的令牌,并将该令牌保存在用户会话中。
- 每次提交请求时都会验证该令牌,以确保请求是合法的。
- 启用SameSite属性:
- 将Cookie的SameSite属性设置为Strict或Lax,以限制跨站请求。
- 这可以在一定程度上缓解CSRF攻击。
- 严格验证请求来源:
- 服务器端可以验证请求的来源是否为预期的网站域名
- 例如检查HTTP Referer头部。
- 使用验证码:
- 在敏感操作(如转账、更改密码等)上使用验证码
- 增加用户身份验证的防护。
- 使用CSRF令牌:
二、csrf跨域请求伪造
- 钓鱼网站
- 搭建一个类似正规网站的页面
- 用户点击网站链接,给某个用户打钱
- 打钱的操作确确实实提交给了中国银行的系统,用户的钱也确实减少
- 但是唯一不同的是,账户打钱的账户不是用户想要打钱的目标账户,变成了其他用户
- 内部本质
- 在钓鱼网站的页面针对对方账户,只给用户提供一个没有name属性的普通input框
- 然后在内部隐藏一个已经写好带有name属性的input框
- 如何避免上面的问题
- csrf跨域请求伪造校验
- 网站在给用户返回一个具有提交数据功能的页面的时候会给这个页面加一个唯一标识
- 当这个页面后端发送post请求的时候,我们后端会先校验唯一标识
- 如果成功则正常执行
- 如果唯一标识不符合则拒绝连接(403 forbidden)
- csrf跨域请求伪造校验
1、正常服务端
前端
html
<h1>这是正规的网站</h1>
<form action="" method="post">
<p>当前账户 :>>>> <input type="text" name="start_user"></p>
<p>目标账户 :>>>> <input type="text" name="end_user"></p>
<p>转账金额 :>>>> <input type="text" name="money"></p>
<input type="submit">
</form>
后端
html
def transform_normal(request):
if request.method == "POST":
user_start = request.POST.get("start_user")
user_end = request.POST.get("end_user")
money = request.POST.get("money")
return HttpResponse(f"当前账户 :>>> {user_start} 向目标用户 :>>> {user_end} 转账了 :>>> {money}")
return render(request, 'transform_normal.html')
2、钓鱼服务端
html
<h1>这是钓鱼的网站</h1>
<form action="http://127.0.0.1:8000/transform_normal/" method="post">
<p>当前账户 :>>>> <input type="text" name="start_user" ></p>
<p>目标账户 :>>>> <input type="text"></p>
<p><input type="text" name="end_user" value="Hopes" style="display: none"></p>
<p>转账金额 :>>>> <input type="text" name="money"></p>
<input type="submit">
</form>
html
def transform_normal(request):
if request.method == "POST":
user_start = request.POST.get("start_user")
user_end = request.POST.get("end_user")
money = request.POST.get("money")
return HttpResponse(f"当前账户 :>>> {user_start} 向目标用户 :>>> {user_end} 转账了 :>>> {money}")
return render(request, 'transform_normal.html')
三、csrf校验
介绍
- csrf校验是一种用于防止跨站请求伪造(Cross-Site Request Forgery)攻击的安全措施
form表单中进行csrf校验
添加CSRF Token字段:
在form表单中添加一个隐藏字段,用于存储CSRF Token的值。
后端服务器在渲染表单时生成一个CSRF Token,并将其存储在会话中或者以其他方式关联到当前用户。
当用户提交表单时,前端将CSRF Token的值包含在请求中。
后端在验证表单数据时,检查请求中的CSRF Token是否与存储的Token匹配,如果不匹配,则拒绝请求。
设置Cookie:
后端服务器在渲染表单时,在客户端设置一个包含随机生成的CSRF Token的Cookie。
当用户提交表单时,表单数据会被一同发送到服务器,并自动包含该Cookie。
后端在验证表单数据时,检查请求中的CSRF Token是否与Cookie中的值匹配,如果不匹配,则拒绝请求。
双重Cookie校验:
后端服务器在渲染表单时,在Cookie中设置一个随机生成的CSRF Token,并将其存储在会话中或以其他方式关联到当前用户。
当用户提交表单时,表单数据会被一同发送到服务器,请求头或请求参数中携带一个包含CSRF Token的自定义字段。
后端在验证表单数据时,同时检查请求中的CSRF Token和Cookie中的值是否匹配,如果不匹配,则拒绝请求。
1、form表单如何校验
在form表单上面加上csrf_token
html
<form action="" method="post">
{% csrf_token %}
<p>username:<input type="text" name="username"></p>
<p>transfer_user<input type="password" name="password"></p>
<p>money<input type="text" name="money"></p>
<input type="submit">
</form>
在页面标签中会自动出现一个标签
html
<input type="hidden" name="csrfmiddlewaretoken" value="zQaNPZsy1tVmLdqC7GIDOOOfR7yT9YfO58lJ5yrjZfTw2edZTrVYUllOVMnkwXKe">
2、ajax如何校验
-
方式一
-
利用标签查找获取页面上的随机字符串
-
键必须叫
csrfmiddlewaretoken
-
html
<button id="b1">ajax请求提交</button>
<script>
$("#b1").click(function () {
$.ajax({
url: '',
type: 'post',
// (1) 利用标签查找获取页面上的随机字符串
data: {"username": "dream",
"csrfmiddlewaretoken":$('[csrfmiddlewaretoken]').val()},
success: function () {
}
})
})
</script>
-
方式二
- 利用模板语法进行快捷引入
html
<button id="b1">ajax请求提交</button>
<script>
$("#b1").click(function () {
$.ajax({
url: '',
type: 'post',
// (2) 利用模板语法提供的快捷书写
data: {"username": "dream",
"csrfmiddlewaretoken": "{{ csrf_token }}"},
success: function () {
}
})
})
</script>
-
方式三
-
定义一个js文件并引入
-
导入该配置文件之前,需要先导入jQuery,因为这个配置文件内的内容是基于jQuery来实现的
-
javascript
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('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);
}
}
});
javascript
<button id="b1">ajax请求提交</button>
<script>
$("#b1").click(function () {
$.ajax({
url: '',
type: 'post',
// (3) 定义外部js文件并引入到本地
data: {"username": "dream"},
success: function () {
}
})
})
</script>
模块importlib
引入
1、介绍
-
importlib 模块是 Python 中用于动态加载和导入模块的内置模块。
-
它提供了一组函数和类,使得我们可以在运行时根据需要加载模块,并且可以对已导入的模块进行操作和管理。
2、建包
- scripts/my_package/my_func.py
python
name = 'Dream'
- scripts/test.py
python
from my_package import my_func
print(my_func.name) # Dream
3、不使用导入语句
python
import importlib
module_name = 'my_package.my_func'
model_f = importlib.import_module(module_name)
print(model_f)
print(model_f.name)
详解及示例
1、动态加载模块
-
最小单位,只能到模块名
-
importlib 模块提供了
import_module
函数 -
通过该函数可以在运行时动态加载一个模块。
-
示例代码如下:
python
# 导入模块 : 动态加载一个模块
import importlib
# 定义模块的名字
module_name = 'math'
# 动态导入模块
math_module = importlib.import_module(module_name)
2、导入指定模块成员
-
importlib 模块还提供了
import_module
函数的变体import_module
-
它可以直接返回指定模块的成员。
-
示例代码如下:
python
# 导入模块 : 动态加载一个模块
import importlib
# 定义模块的名字
module_name = 'math'
# 动态导入模块
math_module = importlib.import_module(module_name)
# 从模块中获取相关的值
sqrt_func = getattr(math_module, 'sqrt')
print(sqrt_func) # <built-in function sqrt>
3、重新加载模块
-
在开发过程中,我们有时需要重新加载一个已经导入的模块,以便应用最新的修改。
-
importlib 模块提供了
reload
函数来实现这个功能。 -
示例代码如下:
python
import importlib
# 要重新加载的模块名
module_name = 'my_module'
# 加载模块
my_module = importlib.import_module(module_name)
# 重新加载模块
my_module = importlib.reload(my_module)
4、获取已导入的模块列表
-
通过
sys.modules
可以获取当前已导入的所有模块的字典 -
其中键为模块名称,值为模块对象。
-
以下示例演示如何遍历已导入的模块列表:
python
import sys
for module_name, module in sys.modules.items():
print(module_name, module)
群发功能演示
1、引入
- 我们在Django的配置文件中,里面的中间件配置文件,虽然使用逗号分开,但是可以做到直接引入某个模块
python
MIDDLEWARE = [
'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',
]
- 这种路径构造方式,我们就可以通过importlib模块实现
2、推导过程
(1)引入
定义一个包
python
# -*-coding: Utf-8 -*-
# @File : send_message .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18
def wechat(content):
print(f"wechat接收到的通知:>>>{content}")
def QQ(content):
print(f"QQ接收到的通知:>>>{content}")
def email(content):
print(f"email接收到的通知:>>>{content}")
启动文件中启动包
python
# -*-coding: Utf-8 -*-
# @File : start .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18
from send_message import *
def send_all(content):
wechat(content=content)
QQ(content=content)
email(content=content)
if __name__ == '__main__':
wechat接收到的通知:>>>这是一条测试信息
QQ接收到的通知:>>>这是一条测试信息
email接收到的通知:>>>这是一条测试信息
(2)升级
(1)功能部分
-
先分别创建不同的消息功能文件
-
在同一个文件夹下
-
创建三个功能文件
-
python
# -*-coding: Utf-8 -*-
# @File : WeChat .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18
class WeChat(object):
def __init__(self):
# 发送消息前的准备工作
# 比如掉接口/初始化配置等
pass
def send(self, content):
print(f"WeChat 发送的消息 :>>>{content}")
python
# -*-coding: Utf-8 -*-
# @File : QQ .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18
class QQ(object):
def __init__(self):
# 发送消息前的准备工作
# 比如掉接口/初始化配置等
pass
def send(self, content):
print(f"QQ 发送的消息 :>>>{content}")
python
# -*-coding: Utf-8 -*-
# @File : email .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18
class email(object):
def __init__(self):
# 发送消息前的准备工作
# 比如掉接口/初始化配置等
pass
def send(self, content):
print(f"email 发送的消息 :>>>{content}")
- 在上面的文件内创建初始化文件
python
# -*-coding: Utf-8 -*-
# @File : __init__ .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18
import settings
import importlib
def send_all(content):
# 拿到每一个包的路径
for path_str in settings.MODEL_LIST:
model_path, class_name = path_str.rsplit('.', maxsplit=1)
# model_path : model.email
# class_name : email
# (1)利用字符串导入模块
# models : 模块对象
models = importlib.import_module(model_path)
# (2)利用反射拿到类名
cls = getattr(models, class_name)
# (3)生成类的对象
obj = cls()
# (4)利用鸭子类型直接调用send发送消息
obj.send(content)
if __name__ == '__main__':
send_all('1')
(3)调用部分
- 在外部定义一个配置文件
python
# -*-coding: Utf-8 -*-
# @File : settings .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18
MODEL_LIST = [
'model.email.email',
'model.QQ.QQ',
'model.WeChat.WeChat',
]
- 在外部的真正功能文件
python
# -*-coding: Utf-8 -*-
# @File : start .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/18
import model
model.send_all('这是测试消息')
email 发送的消息 :>>>这是测试消息
QQ 发送的消息 :>>>这是测试消息
WeChat 发送的消息 :>>>这是测试消息
(4)小结
-
遵从Python中的鸭子类型
- 可以在功能文件中自定义功能文件添加或者注释
-
在
settings.py
文件中相关的路径注释掉或添加上去即可