1-跨域、CORS、CSRF

before

参考或者摘自:

跨域,不可不知的基础概念什么是同源策略?前端网络安全必修 1 同源策略和CSRF浅谈CSRF攻击方式前端跨域系列(2)- CSRF(跨站请求伪造)介绍CORS 和 CSRF的区别CSRF(跨站请求伪造)学习与理解

无论前后端开发,都绕不开跨域这个问题,要想弄明白,这一切都要从同源策略开始说起.......

同源策略

首先,为什么会存在跨域这个问题?因为存在浏览器同源策略,所以才有了跨域问题。那浏览器是出于何种原因会有跨域的限制呢。其实不难想到,跨域限制主要的目的就是为了用户的上网安全

没有同源策略限制的危险场景

如果没有 DOM 同源策略,也就是说不同域的 iframe 之间可以相互访问,那么黑客可以这样进行攻击:

  1. 做一个假网站,里面用 iframe 嵌套一个银行网站 http://mybank.com
  2. 把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
  3. 这时如果用户输入账号密码,我们的主网站可以跨域访问到 http://mybank.com 的 dom 节点,就可以拿到用户的账户密码了。

如果没有 XMLHttpRequest 同源策略,那么黑客可以进行 CSRF(跨站请求伪造) 攻击:

  1. 用户登录了自己的银行页面 http://mybank.comhttp://mybank.com 向用户的 cookie 中添加用户标识。
  2. 用户浏览了恶意页面 http://evil.com,执行了页面中的恶意 AJAX 请求代码。
  3. http://evil.comhttp://mybank.com 发起 AJAX HTTP 请求,请求会默认把 http://mybank.com 对应 cookie 也同时发送过去。
  4. 银行页面从发送的 cookie 中提取用户标识,验证用户无误,response 中返回请求数据。此时数据就泄露了。
  5. 而且由于 Ajax 在后台执行,用户无法感知这一过程。

因此,有了浏览器同源策略,我们才能更安全的上网。

浏览器的同源策略

浏览器的同源策略Origin

Origin

Web内容的源由用于访问它的URL 的方案(协议Protocol),主机(域名Host)和端口(Port)定义。只有当方案,主机和端口都匹配时,两个对象具有相同的起源。

某些操作仅限于同源内容,而可以使用 CORS 解除这个限制。

同源策略(Same Origin Policy,SOP)是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。浏览器的同源策略,出于防范跨站脚本的攻击,禁止客户端脚本(如 Java)对不同域的服务进行跨站调用(通常指使用请求)。它能帮助阻隔恶意文档,减少可能被攻击的媒介。这是一个用于隔离潜在恶意文件的关键的安全机制。同源策略机制是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略。则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础上的,浏览器只是针对同源策略的一种实现。

同源策略是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响,比如源a的js不能读取或设置引入的源b的元素属性。

同源的定义

如果两个 URL 端口、域名、协议 都相同,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。

下表给出了关于同源进行对比的示例:

