源于JavaScript的同源策略。即只有 协议+主机名+端口号全部相同,才允许相互访问。如果其中有一个不同,正常情况下浏览器就会把收到的报文丢弃,然后报一个cors policy的错误。
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";
}
要解决这个问题也很简单,只需要告诉浏览器“这个报文你接受吧,我同意你不遵守cors规则”就行了。这样浏览器就不会扔掉数据报错了。
如何告诉浏览器呢?!这时候我们需要在Response报文中加一个响应头Header信息,也就是Access-Control-Allow-Origin:【被允许跨域访问的源】;
方法一: 用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() {
}
}
假设,当我们用方法二重写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记录登陆信息也可以实现了。
同方法二介绍的一样
6.1 首先要了解option请求。
options请求又叫做预检请求(我选用json格式导致请求变成了需要先发送预检请求的复杂请求),在真正的请求发送出去之前,浏览器会先发送一个options请求向服务询问此接口是否允许我访问。也就是说,你的数据请求实际上浏览器发送了两个请求。第一次是preflight,也就是options请求,用于请求验证, 第二次才是我真正需要发送的Post请求。
预检请求的头信息参数含义。
在还没有配置拦截器的情况下用方法二或者方法三解决跨域问题和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秒,在有效时间内,浏览器无须为同一请求再次发起预检请求,请注意,浏览器自身维护了一个最大有效时间
在没有配置拦截器的情况下,一切都正常有序。
6.2 配值拦截器之后的冲突。
用方法二的addCorsMappings方法解决跨域和session;然后配置了拦截器阻拦了option请求(阻拦的原因包括抛出异常或者拦截器return了false)。这个时候你会发现第二次的请求根本不允许发送。这次是浏览器根本不允许发出去,不同于最开始讨论的等到服务器返回了浏览器才扔掉。
原因:重写addCorsMappings()来给Response添加Access-Control-Allow-Origin头部信息的操作也是在拦截器的某一步操作里做的,这就导致Option请求在被拦截后由于不被放行所以连同导致后面给添加Access-Control-Allow-Origin等头部信息的操作也没能执行。所以第二次的正真请求Request里面也不会被允许发送。 这就是很多网友遇到的冲突问题。
解决方法A:直接换第三种的重写filter方法。filter优先于拦截器,这样添加跨域的头部和拦截option就不会搅和在一起了。就算我的option请求被拦截不被放行,也不影响我添加允许跨域和维护session的标志,因为filter优先于其他拦截器,无论后面操作如何都先把重要的头部信息给我添加上了再说。
解决方法B:无论怎么样拦截器都放行option请求,
if(request.getMethod().equals("OPTIONS")){
return true;
}
用方法三重写filter解决上述所有问题,基本没啥异常了,但是这只是针对拦截器里面没有抛出异常的情况。如果我在拦截器里手动抛出了异常阻拦了option请求,而不是正常的return了false阻拦,那么第二次的正真请求同样也不被允许发送出去。
解决方法A:取消手动抛出异常。。
解决方法B:无论怎么样拦截器都放行option请求。
if(request.getMethod().equals("OPTIONS")){
return true;
}