一:什么是跨域问题
源于JavaScript的同源策略。即只有 协议+主机名+端口号全部相同,才允许相互访问。如果其中有一个不同,正常情况下浏览器就会把收到的报文丢弃,然后报一个cors policy的错误。
二:出现情况(前后端分离开发vue+springboot)
我在本地用nginx服务器挂了一个端口127.0.0.1:10086 用来提供 静态页面;
而静态页面中需要用ajax请求127.0.0.1:8080的后端接口获取数据。
- 前端请求
axios.post("http://127.0.0.1:8080/test/hello").then(function(response){
console.log(response);
},function(error){
console.log(error);
})
- 后端响应
@PostMapping("/test/hello")
public AjaxResponse testhello(){
log.info("hello");
return "hello";
}
- 当前端页面按钮触发ajax请求后,会有cors policy的报错。
三:报错原因
- 首先明确,这个页面发起的ajax请求是被我的后端请求给接受到了的,进入了Controller里面处理了逻辑业务;而且最后服务器是把Response响应给返回了!
- 那么为什么还会报错呢?!原因在于cors policy同域策略,通俗地讲就是浏览器在接受到这个Response的时候才反应过来,这个Response如果浏览器他接收的话他就是违规的,所以浏览器选择遵守规定放弃了这个Response体,并弹出cors policy错误警告。
四:解决跨域问题(springboot后端解决)
-
要解决这个问题也很简单,只需要告诉浏览器“这个报文你接受吧,我同意你不遵守cors规则”就行了。这样浏览器就不会扔掉数据报错了。
-
如何告诉浏览器呢?!这时候我们需要在Response报文中加一个响应头Header信息,也就是Access-Control-Allow-Origin:【被允许跨域访问的源】;
- 在springboot有几种处理的方法,但要知道所有方法的最后其实都是给Response加上了一个Header信息,告诉浏览器不要扔掉信息,都是这个原理。
-
方法一: 用httpServletResponse封装好的类直接给返回头加上这个信息(此方法用于理解…)。
@PostMapping("/hello") public AjaxResponse testhello(HttpServletResponse response){ response.setHeader("Access-Control-Allow-Origin","http://127.0.0.1:10086"); log.info("hello"); return AjaxResponse.success("hello"); }
-
方法二:实现WebMvcConfigurer接口,然后重写addCorsMappings(CorsRegistry registry)方法。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { String mapping = "/**"; // 所有请求,也可配置成特定请求,如/api/** String origins = "http://localhost:10086"; // * 表示所有来源,也可以配置成特定的来源才允许跨域,如http://www.xxxx.com String methods = "*"; // 所有方法,GET、POST、PUT等 Boolean allowCredentials = true; // 表示是否允许携带cookie //解决Session问题 long maxAge =30 * 1000; //表示探测请求通过后,保持认证的时间。 //这个探测请求是针对复杂请求设计的,最后面说明 registry .addMapping(mapping) .allowedOrigins(origins) .allowedMethods(methods) // .allowCredentials(true) // .maxAge(maxAge) ; } }
实际上只是针对跨域问题,只需要配置好mapping origins 和methods三个参数就好了,配置好了以后再次用前端向后端发起跨域请求,就会发现不再报错了。
这个方法二有个弊端,就是如果你还配置了拦截器,那么就会产生冲突。最后面记录我的填坑过程。
-
方法三:
实现Filter接口重写doFilter方法,过滤器中添加头部
@WebFilter(filterName = "MyFilter",urlPatterns = "/*")
public class CorsFilterConfig implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
//指定允许其他域名访问
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
//前端携带cookie时,也就是写了 {withCredentials: true},后端不能用通配符,
//得用"Access-Control-Allow-Origin",httpServletRequest.getHeader("origin")
//响应头设置
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
//允许携带cookie
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
//响应类型
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
//option验证后时间
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
五:解决Session维护问题:
5.1用方法二(重写方法)解决:
-
假设,当我们用方法二重写addCorsMappings(CorsRegistry registry)后,能够跨域请求了。
会出现一个问题。那就是ajax的请求是不会自动携带cookie的,意味着后端无法凭借前端cookie记录的JSessionId来获取后端存储的Session。
通俗讲就是每一次ajax请求后端都会视作为不同的用户,每次都会给这个ajax请求的Response消息头里设置一个新的“Set-Cookie:JSESSIONID=44AA8F3EFF388BD7ADE0551BC33FADB”。
-
如何解决?很简单,让前端页面发起ajax请求的时候让它带上所有cookie不就好了!同时我们返回的Response里面需要加上一个头信息"Access-Control-Allow-Credentials:true"。
-
具体步骤如下
-
第一步让前端的ajax部分加上withCredentials: true这个参数,保证前端会带上cookie。
比如我这里用的axios就这样写
axios.post("http://127.0.0.1:8080/test/hello", {withCredentials: true}).then(function(response){
console.log(response);
},function(error){
console.log(error);
})
或者用Jquery封装的ajax这样加
$.ajax({
url: "http://localhost:8080/orders",
type: "GET",
xhrFields: {
withCredentials: true
},
success: function (data) {
render(data);
}
})
-
第二步,在后端addCorsMappings(CorsRegistry registry)方法里添加 allowCredentials(true)。
(注意,前端授权了cookie发送,也就是配置了withCredentials: true,那么后端allowedOrigins(origins)这个参数origins就不能写 * 匹配所有,而且allowCredentials(allowCredentials)必须是true。)
我理解意思就是前端把cookie全带过来了,后端得指定要求需要的一个域里面的cookie,毕竟不能全要。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
String mapping = "/**";
String origins = "http://localhost:10086"; //写成String origins = "*"; 错误,这时候需要指定明确的域
String methods = "*";
Boolean allowCredentials = true; // 表示是否允许携带cookie //解决Session问题
registry
.addMapping(mapping)
.allowedOrigins(origins)
.allowedMethods(methods)
.allowCredentials(allowCredentials)
;
}
}
这样你会发现session就被同步了,每次Ajax的请求的Response都不会被重新赋值新的JSessionId了。用Session记录登陆信息也可以实现了。
5.2用方法三(filter)解决:
同方法二介绍的一样
- 用Filter进行实现跨域并维护session,前端同样要在发送请求的时候添加{withCredentials: true}参数,
- 后端的filter中也要在response中添加头部信息 httpServletResponse.setHeader(“Access-Control-Allow-Credentials”, “true”)。
六:所谓的addCorsMappings与拦截器冲突问题
- 如果用第二种方法解决跨域和session维护问题,而且你后端还要配值拦截器,那么就有可能出现问题,就是很多网友说的冲突问题。
- 如果在拦截器中抛出异常或者拦截器return了false,这个时候你会发现第二次的请求根本不允许发送。
- 原因:重写addCorsMappings()来给Response添加Access-Control-Allow-Origin头部信息的操作也是在拦截器的某一步操作里做的,这就导致Option请求(Option请求下文有简单介绍)由于某种拦截原因在被拦截后由于不被放行,所以连同导致后面给添加Access-Control-Allow-Origin等头部信息的操作也没能执行。所以浏览器还是会遵守规则不接受你的报文。
- 拦截原因:被拦截而不被放行的原因可能有是人为或非人为地在拦截器抛出异常;或者拦截器那里你的逻辑返回了false导致不放行(比如说下面介绍的,你的请求用的是复杂请求,而又没对复杂请求中的option方法设计好逻辑,导致不被放行)。
- 解决方法:换方法三,重写filter方法。filter优先于拦截器,这样添加跨域的头部和拦截器就不会搅和在一起了。就算你拦截器逻辑中不被放行,但我回复的报文已经提前加上相关头部信息了。(这种情况仍然解决不了option请求的问题,仍需要额外处理)
七:拦截器中对option请求的处理。
7.1 首先要了解option请求。
-
options请求又叫做预检请求(我选用json格式导致请求变成了需要先发送预检请求的复杂请求),在真正的请求发送出去之前,浏览器会先发送一个options请求向服务询问此接口是否允许我访问。也就是说,你的数据请求实际上浏览器发送了两个请求。第一次是preflight,也就是options请求,用于请求验证, 第二次才是我真正需要发送的Post请求。只有第一个OPTION正常返回了,第二次请求才会正常发送。
-
预检请求的头信息参数含义。
在还没有配置拦截器的情况下用方法二或者方法三解决跨域问题和session维护问题后,预检请求有下列正常参数。
-
预检请求发出Request头部信息中有两个参数:
Access-Control-Request-Headers: content-type //告知服务器,实际请求携带自定义请求首部字段Content-Type Access-Control-Request-Method: POST //告知服务器,实际请求将使用 POST 方法
-
预检请求返回的Response头信息中的重要参数:
Access-Control-Allow-Origin: http://localhost:10086 //表明服务器允许跨域,允许的域是http://localhost:10086 Access-Control-Allow-Methods: POST, GET, OPTIONS // 表明服务器允许客户端使用 POST,GET 和 OPTIONS 方法发起请求 Access-Control-Allow-Headers: Content-Type // 表明服务器允许请求中携带字段Content-Type Access-Control-Max-Age: 1800 //这个预检请求认证通过的有效期为1800秒,在有效时间内,浏览器无须为同一请求再次发起预检请求,请注意,浏览器自身维护了一个最大有效时间
-
在没有配置拦截器的情况下,一切都正常有序。
7.2 你的拦截器或filter的逻辑极大概率不会放行OPTION请求,导致异常。
- 配置了拦截器就意味着必然会拦截到第一次的option请求。因为option请求是不会携带参数信息,例如cookie,token那些个,数据处理可能就出现问题。所以这个时候你的逻辑没有对option进行特殊处理的话,就有极大可能不放行此次option请求。
- 这里无论是用addCorsMappings(CorsRegistry registry) 还是用filter来处理上述的session维护问题和跨域问题,都可能会因为option请求导致异常。但是异常出现的原因是不一致的。
- 用addCorsMappings(CorsRegistry registry)解决跨域和session问题,然后配置了拦截器阻拦了option请求。这个时候,是因为OPTION请求没加上跨域头部信息,所以你会发现第二次的请求根本不允许发送。这次是浏览器根本不允许发出去,不同于最开始讨论的等到服务器返回了浏览器才扔掉。
检查option请求返回的Response体,发现添加的允许跨域的几个头部信息没有被添加上。
- 用filter处理session和跨域问了后,拦截OPTION请求,同样第二次的正真请求同样也不被允许发送出去。原因是OPTION没有正确的状态码,本来也是这样,OPTION不被放行,而第二次请求必然不会发送(不同的是,这里跨域问题是解决的了)。
前端报错如下,没有正确的status。
- 通用的解决方法:无论怎么样拦截器或者filter都放行option请求。
比如我配置的拦截器:
public class Login_Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
if(request.getMethod().equals("OPTIONS")){
log.info("OPTIONS直接放行");
return true;
}
HttpSession session = request.getSession();
Object userInfo = session.getAttribute("userInfo");
if (userInfo==null) {
log.info("被拦截++,:session.getId()ID为:"+session.getId());
return false;
}else {
log.info("被放行++,:session.getId()ID为:"+session.getId());
return true;
}
}
}
七:总结:
- springboot后端处理跨域问题有两个主要方法:
- 重写addCorsMappings(CorsRegistry registry)
- 实现Filter接口重写doFilter方法
- 如果还需要用拦截器来实现一些业务逻辑,比如我在拦截器里面用session判断是否登陆,而且还要手动抛出异常的话。那么最好用实现Filter接口重写doFilter()的方法。
- 如果你的请求中包含还复杂请求,那么最好还要针对option请求进行放行操作。原因已经在上面记录了。
- 全剧终,希望对你我有帮助。