URL 结果 原因
http://store.company.com/dir2/other.html
http://store.company.com/dir/inner/another.html
同源 只有路径不同
https://store.company.com/secure.html
http://store.company.com:81/dir/etc.html
失败 协议不同,http和https
http://example.com
http://example.com:8080
失败 端口不同( http:// 默认端口是80)

同源策略带来的问题

  1. 一级域名相同,只是二级域名不同的同一所有者的网页被限制(Cookie、LocalStorage、IndexDB的读取)
  2. 无法跨域发送 AJAX 请求
  3. 无法操作 DOM

Q:为什么 Form 表单可以跨域发送请求,而 AJAX 不可以。
A:因为 Form 表单提交之后会刷新页面,所以即使跨域了也无法获取到数据,所以浏览器认为这个是安全的。而 AJAX 最大的优点就是在不重新加载整个页面的情况下,更新部分网页内容。如果让它跨域,则可以读取到目标 URL 的私密信息,这将会变得非常危险,所以浏览器是不允许 AJAX 跨域发送请求的。

跨域的原理

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的。
同源策略,是浏览器对 JavaScript 实施的安全限制,只要协议、域名、端口有任何一个不同,都被当作是不同的源。
跨域原理,即是通过各种方式,避开浏览器的安全限制

跨域解决方案

虽然同源策略一定程度上保证了用户的信息安全,但是也因此带来了很多不便,那么我们就要有解决方案来绕过跨域进行数据交互。

由于同源策略是浏览器的限制,所以请求的发送和响应是可以进行,只不过浏览器不接受罢了

再次强调,浏览器同源策略并不是对所有的请求均制约:

  • 制约: XmlHttpRequest,即ajax请求。
  • 不叼: img、iframe、script等具有src属性的标签。

随着技术的发展,跨域的解决方案也在改变:

  • jsonp(JSON with Padding) 是 json 的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。

    ajax 请求受同源策略影响,不允许进行跨域请求,而 script 标签 src 属性中的链 接却可以访问跨域的 js 脚本,利用这个特性,服务端不再返回 JSON 格式的数据,而是 返回一段调用某个函数的 js 代码,在 src 中进行了调用,这样实现了跨域。

  • document.domin,基础域名相同 子域名不同

  • CORS,(Cross-origin resource sharing)跨域资源共享 服务器设置对CORS的支持原理:服务器设置Access-Control-Allow-Origin HTTP响应头之后,浏览器将会允许跨域请求。实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

  • proxy代理,目前常用方式。 通俗点说就是客户端浏览器发起一个请求会存在跨域问题,但是服务端向另一个服务端发起请求并无跨域,因为跨域问题归根结底源于同源策略,而同源策略只存在于浏览器。所以,我们是不是可以通过 Nginx 配置一个代理服务器,反向代理访问跨域的接口,并且我们还可以修改 Cookiedomain 信息,方便当前域 Cookie 写入

  • window.postMessage(),利用h5新特性window.postMessage()。

  • websocket

  • requests,如果你是用的Python,那么你可以使用requests模块来解决跨域问题,这里不在展开说了。

Cross-Origin Read Blocking (CORB)

https://juejin.cn/post/6844903831373889550

https://cloud.tencent.com/developer/article/1730263?from=15425

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types#textjavascript

这个算是强势插入的部分,因为这部分不解决,后面演示jsonp的示例的时候,还是会被阻止,所以,先把这个问题解决掉。

什么是CORB

CORB( Cross-Origin Read Blocking),跨域读取阻止。旨在为可疑的跨域资源负载可能会在网络浏览器到达网页之前被识别并阻止。

例如我有这样一个server端的返回,就简单的返回一个字符串:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import datetime
from django.shortcuts import render, HttpResponse, redirect

def secret_data(request):
    """ 机密数据 """
    data = "来自9000端口的的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    response = HttpResponse(data)
    # response['Content-Type'] = "text/javascript;charset=UTF-8"
    return response

如果,我直接通过script的src进行发请求,那么就会报:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
</body>
<script src="http://127.0.0.1:9000/secret_data/"></script>
</html>
/*
控制台会报错:
Cross-Origin Read Blocking (CORB) 已屏蔽 MIME 类型为 text/html 的跨域响应 http://127.0.0.1:9000/secret_data/。
如需了解详情,请参阅 https://www.chromestatus.com/feature/5629709824032768。
*/

分析其原因,server端正常响应了。响应头中有两个请求头是我们需要注意的:

Content-Type: text/html; charset=utf-8
X-Content-Type-Options: nosniff

首先,X-Content-Type-Optionsnosniff的作用是,下面两种情况的请求将会被阻止:

  • 请求类型是style,但是 MIME 类型不是 text/css
  • 请求类型是"script” 但是 MIME 类型不是 JavaScript MIME类型。

而我们的响应头Content-Typetext/html不是 JavaScript MIME类型,所以被阻止了。

关于为啥text/html不是 JavaScript MIME类型,参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types#textjavascript

所以,将Content-Type的值设置为符合 JavaScript MIME类型的值就完了。

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import datetime
from django.shortcuts import render, HttpResponse, redirect

def secret_data(request):
    """ 机密数据 """
    data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    response = HttpResponse(data)
    # 将 Content-Type 设置为符合 JavaScript mime 类型的值
    response['Content-Type'] = "text/javascript;charset=UTF-8"
    return response

这样,前端script的src请求就会报个Uncaught SyntaxError: Unexpected token ':'的错误了,至于这个错误怎么解决,那就是下个小节jsonp要处理的事情了。

jsonp实现跨域请求

由于script标签不受同源策略影响,所以jsonp本质上就是通过script标签的src属性来向服务器进行数据请求。

json的缺点:jsonp只支持get请求,因为script标签的src只能使用get请求,而且前端使用jsonp,后端也需要配合。

Ajax直接像非同源地址发送请求,会被拦截

首先我的后端代码是这样的:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
# 后端监听:http://127.0.0.1:9000/secret_data/
import datetime
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse
from api import models


def secret_data(request):
    """ 机密数据 """
    data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    response = HttpResponse(data)
    # 将 Content-Type 设置为符合 JavaScript mime 类型的值
    response['Content-Type'] = "text/javascript;charset=UTF-8"
    return response

然后,前端直接通过Ajax发送请求的话,会被拦截:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TT</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    /*
    首先, 我直接通过Ajax直接向非同源地址发送请求会被拒
    Access to XMLHttpRequest at 'http://127.0.0.1:9000/secret_data/' from origin 'http://127.0.0.1:8000'
    has been blocked by CORS policy: No 'Access-Control-Allow-Origin'
    header is present on the requested resource.
    注意,并不是请求的服务器没有返回数据,而是返回给浏览器时,浏览器根据同源策略做了拦截
    接下来,我们来演示通过jsonp来绕过同源策略,达到请求到数据的目的
    */
    $.ajax({
        url: "http://127.0.0.1:9000/secret_data/",
        type: "GET",
        success: function(data) {
            console.log(data)
        }
    })
</script>
</html>

被拒了,想法处理吧!

手动档:jsonp的形式进行发送请求

需要后端对返回的数据进行处理:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import time
import datetime
from django.shortcuts import render, HttpResponse, redirect


def secret_data(request):
    """ 机密数据 """
    # 睡1秒,是让你有时间看dom结构,ajax的jsonp也是搞个script标签,拿到数据,再删掉
    # 但请求太快,看不清楚,所以睡一秒,让你看一眼
    time.sleep(1)
    data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    # 这个callback跟前端部分的回调函数名一致
    call_back = "jsonp_callback"
    response = HttpResponse('{}("{}")'.format(call_back, data))
    # 将 Content-Type 设置为符合 JavaScript mime 类型的值
    response['Content-Type'] = "text/javascript;charset=UTF-8"
    return response

前端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TT</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h3>正经网站</h3>
            <div>
                <h4>只是用jsonp配合后端获取数据</h4>
                <input type="button" onclick="jsonp1('http://127.0.0.1:9000/secret_data/')" value="手动通过jsonp请求">
            </div>
            <div id="data" style="color: red;"></div>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    // 自定义回调函数名字
    function jsonp_callback(msg) {
        $("#data").html(msg)
        // script标签使用完,偷摸删掉
        document.head.removeChild(tag);
    }

    function jsonp1(url) {
        tag = document.createElement('script')
        tag.src = url;
        document.head.appendChild(tag);
    }
</script>
</html>

这么搞,不够灵活,后端需要跟前端约定怎么写回调函数。

自动档,通过Ajax支持jsonp的原理实现

后端:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import time
import datetime
from django.shortcuts import render, HttpResponse, redirect


def secret_data(request):
    """ 机密数据 """
    # 睡1秒,是让你有时间看dom结构,ajax的jsonp也是搞个script标签,拿到数据,再删掉
    # 但请求太快,看不清楚,所以睡一秒,让你看一眼
    time.sleep(1)
    data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    # 这个callback跟前端部分的回调函数名一致
    # call_back = "jsonp_callback"
    call_back = request.GET.get("callback")
    response = HttpResponse('{}("{}")'.format(call_back, data))
    # 将 Content-Type 设置为符合 JavaScript mime 类型的值
    response['Content-Type'] = "text/javascript;charset=UTF-8"
    return response

后端,这么搞,通过传参,你前端给我啥回调函数,我就用啥回调函数,相对比较灵活。

前端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TT</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h3>正经网站</h3>
            <div>
                <h4>通过Ajax支持json的原理发送请求</h4>
                <input type="button" onclick="jsonp2()" value="点我发送jsonp请求">
            </div>
            <div id="data" style="color: red;"></div>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    // 自定义回调函数名字
    function jsonp_callback(msg) {
        $("#data").html(msg)
    }
    function jsonp2() {
        $.ajax({
            url: "http://127.0.0.1:9000/secret_data/",
            type: "GET",
            dataType: "JSONP",
            jsonp: "callback",  // get请求的参数的key
            jsonpCallback:"jsonp_callback"  // 回调函数名字
            // 相当于拼接出来一个http://127.0.0.1:9000/secret_data/?callbakc=jsonp_callback
        })
    }
</script>
</html>

通过这个示例,可以发现,jQuery的Ajax支持jsonp,跟我们之前手扣的原理一样。

这个前端示例,你看这是Ajax请求,但由于搞得是jsonp,所以,实际发的是jsonp而不是Ajax请求。

另外,使用Ajax发jsonp请求,请求方式只能是GET,你就算将type改成POST,它内部最后发的也是GET,因为其原理还是借助script标签的src发请求,而src只能发GET请求。

CORS实现跨域

本节后端示例,基于Django实现,且注释django.middleware.csrf.CsrfViewMiddleware中间件。

参考或摘自:

https://www.cnblogs.com/magicg/p/13468577.html

https://baijiahao.baidu.com/s?id=1677316686417898389&wfr=spider&for=pc

https://www.jianshu.com/p/a52ffa343e7c

https://www.cnblogs.com/wupeiqi/articles/5703697.html

https://juejin.cn/post/7003232769182547998

jsonp相当于根据同源策略不限制script标签发送跨域请求,搞出来的这样的一个解决方案。

而CORS就是不是了,它是在服务器返回数据时,通过特殊的响应头,告诉浏览器不要进行跨域拦截。

什么是CORS?

什么是CORS?

CORS(Cross-origin resoure sharing,跨域资源共享 )是一个W3C标准,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了A JAX只能同源使用的限制。

CORS 是 HTTP 的一部分,它允许服务端来指定哪些主机可以从这个服务端加载资源。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附件的头信息,有时还会多处一次附件的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口(响应报文包括了正确的 CORS 响应头),就可以跨源通信。

补充:AJAX与XMLHttpRequest
AJAX是一种技术方案,但并不是一种新技术。它依赖的是现有的CSS/HTML/Javascript,而其中最核心的依赖是浏览器提供的XMLHttpRequest对象,是这个对象使得浏览器可以发出HTTP请求与接收HTTP响应。
可以总结为:我们使用XMLHttpRequest对象来发送一个AJAX请求。

使用CORS解决跨域

此时,我们将服务端代码还原:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import datetime
from django.shortcuts import render, HttpResponse, redirect


def secret_data(request):
    """ 机密数据 """
    data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    response = HttpResponse(data)
    return response

前端ajax代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TT</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h3>正经网站</h3>
            <div>
                <h4>通过Ajax直接向非同源地址发送请求,会被拒</h4>
                <input type="button" onclick="cors()" value="点我发送正常ajax请求">
            </div>
            <div id="data" style="color: red;"></div>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    function cors() {
        $.ajax({
            url: "http://127.0.0.1:9000/secret_data/",
            type: "GET",
            success: function (msg) {
                $("#data").html(msg)
            }
        })
    }
</script>
</html>

被拒了!想办法解决吧。

接下来,写个简单的解决示例,需要在服务器端设置Access-Control-Allow-Origin响应头,客户端无需特殊处理。

后端:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import datetime
from django.shortcuts import render, HttpResponse, redirect


def secret_data(request):
    """ 机密数据 """
    data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    response = HttpResponse(data)
    # 指定某个非同源的地址向我发请求,并要求浏览器不要进行跨域拦截
    # response['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"
    # 所有请求我的非同源地址,都不要拦截
    response['Access-Control-Allow-Origin'] = "*"
    return response

前端代码不变:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TT</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h3>正经网站</h3>
            <div>
                <h4>通过Ajax直接向非同源地址发送请求,会被拒</h4>
                <input type="button" onclick="cors()" value="点我发送正常ajax请求">
            </div>
            <div id="data" style="color: red;"></div>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    function cors() {
        $.ajax({
            url: "http://127.0.0.1:9000/secret_data/",
            type: "GET",
            success: function (msg) {
                $("#data").html(msg)
            }
        })
    }
</script>
</html>

问题虽然解决了,但是还有些问题需要继续研究。

请求头:Origin

https://www.cnblogs.com/magicg/p/13468577.html

再往下说之前,我们来补充一个知识点,那就是请求头origin。

并不是所有的请求都会携带origin,在浏览器中,携带origin的逻辑如下:

  • 如果存在跨域,则带上origin,值为当前的域名。
  • 如果不存在跨域,则不带origin。
# 注意,此时当前server端的CsrfViewMiddleware是开着的
import datetime
from django.shortcuts import render, HttpResponse, redirect


def secret_data(request):
    """ 机密数据 """
    print(request.headers.get('Origin'))
    data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    response = HttpResponse(data)
    # 指定某个非同源的地址向我发请求,并要求浏览器不要进行跨域拦截
    # response['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"
    # 所有请求我的非同源地址,都不要拦截
    response['Access-Control-Allow-Origin'] = "*"
    return response

上例,如果是别的域发的请求,request.headers.get('Origin')会打印对方的域名http://127.0.0.1:8000,如果是自己域发的请求,就没有值,返回None。

简单请求/复杂请求

浏览器将CORS请求分为两类,简单请求(simple request)和复杂请求(not-so-simple request)。

若满足下面所有条件,则该请求是简单请求:

请求方法是以下三种之一:
    HEAD
    GET
    POST
HTTP 的头信息不超出以下几种字段:
    Accept
    Accept-Language
    Content-Language
    Content-Type
    DPR
    Downlink
    Save-Data
    Viewport-Width
    Width
Content-Type 的值仅限于以下三种之一
    text/plain
    multipart/form-data
    application/x-www-form-urlencoded
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器。
请求中没有使用 ReadableStream 对象。

凡是不满足上述条件的,都是复杂请求。

简单请求和复杂请求的区别:

  • 简单请求,一次请求。
  • 复杂请求,两次请求,在真正的请求发送之前,会先发一次请求做"预检","预检"通过,正真的请求才会发送并得到正确的响应。预检请求,一般是OPTIONS请求。

简单请求

本节开始的示例,就是一个简单请求。

后端通过设置Access-Control-Allow-Origin告诉浏览器,不需要拦截。

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import datetime
from django.shortcuts import render, HttpResponse, redirect


def secret_data(request):
    """ 机密数据 """
    data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    response = HttpResponse(data)
    
    # 指定某个非同源的地址向我发请求,并要求浏览器不要进行跨域拦截
    # response['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"
    
    # 所有请求我的非同源地址,都不要拦截
    response['Access-Control-Allow-Origin'] = "*"
    return response

复杂请求

复杂请求,就是我们的server端要对那些有特殊要求的请求,进行处理,比如请求类型是PUT或者DELETE,或者Content-Type是applicatin/json的。

复杂请求会在正式i请求前,增发一次HTTP请求,称为预检请求(preflight)。

浏览器在预检请求中,会询问服务器,当前网页所在的域名是否可以通过,以及可以使用哪些请求头等信息,只有在服务器在预检请求的响应中得到肯定的答复,浏览器才会正式的发送请求,否则就会报错。

如前端的Ajax请求,请求方法是PUT,并且携带请求头xx

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TT</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h3>正经网站</h3>
            <div>
                <h4>通过Ajax直接向非同源地址发送请求,会被拒</h4>
                <input type="button" onclick="cors()" value="点我发送正常ajax请求">
            </div>
            <div id="data" style="color: red;"></div>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    function cors() {
        $.ajax({
            url: "http://127.0.0.1:9000/secret_data/",
            type: "PUT",
            headers: {"xx": "oo"},
            success: function (msg) {
                $("#data").html(msg)
            }
        })
    }
</script>
</html>

直接向后发的话,就算后端加上response['Access-Control-Allow-Origin'] = "*",前端也报错:

Access to XMLHttpRequest at 'http://127.0.0.1:9000/secret_data/' from origin 'http://127.0.0.1:8000' has been blocked by CORS policy: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.

所以,需要在后端预检请求中处理:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import time
import datetime
from django.shortcuts import render, HttpResponse, redirect


def secret_data(request):
    """ 机密数据 """
    if request.method == "OPTIONS":
        # 预检请求的response可以不传值,主要目的就是通过设置响应头,让后浏览器不要拦截后续的真正的请求
        response = HttpResponse()

        # 所有请求我的非同源地址,都不要拦截
        response['Access-Control-Allow-Origin'] = "*"
        # 告诉浏览器,我允许复杂请求的PUT请求访问
        response['Access-Control-Allow-Methods'] = "PUT"

        # 支持多种复杂的请求方式,以逗号隔开
        # response['Access-Control-Allow-Methods'] = "PUT,DELETE"

        # 如果你想在请求头中携带一些特殊的请求头,就要按照以下这么设置
        # 多个请求头以逗号隔开,且只允许下面声明的请求头
        # 当然,可以少携带或者不携带也不报错,但不能多携带,否则就报错
        response['Access-Control-Allow-Headers'] = "xx,oo"
        return response
    elif request.method == "PUT":
        print(request.headers.get('Origin'))
        print(request.headers.get('xx'))
        print(request.headers.get('oo'))
        data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        response = HttpResponse(data)
        # 所有请求我的非同源地址,都不要拦截
        response['Access-Control-Allow-Origin'] = "*"
        return response

如果要对所有的请求都要这么搞,你可以把它写在响应的中间件中,比较方便。

注意,如果你想运行本小节的示例,必须将9000端口的server端的的CsrfViewMiddleware中间件关掉,因为CsrfViewMiddleware开着的话,它会去请求头中提取cookie数据,而我们,这个请求压根就没带cookie,所以,请求肯定被CsrfViewMiddleware拦截掉,然后你就发现,server端报403 Forbidden,客户端报CORS错误,这真是节外生枝............

想要解决这个问题,我们再下个小节中,进行cookie的相关处理。

请求携带cookie

接上一小节,server端需要cookie数据,那么我们就来看看这两端都是需要做哪些操作。

结合我们的示例,想要同时解决CsrfViewMiddleware的校验问题和CORS问题,就需要:

  • 9000的server端,要给别的源(这里是8000端口的客户端)设置cookie,好让人家请求携带,这是Django的csrf特有的,如果你的server端不是Django,或者关闭CsrfViewMiddleware中间件,这一步设置cookie就没必要了。
  • 8000端口的前端:在Ajax中必须声明xhrFields:{withCredentials: true},注意,withCredentials的值必须是true,否则不携带。
  • 9000的server端:
    • 无论预检请求还是真正的请求,都要声明response['Access-Control-Allow-Credentials'] = "true",注意,必须是"true",不是Python的True,人家浏览器不认True
    • response['Access-Control-Allow-Origin']的值不能是*,而是要明确一个域,如"http://127.0.0.1:8000",这样才可以。

前端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TT</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h3>正经网站</h3>
            <div>
                <h4>通过Ajax直接向非同源地址发送请求,会被拒</h4>
                <input type="button" onclick="cors()" value="点我发送正常ajax请求">
            </div>
            <div id="data" style="color: red;"></div>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<!-- 为了方便使用jQuery提取cookie值,特地引入了jquery.cookie这个文件来方便操作,更多请参考本文最后的部分 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    function cors() {
        console.log($.cookie("csrftoken"));
        $.ajax({
            url: "http://127.0.0.1:9000/secret_data/",
            type: "PUT",
            headers: {"xx": "oo", "X-CSRFToken": $.cookie("csrftoken")}, // 添加后端需要的cookie值
            xhrFields:{withCredentials: true},  // withCredentials的值必须时true,否则不携带
            success: function (msg) {
                $("#data").html(msg)
            }
        })
    }
</script>
</html>

后端:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import time
import datetime
from django.shortcuts import render, HttpResponse, redirect
from django.views.decorators.csrf import ensure_csrf_cookie

# 该装饰器强行为请求设置csrf相关的cookie值
@ensure_csrf_cookie
def secret_data(request):
    """ 机密数据 """
    if request.method == "OPTIONS":
        # 预检请求的response可以不传值,主要目的就是通过设置响应头,让后浏览器不要拦截后续的真正的请求
        response = HttpResponse()

        # 所有请求我的非同源地址,都不要拦截
        response['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"
        # 告诉浏览器,我允许复杂请求的PUT请求访问
        response['Access-Control-Allow-Methods'] = "PUT"

        # 支持多种复杂的请求方式,以逗号隔开
        # response['Access-Control-Allow-Methods'] = "PUT,DELETE"

        # 如果你想在请求头中携带一些特殊的请求头,就要按照以下这么设置
        # 多个请求头以逗号隔开,且只允许下面声明的请求头
        # 当然,可以少携带或者不携带也不报错,但不能多携带,否则就报错
        # X-CSRFToken 这个请求头,是为了通过Django的csrf中间件特意携带的
        response['Access-Control-Allow-Headers'] = "xx,oo,X-CSRFToken"

        # 允许发送cookie,必须是"true"
        response['Access-Control-Allow-Credentials'] = "true"
        return response
    elif request.method == "PUT":
        print(request.headers.get('Origin'))
        print(request.headers.get('xx'))
        print(request.headers.get('oo'))
        data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        response = HttpResponse(data)
        # 所有请求我的非同源地址,都不要拦截
        response['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"

        # 允许发送cookie,必须是"true"
        response['Access-Control-Allow-Credentials'] = "true"

        # 指定预检请求的有效期,单位: 秒
        response['Access-Control-Max-Age'] = 10

        # 设置其它cookie,下次请求时,就会拿到对方上传过来的cookie
        response.set_cookie("cookie_key", "cookie_value")
        return response

设置预检请求的有效期

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Max-Age
这里再补充一个参数Access-Control-Max-Age,该参数用于设置预检请求的有效期,当浏览器获取了目标服务器的这个响应头,就会进行"缓存",然后在有效期内,无需再发送预检请求,减轻服务器压力。

如果不指定该参数,那么不同的浏览器也会有不同的功能来实现预检请求的有效期。
在 Firefox 中,上限是24小时 (即 86400 秒)。
在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。
从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。
Chromium 同时规定了一个默认值 5 秒。
如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。

注意Access-Control-Max-Age设置针对完全一样的url,当url包含路径参数时,其中一个url的Access-Control-Max-Age设置对另一个url没有效果。

PS:如果看不出来效果,就换个浏览器试试。

前端代码不变:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TT</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h3>正经网站</h3>
            <div>
                <h4>通过Ajax直接向非同源地址发送请求,会被拒</h4>
                <input type="button" onclick="cors()" value="点我发送正常ajax请求">
            </div>
            <div id="data" style="color: red;"></div>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<!-- 为了方便使用jQuery提取cookie值,特地引入了jquery.cookie这个文件来方便操作,更多请参考本文最后的部分 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    function cors() {
        console.log($.cookie("csrftoken"));
        $.ajax({
            url: "http://127.0.0.1:9000/secret_data/",
            type: "PUT",
            headers: {"xx": "oo", "X-CSRFToken": $.cookie("csrftoken")}, // 添加后端需要的cookie值
            xhrFields:{withCredentials: true},  // withCredentials的值必须时true,否则不携带
            success: function (msg) {
                $("#data").html(msg)
            }
        })
    }
</script>
</html>

后端代码:

# 注意,此时当前server端的CsrfViewMiddleware是开着的
import time
import datetime
from django.shortcuts import render, HttpResponse, redirect
from django.views.decorators.csrf import ensure_csrf_cookie

# 该装饰器强行为请求设置csrf相关的cookie值
@ensure_csrf_cookie
def secret_data(request):
    """ 机密数据 """
    if request.method == "OPTIONS":
        # 预检请求的response可以不传值,主要目的就是通过设置响应头,让后浏览器不要拦截后续的真正的请求
        response = HttpResponse()

        # 所有请求我的非同源地址,都不要拦截
        response['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"
        # 告诉浏览器,我允许复杂请求的PUT请求访问
        response['Access-Control-Allow-Methods'] = "PUT"

        # 支持多种复杂的请求方式,以逗号隔开
        # response['Access-Control-Allow-Methods'] = "PUT,DELETE"

        # 如果你想在请求头中携带一些特殊的请求头,就要按照以下这么设置
        # 多个请求头以逗号隔开,且只允许下面声明的请求头
        # 当然,可以少携带或者不携带也不报错,但不能多携带,否则就报错
        # X-CSRFToken 这个请求头,是为了通过Django的csrf中间件特意携带的
        response['Access-Control-Allow-Headers'] = "xx,oo,X-CSRFToken"

        # 允许发送cookie,必须是"true"
        response['Access-Control-Allow-Credentials'] = "true"

        # 指定预检请求的有效期,单位: 秒
        response['Access-Control-Max-Age'] = 10

        return response
    elif request.method == "PUT":
        print(request.headers.get('Origin'))
        print(request.headers.get('xx'))
        print(request.headers.get('oo'))
        data = "来自9000的机密数据,时间: {}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        response = HttpResponse(data)
        # 所有请求我的非同源地址,都不要拦截
        response['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"

        # 允许发送cookie,必须是"true"
        response['Access-Control-Allow-Credentials'] = "true"

        # 设置其它cookie,下次请求时,就会拿到对方上传过来的cookie
        response.set_cookie("cookie_key", "cookie_value")
        return response

CORS五大响应头小结

我们了解了这么些个请求头或者响应头:

  • Origin,这个特殊的请求头,如果存在跨域,则带上origin,值为当前的域名。如果不存在跨域,则不带origin。
  • Access-Control-Allow-Origin,这个响应头非常重要,在服务器端设置,允许指定域http://127.0.0.1:8000或者所有域*不受跨域的影响。
  • Access-Control-Allow-Methods,预检请求中,指定的响应头,允许哪些复杂请求不受跨域影响。
  • Access-Control-Allow-Headers,当跨域的Ajax请求时,想要携带额外的请求头,就需要在这个响应头中声明。
  • Access-Control-Allow-Credentials,如果Ajax请求想要携带参数,需要将这个响应头的值指定为"true",而且还需要在Ajax中指定xhrFields:{withCredentials: true}才行。
  • Access-Control-Max-Age,指定预检请求在后续的指定时间内,无需重复发送预检请求,减轻了服务器的压力。如果不指定,那么不同的浏览器则有不同的规则达到"缓存"预检请求的目的。

CORS与jsonp的比较

CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。

JSONP 只支持 GET 请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。

CSRF

参考或者摘自:

https://blog.51cto.com/m0re/3884882

https://blog.51cto.com/u_15127679/3319486

https://juejin.cn/post/6879363378381488142

https://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html

https://juejin.cn/post/6844903991575314445

https://www.cnblogs.com/clschao/articles/10468335.html#part_3

https://www.cnblogs.com/wupeiqi/articles/5703697.html

什么是CSRF?

CSRF(Cross-site request forgery,跨站请求伪造,也可以称为one click attack/session riding,简写:CSRF/XSRF)是一种常见的攻击方式。

攻击过程如下图:

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1. 登录受信任的网站A,并在本地生成cookie。
  2. 在不登出网站A的情况,访问了危险网站B。

看到这里,你也许会说:"如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击"。是的,确实如此,但你不能保证以下情况不会发生:

  1. 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。

  2. 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了......)

  3. 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。

上面摘自:https://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html

扯原理,百度出来一堆,我们就看看我们怎么在Django中设置csrftoken,从而避开403 Forbidden这个大黄页。

Django的csrftoken认证机制

我们根据这如下这个登录示例展开来说,且settings中的csrf相关的中间件是打开的:

MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',
]

后端:

from django.shortcuts import render, redirect
from django.http import JsonResponse


def login(request):
    if request.method == "GET":
        return render(request, 'login.html')
    else:
        user = request.POST.get("user")
        pwd = request.POST.get("pwd")
        if request.is_ajax():  # Ajax提交的数据
            if user == 'root' and pwd == '123':
                return JsonResponse({'content': 'login ok'})
            else:
                return JsonResponse({'content': "user or password error"})
        else:  # form表单提交的数据
            if user == 'root' and pwd == '123':
                # 本来应该跳转到主页的,懒得写,就这么凑活吧,反正这块也不是我们的重点
                return render(request, 'login.html', {"msg": "login ok"})
            else:
                return render(request, 'login.html', {"msg": "user or password error"})

前端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>form提交数据</h2>
            <form action="" method="post">
                {% csrf_token %}
                <label for="user">用户名</label>
                <input type="text" value="root" name="user" class="form-control">
                <label for="pwd">密码</label>
                <input type="text" value="123" name="pwd" class="form-control">
                <button type="submit" id="formBtn"  class="btn btn-success">提交</button>
                <span style="color: red">{{ msg }}</span>
            </form>
            <hr>
            <h2>ajax提交数据</h2>
            <form action="" method="post">
                <label for="user">用户名</label>
                <input type="text" value="root" id="user" class="form-control">
                <label for="pwd">密码</label>
                <input type="text" value="123" id="pwd" class="form-control">
                <!-- 虽然Ajax这部分也是form表单,但是button的type是button,那么这个提交按钮就是普通的按钮,不具有默认提交事件 -->
                <button type="button" id="ajaxBtn"  class="btn btn-default">提交</button>
                <span style="color: red" id="ajaxMsg"></span>
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    // Ajax的csrf认证前提也是页面中必须有{% csrf_token %}
    $("#ajaxBtn").click(function () {
        let user = $("#user").val();
        let pwd = $("#pwd").val();
        $.ajax({
            url: "/login/",
            type: "POST",
            data: {"user": user, "pwd": pwd, "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val()},
            success: function(msg) {
                $("#ajaxMsg").html(msg['content']);
            }
        })
    })
</script>
</html>

我们来以form表单提交数据,来看Django的csrf认证流程是怎样的。

1. 用户请求页面

当用户以GET方式请求登录页面,Django在返回页面的时候,在页面中还添加了{% csrf_token %},那么它会在页面中渲染成一个隐藏的input框;除此之外,还返回了一个关于csrf的cookie键值对,它们长这样:

#  模板字符串 {% csrf_token %} 被渲染成了隐藏的input框,请重点关注 name 和 value这两个属性值
<input type="hidden" name="csrfmiddlewaretoken" value="HyJOaaMnmG6gY9Yt8TgTQUbcJxeeUv3EX6KjUovXzDTsV2cwqjKqHCY7vrsZR5bi">

#  与此同时,F12中,network栏,也可以找到login页面返回的Cookie值
csrftoken: GtmrFSNCUXlcVZZGDSSrdCFOXD4DkGIXPrakRt7gXEn3a9H6DsDrAeAlQs4PKgrN

注意,如果你刷新页面,{% csrf_token %}的隐藏input框的value值会发生改变;但是cookie值不变。

2. 用户提交数据

当用户点击form表单的的提交按钮后,隐藏的input框的csrf的字符串和其他input框的值一起发送到Django后台,还包括cookie中的csrftoken值。

3. Django的csrf中间件进行认证

在Django的csrf认证中间件中,会取出cookie的csrftoken值和请求体中的csrfmiddlewaretoken的值。

验证过程:

Django分别取出这两个crsrf token字符串。

csrfmiddlewaretoken: HyJOaaMnmG6gY9Yt8TgTQUbcJxeeUv3EX6KjUovXzDTsV2cwqjKqHCY7vrsZR5bi
csrftoken: GtmrFSNCUXlcVZZGDSSrdCFOXD4DkGIXPrakRt7gXEn3a9H6DsDrAeAlQs4PKgrN

而关于这两个token字符串,其前32位是salt,后面是加密后的token,django通过salt能解密出唯一的secret key。
如果两个字符串解密后secret key跟那个唯一的secret key一样,表示本次请求合法,否则就403 Forbidden。

而关于secret key,你也应该不陌生,它就是Django settings中的:

SECRET_KEY = 'django-insecure-cy#c6k1=n2ca@9v6%(hdy&617^nd1rdh_14zlgzyp$^&n#axuw'

想要研究源码,一探究竟的,可以参考:

from django.middleware import csrf
from django.middleware.csrf import CsrfViewMiddleware

再补充一点,Django在获取token时,会先去请求体中找csrfmiddlewaretoken值,找不到的话,会去请求头中找,找一个叫做X-CSRFToken的键值对,如果这个键对应的值和cookie中的csrftoken对应的值相同,也能通过认证。

这就意味着,我们可以通过请求头和请求体这两种方式携带csrftoken值,进行校验。

Ajax请求设置csrftoken

form表单提交数据,如何携带csrf token我们知道了,就是在form表单中搞个{% csrf_token %}模板字符串就完事了。

那么如果是Ajax请求该如何设置csrftoken呢?一起来看看几种携带的方式。

首先,Django后端代码不变:

from django.shortcuts import render, redirect
from django.http import JsonResponse


def login(request):
    if request.method == "GET":
        return render(request, 'login.html')
    else:
        user = request.POST.get("user")
        pwd = request.POST.get("pwd")
        print(request.META)
        if request.is_ajax():  # Ajax提交的数据
            if user == 'root' and pwd == '123':
                return JsonResponse({'content': 'login ok'})
            else:
                return JsonResponse({'content': "user or password error"})
        else:  # form表单提交的数据
            if user == 'root' and pwd == '123':
                # 本来应该跳转到主页的,懒得写,就这么凑活吧,反正这块也不是我们的重点
                return render(request, 'login.html', {"msg": "login ok"})
            else:
                return render(request, 'login.html', {"msg": "user or password error"})

我们主要看页面中的Ajax怎么携带csrftoken的。

请求头中携带

前端页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>ajax提交数据</h2>
            <form>
                <label for="user">用户名</label>
                <input type="text" value="root" id="user" class="form-control">
                <label for="pwd">密码</label>
                <input type="text" value="123" id="pwd" class="form-control">
                <!-- 虽然也是form表单,但是button的type是button,那么这个提交按钮就是普通的按钮,不具有默认提交事件 -->
                <button type="button" id="ajaxBtn" class="btn btn-default">提交</button>
                <span style="color: red" id="ajaxMsg"></span>
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    $("#ajaxBtn").click(function () {
        let user = $("#user").val();
        let pwd = $("#pwd").val();
        $.ajax({
            url: "/login/",
            type: "POST",
            // 注意X-CSRFToken的值不能是模板字符串中的name值(之前说过,input标签的name值和cookie的csrftoken值不一样)
            // 而是要从cookie中进行提取
            // 而要从cookie中提取csrftoken值,可以直接通过document.cookie提取,但是它提取的是所有的cookie键值对,一般我们不这么用
            // 而是通过引入jquery-cookie文件,然后通过$.cookie("csrftoken")直接提取我们想要的值,比较方便
            headers: {"X-CSRFToken": $.cookie("csrftoken")},
            data: {"user": user, "pwd": pwd},
            success: function (msg) {
                $("#ajaxMsg").html(msg['content']);
            }
        })
    })
</script>
</html>

关于jQuery操作cookie的更多姿势,请参考本文的最后部分。

由于还需要单独引入外部jquery-cookie文件,所以,一般用的不多。

另外,还有需要注意的:

如果使用从cookie中取csrftoken的方式,需要确保cookie存在csrftoken值。

如果你的视图渲染的HTML文件中没有包含 {% csrf_token %},Django可能不会设置CSRFtoken的cookie。

这个时候需要使用ensure_csrf_cookie()装饰器强制设置Cookie。

django.views.decorators.csrf import ensure_csrf_cookie


@ensure_csrf_cookie
def login(request):
    pass

更多详见:https://docs.djangoproject.com/en/3.2/ref/csrf/

接着补充。

如果你不想引入外部的jquery-cookie文件,就像手扣出来cookie值,那么你可以这么来:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>ajax提交数据</h2>
            <form>
                <label for="user">用户名</label>
                <input type="text" value="root" id="user" class="form-control">
                <label for="pwd">密码</label>
                <input type="text" value="123" id="pwd" class="form-control">
                <!-- 虽然也是form表单,但是button的type是button,那么这个提交按钮就是普通的按钮,不具有默认提交事件 -->
                <button type="button" id="ajaxBtn" class="btn btn-default">提交</button>
                <span style="color: red" id="ajaxMsg"></span>
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    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');
    // console.log(csrftoken);
    $("#ajaxBtn").click(function () {
        let user = $("#user").val();
        let pwd = $("#pwd").val();
        $.ajax({
            url: "/login/",
            type: "POST",
            headers: {"X-CSRFToken": getCookie('csrftoken')},
            data: {"user": user, "pwd": pwd},
            success: function (msg) {
                $("#ajaxMsg").html(msg['content']);
            }
        })
    })
</script>
</html>

当然了,其他姿势还是要补充的,如们使用jQuery.ajaxSetup()函数帮我们处理。该函数用于为ajax预先设置(更改)一些默认设置。记住,它是全局生效的。如果我们在这个函数中配置关于ajax的一些设置,那么在以后的ajax请求中,都会使用该函数配置的设置。比如,我们可以在这个函数中配置解决跨域的参数:

<script>
    // 预先设置ajax的某些配置,这里配置跨域参数
    $.ajaxSetup({
        data: {csrfmiddlewaretoken: '{{ csrf_token }}'}
    });
    // 后续的ajax请求,正常使用即可,无需关心跨域问题
    $("#ajaxBtn").click(function () {
        let user = $("#user").val();
        let pwd = $("#pwd").val();
        $.ajax({
            url: "/login/",
            type: "POST",
            data: {"user": user, "pwd": pwd},
            success: function (msg) {
                $("#ajaxMsg").html(msg['content']);
            }
        })
    })
</script>

上面那么写,或者下面这么写也行:

<script>
    // 预先设置ajax的某些配置,这里配置跨域参数
    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", $.cookie("csrftoken"));
            }
        }
    });
    // 后续的ajax请求,正常使用即可,无需关心跨域问题
    $("#ajaxBtn").click(function () {
        let user = $("#user").val();
        let pwd = $("#pwd").val();
        $.ajax({
            url: "/login/",
            type: "POST",
            data: {"user": user, "pwd": pwd},
            success: function (msg) {
                $("#ajaxMsg").html(msg['content']);
            }
        })
    })
