CORS
1.浏览器为什么会有跨域问题
- 同源策略(SOP Same origin policy):是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也是最基本的安全功能,如果缺少同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip,也非同源。
- 跨域(Cross Origin)指浏览器不允许当前页面所在的源去请求另一个源的数据。(要理解当前页面的源是什么?就是当前页面部署的服务器路径)跨域不一定都会有跨域问题。因为跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是与当前页域名相同的路径,这能有效的阻止跨站攻击。因此:跨域问题 是针对ajax的一种限制。
首先第一个问题。这个请求会不会发出去?
实际上是会的,准确来说绝大部分情况下,浏览器虽然发现这个请求跨域了,任然会把这些请求都会发送出去。
等待服务端响应后,浏览器会进行一个校验,通过则可以继续执行后续的操作。否则抛出跨域错误。
2.CORS基本理念
Cross-Origin Resource Sharing
CORS是一套机制,用于浏览器校验跨域请求
它的基本理念是:
只要服务器明确表示允许,则校验通过
服务器明确拒绝或没有表示,则校验不通过
所以就可以产生一个基本认识了:基于这套规则来解决跨域问题,必须要保证服务器那边是自己人。
3.CORS具体校验规则
CORS将请求分为两类: 简单请求、预检请求。
简单请求的校验会宽松一些,预检请求的校验会严格一些
3.1如何区分简单请求、预检请求?
- 简单请求:需要满足以下全部条件
- 请求方法为:
GET
、HEAD
、POST
- HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
- 其他请求字段都是使用浏览器默认的。
- 请求方法为:
- 预检请求:非简单请求
如下代码案例,可以更直观的看出来
// 1 简单请求
fetch('https://douyin.com')
// 2 预检请求,因为修改了头部
fetch('https://douyin.com',{
headers: {
a: 1
}
})
// 3 简单请求 没有修改头部
fetch('https://douyin.com',{
method: 'POST',
body: JSON.stringify({a:1,b:2})
})
// 4 预检请求,修改的content-type超出了3个选项之外
fetch('https://douyin.com',{
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({a:1,b:2})
})
// 5 预检请求
// 如果使用了一些第三方库比如axios
// 那么这些请求就是预检请求了,因为这些第三方库往往会在内部有一些处理
// 比如axios 在发现参数是object时就会自动加上 content-type:application/json
axios.post('https://douyin.com',{a:1,b:2})
在浏览器中也可以直接看出来
带上preflight的就是预检请求,预检请求会在请求之前先发送一个 OPTIONS
方法的请求。后面会细说
3.2简单请求校验规则
当浏览器发现发起的ajax请求是简单请求时,会在请求头中携带一个字段:Origin.
Origin中会指出当前请求属于哪个域(协议+域名+端口)和Request URL进行对比。服务会根据这个值决定是否允许其跨域。
如果服务器允许跨域,需要在返回的响应头中携带下面信息:
Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*(代表任意域名)
Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true。
要想操作cookie,需要满足3个条件:
- 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
- 浏览器发起ajax需要指定withCredentials 为true
- 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名
3.3预检请求校验规则
在正式通信之前,浏览器会先发送一次方法为OPTIONS
的预检请求。浏览器先询问服务器是否允许跨域得到肯定的答复,浏览器才会向服务器发送正式请求,否则抛出CORS异常。
- 浏览器会携带很多信息给服务器
- Origin: 请求源是哪里
- 请求的方法是什么
- 修改了哪些header字段
- 然后服务端需要响应:
- 允许哪些源访问
- 允许哪些请求方法
- 允许修改哪些header字段
- 还可以带上一个缓存
Max-Age
: 86400秒内服务器的规则都是一样的,不用再发送预检请求了。
- 预检请求通过后,浏览器才会发送正式请求。
- 预检请求头字段:
- Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
- Access-Control-Request-Headers:会额外用到的头信息
- 预检请求的响应:
- Access-Control-Allow-Methods:允许访问的方式
- Access-Control-Allow-Headers:允许携带的头
- Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了
4.java后端使用CORS解决跨域问题
4.1 WebMvcConfigurer#addCorsMappings方法
注意注意注意:addMapping()配置应用路径(server.servlet.context-path)是不能进行拦截的,拦截的而是Controller配置确切的路径。一般配置/**即可,即拦截所有Controller配置的路径
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 拦截的路径
// 注意注意注意:addMapping()配置应用路径(server.servlet.context-path)是不能进行拦
// 截的,拦截的而是Controller配置确切的路径
// 一般配置/**即可,即拦截所有Controller配置的路径
registry.addMapping("/**")
// 允许跨域的域名,*:代表所有。允许携带cookie不能为*
.allowedOrigins("*")
// 允许跨域的请求方法
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
// 是否允许跨域携带cookie,为true允许跨域的域名需要指定,不能为*
.allowCredentials(false)
// 本次许可的有效时间,单位秒,过期之前的ajax请求就无需再次进行预检
// 默认是1800s,此处设置1h
.maxAge(3600)
// 允许跨域携带的头信息,*代表所有头。可以添加多个
.allowedHeaders("*");
}
}
4.2 自定义Filter解决跨域
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@WebFilter(urlPatterns = { "/*" }, filterName = "headerFilter")
public class HeaderFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(HeaderFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
// 解决跨域访问报错
// 允许跨域的域名,*:代表所有域名
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
// 允许跨域请求的方法
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
// 本次许可的有效时间,单位秒,过期之前的ajax请求就无需再次进行预检啦
// 默认是1800s,此处设置1h
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
// 允许的响应头
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, client_id, uuid, Authorization");
// 支持HTTP 1.1.
httpServletResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
// 支持HTTP 1.0. response.setHeader("Expires", "0");
httpServletResponse.setHeader("Pragma", "no-cache");
// 编码
httpServletResponse.setCharacterEncoding("UTF-8");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("-----------------cross origin filter start-------------------");
}
@Override
public void destroy() {
logger.info("-----------------cross origin filter end-------------------");
}
}
4.3 @CrossOrigin注解
@CorssOrigin一个注解轻松解决,可以用在类上和方法。
后续小问题
- 跨域上传图片没有问题,但是提交普通表单却遇到了跨域问题。可能的原因是什么?
可能的原因:
上传图片使用的请求头是Content-Type: multipart/form-data
所以上传图片是一个简单请求。
提交表单的时候请求头是Content-Type: application/json
是一个预检请求,
可能是服务器只处理了简单请求的校验。没有处理预检请求的校验。