cors跨域之简单请求与预检请求(发送自定义请求头)

引子

前后端分离这个问题,对cors的应用不断增多,暴露出的问题也接踵而至。
正所谓虑一千次,不如去做一次。 犹豫一万次,不如实践一次,本篇主要讨论在发送ajax请求,头部带上自定义token验证验证,暴露出的跨域问题。

先说说定义

CORS:跨来源资源共享(CORS)是一份浏览器技术的规范,提供了 Web 服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,是 JSONP 模式的现代版。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。用 CORS 可以让网页设计师用一般的 XMLHttpRequest,这种方式的错误处理比JSONP要来的好,JSONP对于 RESTful 的 API 来说,发送 POST/PUT/DELET 请求将成为问题,不利于接口的统一。但另一方面,JSONP 可以在不支持 CORS 的老旧浏览器上运作。不过现代的浏览器(IE10以上)基本都支持 CORS。
预检请求(option):在 CORS 中,可以使用 OPTIONS 方法发起一个预检请求(一般都是浏览检测到请求跨域时,会自动发起),以检测实际请求是否可以被服务器所接受。预检请求报文中的 Access-Control-Request-Method 首部字段告知服务器实际请求所使用的 HTTP 方法;Access-Control-Request-Headers 首部字段告知服务器实际请求所携带的自定义首部字段。服务器基于从预检请求获得的信息来判断,是否接受接下来的实际请求。

OPTIONS /resources/post-here/ HTTP/1.1 
Host: bar.other 
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 
Accept-Language: en-us,en;q=0.5 
Accept-Encoding: gzip,deflate 
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 
Connection: keep-alive 
Origin: http://foo.example 
Access-Control-Request-Method: POST 
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

服务器所返回的 Access-Control-Allow-Methods 首部字段将所有允许的请求方法告知客户端。该首部字段与 Allow 类似,但只能用于涉及到 CORS 的场景中。

问题描述

话不多说,先上代码:

前端(ajax库:vue-resource)
        userLogin:function(){
            this.$http({
                method:'post',
                url:'http://localhost:8089/StockAnalyse/LoginServlet',
                params:{"flag":"ajaxlogin","loginName":this.userInfo.id,"loginPwd":this.userInfo.psd}, 
                headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 
                credientials:false, 
                emulateJSON: true                    
            }).then(function(response){
                sessionStorage.setItem("token",response.data);
                this.isActive =false;
                document.querySelector("#showInfo").classList.toggle("isLogin");
            })                 
        }
后端相关配置:
        response.setHeader("Access-Control-Allow-Origin", "http://localhost"); //允许来之域名为http://localhost的请求        
    response.setHeader("Access-Control-Allow-Headers", "Origin,No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, userId, token");
    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); //请求允许的方法
    response.setHeader("Access-Control-Max-Age", "3600");    //身份认证(预检)后,xxS以内发送请求不在需要预检,既可以直接跳过预检,进行请求(前面只是照猫画虎,后面才理解)

关于上面一段代码,是我的用户首次登录认证,生成token令牌,保存在sessionStorage中,供后面调用;需要说明的是,前端服务器地址是:localhost:80,后端服务器地址:localhost:8089,所以前后端涉及到跨域,自己在后端做了相应的跨域设置:response.setHeader("Access-Control-Allow-Origin", "http://localhost"); 所以登录认证,安全的实现了跨域信息认证,后端相应发送回来了相应的token信息。
但获取到token后,想在需要的时候,在请求的头部携带上这个令牌,来做相应的身份认证,所以自己在请求中做了这些改动(有标注),后端没改动,源码:

        checkIdentity:function(){
            let token =sessionStorage.getItem('token');
            this.$http({
                method:'post',
                url:'http://localhost:8089/StockAnalyse/LoginServlet',
                params:{"flag":"checklogin","isLogin":true,"token":token}, 
                headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 
                headers:{'token':token},        //header中携带令牌信息            
                credientials:false, 
                emulateJSON: true                    
            }).then(function(response){
                console.log(response.data);
            })                 
        }   

但实际上在devtools打印了如下错误信息:Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access.仔细想一想,好像,似乎这个问题遇到过,还提过问,确实提过。但这次的设置和上次一样,就在header里多加了一个自定义token,但却报了和上次没有设置headers: {'Content-Type': 'application/x-www-form-urlencoded'}一样的错误信息,于是,不知所措,算了,重头再来,好好百度,研究一下cors跨域。