</script>

请求体中携带

这种方式用的较多。

前端页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>ajax提交数据</h2>
            <!-- Ajax提交,我们需要在页面中生成csrf_token值,然后ajax请求时,在请求头或这请求体中携带  -->
            <!-- 且csrf_token模板字符串只要在页面中存在就行,是否在form表单中无所谓  -->
            {% csrf_token %}
            <form>
                <label for="user">用户名</label>
                <input type="text" value="root" id="user" class="form-control">
                <label for="pwd">密码</label>
                <input type="text" value="123" id="pwd" class="form-control">
                <!-- 虽然也是form表单,但是button的type是button,那么这个提交按钮就是普通的按钮,不具有默认提交事件 -->
                <button type="button" id="ajaxBtn" class="btn btn-default">提交</button>
                <span style="color: red" id="ajaxMsg"></span>
            </form>
        </div>
    </div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    $("#ajaxBtn").click(function () {
        let user = $("#user").val();
        let pwd = $("#pwd").val();
        $.ajax({
            url: "/login/",
            type: "POST",
            // $("[name='csrfmiddlewaretoken']").val() 这种方式是通过提取页面中的input标签中的name属性值,需要在页面中定义 csrf_token 模板字符串
            // data: {"user": user, "pwd": pwd, "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val()},
            // "{{ csrf_token }}" 这种方式是直接生成csrf_token值,不需要在页面中定义 csrf_token 模板字符串了
            data: {"user": user, "pwd": pwd, "csrfmiddlewaretoken": "{{ csrf_token }}"},
            success: function (msg) {
                $("#ajaxMsg").html(msg['content']);
            }
        })
    })
