Sanic框架基础之解决CORS跨域

在前后端分离的情况下,CORS是必然要解决的问题。那什么是CORS呢?
CORS是跨域资源共享的英文单词缩写,CORS是浏览器的一种策略,出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求响应,比如在http://127.0.0.1:8080的页面上向http://127.0.0.1:80通过ajax发出一个post请求,此时80端口后端可以收到http请求,但浏览器会主动拦截响应结果。

那么怎么解决跨域问题呢,有两个方案

  1. 通过后端服务器设置正确的响应头,来告诉浏览器我允许这次跨域,请求浏览器放行
  2. JSONP

本文主要使用第一个方案,所以先简单讲讲JSONP,大家肯定都用过script标签,一些常用的前端框架如jquery,我们可以直接使用对方提供的url,浏览器也不会拦截这些内容,也就是说浏览器对script标签的url是不存在跨域访问控制的,基于这一特性,我们将一个跨域请求包装在script标签内,这样就可以规避CORS的拦截。那么问题在于我通过script请求获得的数据要如何操作,通常的操作是通过定义回调函数名,后端将回调函数和实际返回的数据做字符串拼接,前端收到数据后直接执行前端定义好的方法。

举个栗子 我有一个api http://127.0.0.1:8080 正常响应的内容{'msg': 'hello'} 允许传入一个参数callback 假如callback=printf 那么响应内容变成printf({'msg': 'hello'})
前端域名http://127.0.0.1 现在向访问前面的api 弹出msg对应的内容 那么在前端使用jsonp的方式应该是这样的

<script>
function printf(data) {
    alert(data.msg)
}
</script>
// 通过js代码生成一个这样的<script>标签
<script src="http://127.0.0.1:8080?callback=printf"></script>

所以JSONP的核心就是基于script的特性,通过前端的回调和后端的字符串拼接来避过CORS策略。
JSONP的问题在于scipt标签只能get请求,不能解决post、put等其他请求方式的问题

JSONP是通过另辟蹊径来绕过CORS,那么有没有一种直面CORS请求浏览器放行的策略呢,当然也是有的
我们可以通过响应头来告诉浏览器,这个响应允许某个域接受,如果你碰到了这个域你就放行吧,关于CORS所规范的响应头如下(参考资料

  • Access-Control-Allow-Origin
    参数的值指定了允许访问该资源的外域 URI,服务器可以指定该字段的值为通配符"*"
  • Access-Control-Expose-Headers
    在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。
  • Access-Control-Max-Age
    指定了预检请求的结果能够被缓存多久
  • Access-Control-Allow-Credentials
    指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容
  • Access-Control-Allow-Methods
    用于预检请求的响应,其指明了实际请求所允许使用的 HTTP 方法
  • Access-Control-Allow-Headers
    用于预检请求的响应,其指明了实际请求中允许携带的首部字段

简单说说前面没有聊到的预检概念:
浏览器将请求分成了简单请求和复杂请求两种类别,对于复杂请求,浏览器首先会有一个"预检"的行为,具体而言就是先向url发起一次OPTIONS请求,这个请求被称为预检。后端收到预检请求后,应该响应前面规定的几个响应头,来告诉浏览器后端允许的一些跨域策略。

到现在,要解决跨域问题就比较明朗了,前端不需要任何操作,后端注意两个点即可

  • 实现OPTIONS预检请求响应
  • 在正常请求响应中添加Access-Control-Allow-Origin响应头

回到应用,现在就以Sanic为例,来看看如何解决跨域(逐渐想起标题...)

首先在Sanic的生命周期中,有一点我是比较失望的,流程大致如下:
http请求——Sanic解析request——匹配路由——请求中间件——视图函数——响应中间件——http响应

Sanic在匹配路由中会检测是否存在对应的请求方法,如果没有直接响应405,根本不走后面的中间件了,这意味着你不能使用中间件来实现所有路由表上的预检请求,那么在注册路由的时候,就一定要显式申明OPTIONS或在CBV中实现options方法,之后我们才可以通过中间件来实现功能

@app.middleware("request")
def cors_middle_req(request: Request):
    """路由需要启用OPTIONS方法"""
    if request.method.lower() == 'options':
        allow_headers = [
            'Authorization',
            'content-type'
        ]
        headers = {
            'Access-Control-Allow-Methods':
                ', '.join(request.app.router.get_supported_methods(request.path)),
            'Access-Control-Max-Age': '86400',
            'Access-Control-Allow-Headers': ', '.join(allow_headers),
        }
        return HTTPResponse('', headers=headers)

@app.middleware("response")
def cors_middle_res(request: Request, response: HTTPResponse):
    """跨域处理"""
    allow_origin = '*'
    response.headers.update(
        {
            'Access-Control-Allow-Origin': allow_origin,
        }
    )

前一个请求中间件cors_middle_req用于拦截所有OPTIONS方法,它在设置三个跨域请求头后直接返回HTTPResponse对象,在这个中间件中,告知了浏览器允许的请求方式和请求头,并设置了一个24小时的缓存(86400/3600)时间
后一个响应中间件cors_middle_res用于处理所有响应请求,在响应头中加入Access-Control-Allow-Origin来允许跨域,allow_origin可以改成指定域名
在整个过程中我们使用到了6个规范响应头中最常用的4个,剩下两个响应头大家可以自行去了解用途和适用场景

至此就解决了Sanic的跨域问题,整套流程梳理起来,当你了解了相关规范后其实你会发现并不复杂,任何web框架都可以简单的处理跨域问题。

posted @ 2020-09-20 12:58  秋叶红了  阅读(2658)  评论(0编辑  收藏  举报