理论学习

运气不错,找到了一篇文章,文章讲的很细,也找到自己问题的所在:触发 CORS 预检请求。引用原文的话加以自己总结:跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request:似曾相识有没有?诶,对,上面那个错误信息中,就有一个这样陌生的词汇),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。所以跨域请求分两种:简单请求和预检请求。一次完整的请求不需要服务端预检,直接响应的,归为简单请求;而响应前需要预检的,称为预检请求,只有预检请求通过,才有接下来的简单请求。对于那些是简单请求,那些会触发预检请求,文章做了详细的总结,这里列出触发预检请求的条件(不知道脑子为啥会想到那些会触发BFC的条件),不要跑题,原文是这样总结的:

当请求满足下述任一条件时,即应首先发送预检请求:
使用了下面任一 HTTP 方法:
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:
Accept
Accept-Language
Content-Language
Content-Type (but note the additional requirements below)
DPR
Downlink
Save-Data
Viewport-Width
Width
 Content-Type 的值不属于下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain

问题分析

所以,再来看自己两次犯错(第一次是没有设置:headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 第二次是设置自定义header,headers:{'token':token}。很巧,有没有,一次少,一次多,都点燃了导火索),其实都是触发了预检请求。对于第一次的错误,很好解决,增加headers: {'Content-Type': 'application/x-www-form-urlencoded'},就解决了,关于Conten-Type的几种取值,你需要知道的。但对于第二个错误,好像没法向第一种那样,将预检请求转变为简单请求,所以,只有寻找方法怎么在后端实现相应的预检请求,来返回一个状态码2xx,告诉浏览器此次跨域请求可以继续。所以注意力转向后端。
关于JAVA实现预检请求,基本都是采用过滤器,不要问我为什么不是监听器或者拦截器(我就是个伪全栈,就不要相互为难了,自己百度之),自定义(copy)了一个filter,并在web.xml中进行了设置。源码:

Filter接口实现部分:
package stock.model;
import java.io.IOException;   
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;    
import org.apache.commons.httpclient.HttpStatus;   //这里需要添加commons-httpclient-3.1.jar
public class CorsFilter implements Filter {     //filter 接口的自定义实现
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        response.setHeader("Access-Control-Allow-Origin", "*");           
        String token = request.getHeader("token");
        System.out.println("filter origin:"+token);//通过打印,可以看到一次非简单请求,会被过滤两次,即请求两次,第一次请求确认是否符合跨域要求(预检),这一次是不带headers的自定义信息,第二次请求会携带自定义信息。
        if ("OPTIONS".equals(request.getMethod())){//这里通过判断请求的方法,判断此次是否是预检请求,如果是,立即返回一个204状态吗,标示,允许跨域;预检后,正式请求,这个方法参数就是我们设置的post了
          response.setStatus(HttpStatus.SC_NO_CONTENT); //HttpStatus.SC_NO_CONTENT = 204
          response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS, DELETE");//当判定为预检请求后,设定允许请求的方法
          response.setHeader("Access-Control-Allow-Headers", "Content-Type, x-requested-with, Token"); //当判定为预检请求后,设定允许请求的头部类型
          response.addHeader("Access-Control-Max-Age", "1");  // 预检有效保持时间                       
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {     
    }
}
web.xml配置部分
<filter>
<filter-name>cors</filter-name>
<filter-class>stock.model.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>   
关于Access-Control-Max-Age

最近又开始写java,再回来看这个,发现当时Access-Control-Max-Age设置了1.其实这样写有很大问题,因为每个复杂请求都会发两次。显然这样是当代所不能接受的,所以Max-Age的值适合设的大一些,具体多大很业务需求相关。另外Access-Control-Max-Age不是针对请求域名有效的,是请求的完成路径有效的,比如第一次发出www.exanple.com/api/corsGet,会产生一次options请求和一次post请求,然后我再请求一次,这时没有预检请求了,只有post请求。但再发送一次www.exanple.com/api/corsSave请求,会发现又产生了一次options请求和一次post请求,所以Access-Control-Max-Age不是针对相同的origin有效,而是针对相同的requestUrl有效。很重要哦。

结论

当在后端实现添加上面的源码后,皆大欢喜,问题得以解决,补上失败和成功,自己截下的两张请求响应图

仔细看请求响应失败发起响应那张图,在General的数据集中,可以看到方法是options,而非代码指定的post请求,所以这是一次浏览器发出的一次预检请求,让服务器确认此IP是否有访问的权限,如果有,服务器需要返回一个2xx的状态码给浏览器。紧接着再发起一次简单请求。如下面在devtools中的截取图片(为了对比清除,我把两次分别截取,做了拼接,因为不会做动态图)。可以看到同一个post请求,实际上产生了两次网络连接。


但关于cors,要去探索的,还有很多很多,所以遵循革命语录:实践(有时也可以是时间)是检验真理的唯一标准,是没有错的。后续有新的收获,再补充。

转载于:https://my.oschina.net/incess/blog/3060511

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 uni-app 中,解决跨域请求的方法有以下几种: 1. 使用 jsonp 请求 2. 使用代理,在本地启动一个服务器来作为代理,发起跨域请求时以本地服务器为中介,避免跨域的限制 3. 在服务器端配置跨域,如果服务器端允许跨域,则可以在服务器端设置 HTTP header,允许跨域访问。例如,在 Express.js 中可以使用 cors 中间件来实现。 如果你想深入了解跨域请求的原理,你可以参考以下资料: - 跨域资源共享 (CORS):https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS - HTTP 访问控制 (CORS):https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS - 跨域请求简单请求预检请求:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS/Simple_requests_and_preflight_requests ### 回答2: 在uni-app中请求跨域解决方法有以下几种: 1. 使用uni.request方法发送请求时,在请求header中添加"Access-Control-Allow-Origin"字段,允许指定的域名或通配符"*",例如: ``` uni.request({ url: 'http://example.com/api', method: 'GET', header: { "Access-Control-Allow-Origin": "*" }, success: function(res) { console.log(res.data); } }); ``` 2. 在uni-app的开发配置文件manifest.json中,添加"networkTimeout"字段来配置跨域请求的超时时间,例如: ``` "networkTimeout": { "request": 10000, "connectSocket": 10000, "uploadFile": 10000, "downloadFile": 10000 } ``` 3. 使用uni.request方法发送请求时,设置withCredentials为true,开启跨域请求携带cookie功能。但需要注意服务器端也需要设置响应头允许接收cookie,例如: ``` uni.request({ url: 'http://example.com/api', method: 'GET', withCredentials: true, success: function(res) { console.log(res.data); } }); ``` 4. 在uni-app的开发配置文件manifest.json中,添加"filters"字段配置全局过滤器来处理跨域请求,例如: ``` "filters": { "request": { "origin": "*", "x-requested-with": "*", "content-type": "application/json" } } ``` 以上是一些常用的uni-app中处理cors跨域请求的方法,根据具体情况选择适合的解决方案。 ### 回答3: 在UniApp中解决跨域问题可以通过配置服务器设置CORS跨域资源共享)来实现。 首先,在后端服务器上设置CORS。在服务器返回响应时,需要在响应头中添加相关的CORS头信息,允许前端跨域访问该接口。常见的CORS头信息包括:Access-Control-Allow-Origin、Access-Control-Allow-Headers、Access-Control-Allow-Methods等。具体配置方法可以根据后端服务器的不同而有所差异,请根据服务器文档进行设置。 其次,在UniApp中发起请求时,需要在请求头中添加Origin字段表示请求的源地址。同时,UniApp默认会在请求头中自动添加Referer字段来表示访问来源,可根据需要进行调整。 另外,UniApp还提供了特定的API来设置请求的配置,可以通过修改uni.request方法的header参数或通过配置uni.request.defaults.header来添加请求头,以适应不同的CORS设置。 需要注意的是,前端涉及到跨域请求时,在接口上必须进行预检请求(Preflight Request)的检测,包括发送一个OPTIONS请求以校验服务器是否允许当前请求。 总结起来,解决UniApp中的CORS跨域问题,需要在后端服务器进行相关CORS设置,并在UniApp中添加请求头信息,确保请求能够正常跨域访问后端接口。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值