</script>
</html>

jQuery操作cookie

jquery.cookie.py下载地址:http://plugins.jquery.com/cookie/

参考:https://www.cnblogs.com/clschao/articles/10480029.html

想要通过jQuery操作cookie,需要引入js文件jquery.cookie.js,首先这个文件它依赖jQuery,所以,下载完别忘了先引入jQuery,再引入jquery.cookie.py

PS:你如果下载的话,你可能会看到,这个鬼东西最近更新是2014年,看着挺老了的,但是还能用。

引入:

<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery.cookie.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>

基操

// 1. 添加一个 “会话cookie(session cookie)”
$.cookie('the_cookie', 'the_value');
// 这里没有指明 cookie有效时间,所创建的cookie有效期默认到用户关闭浏览器为止,所以被称为 “会话cookie(session cookie)”。

// 2.创建一个cookie并设置有效时间为 7天
$.cookie('the_cookie', 'the_value', { expires: 7 });
// 这里指明了cookie有效时间,所创建的cookie被称为“持久 cookie (persistent cookie)”。注意单位是:天;

// 3.创建一个cookie并设置 cookie的有效路径
$.cookie('the_cookie', 'the_value', { expires: 7, path: '/' });
// 在默认情况下,只有设置 cookie的网页才能读取该 cookie。如果想让一个页面读取另一个页面设置的cookie,
// 必须设置cookie的路径。cookie的路径用于设置能够读取 cookie的顶级目录。将这个路径设置为网站的根目录,
// 可以让所有网页都能互相读取 cookie (一般不要这样设置,防止出现冲突)。

// 4.读取cookie
$.cookie('the_cookie');

// 5.删除cookie
$.cookie('the_cookie', null);   //通过传递null作为cookie的值即可

// 6.可选参数
$.cookie('the_cookie','the_value',{
    expires:7, 
    path:'/',
    domain:'jquery.com',
    secure:true
}) 
/*
expires:(Number|Date)有效期;设置一个整数时,单位是天;也可以设置一个日期对象作为Cookie的过期日期;
path:(String)创建该Cookie的页面路径;
domain:(String)创建该Cookie的页面域名;
secure:(Booblean)如果设为true,那么此Cookie的传输会要求一个安全协议,例如:HTTPS;
*/

drf + vue项目解决跨域问题

python3.9 + django3.2.5 + vue3.2.37 + vite3.1.0

前后端分离项目解决cors跨域问题,有前端配置和后端配置两种方式。
方案1:前端基于nodejs解决跨域问题
编辑前端项目根目录下的vite.config.js文件:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),
            Components({
            resolvers: [ElementPlusResolver()],
        }),
    ],
    server: {   // 就这个server部分
        port: '3000',           // 客户端的运行端口,此处也可以绑定vue运行的端口,当然也可以写在pycharm下
        host: 'www.luffycity.cn', // 客户端的运行地址,此处也可以绑定vue运行的域名,当然也可以写在pycharm下
        // 跨域代理
        proxy: {
            '/api': {
                // 凡是遇到 /api 路径的请求,都映射到 target 属性  /api/header/  ---> http://api.luffycity.cn:8000/header/
                target: 'http://api.luffycity.cn:8000/',
                changeOrigin: true,   // 是否支持跨域
                ws: true,    // 是否支持websocket跨域
                rewrite: path => path.replace(/^\/api/, '')
            }
        }
  }
})

方案2:在Django中配置
文档:https://github.com/ottoyiu/django-cors-headers/
需要下载一个专门解决跨域的模块:

pip install django-cors-headers

在Django的配置文件中配置:

# 1. 注册app
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',  # 解决跨域问题的应用
    'rest_framework',
    'home'
]

# 2. 添加相关中间件
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # cors跨域的中间件,往前放,必须在CommonMiddleware中间件前面
    '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',
]

# 3. 添加客户端白名单, CORS的配置信息:
# 方案1:
# CORS_ORIGIN_WHITELIST = (
#     'http://www.luffycity.cn:3000',
# )
# CORS_ALLOW_CREDENTIALS = False  # 不允许ajax跨域请求时携带cookie

# 方案2:
CORS_ALLOW_ALL_ORIGINS = True

常见跨域问题

Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/folder/3' from origin 'http://localhost:8081' has beer blocked by CORS Dolic: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

django3.2 + restframework + vue3

报错截图:

解决方案就是后端处理,vue前端不动。
这里我用中间件处理,首先 写好中间件:

from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponse

class AuthMiddleware(MiddlewareMixin):
	""" 在这里处理前后端分离的跨域问题,这样前端不用任何处理 """
	def process_request(self, request):
		""" 在这里处理预检请求 """
		if request.method == "OPTIONS":
			response = HttpResponse("")
			response["Access-Control-Allow-Origin"] = "*"
			response["Access-Control-Allow-Headers"] = "*"
			response["Access-Control-Allow-Methods"] = "*"
			return response  # 当发现请求是预检的options请求时,直接返回不做拦截
	
	def process_response(self, request, response):
		""" 其它复杂请求,都添加上各种响应头给客户端返回 """
		response["Access-Control-Allow-Origin"] = "*"
		response["Access-Control-Allow-Headers"] = "*"
		response["Access-Control-Allow-Methods"] = "*"
		return response

然后重点来了,要把这个中间件注册到settings.py中的最上面:

MIDDLEWARE = [
	'utils.middlewares.AuthMiddleware',  # 注册到其它中间件的前面,先处理跨域问题
	'django.middleware.security.SecurityMiddleware',
	# 'django.contrib.sessions.middleware.SessionMiddleware',
	'django.middleware.common.CommonMiddleware',
	# 'django.middleware.csrf.CsrfViewMiddleware',
	# 'django.contrib.authentications.middleware.AuthenticationMiddleware',
	# 'django.contrib.messages.middleware.MessageMiddleware',
	'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

欢迎斧正,that's all
posted @ 2022-04-16 12:11  听雨危楼  阅读(1822)  评论(3编辑  收藏  举报