基本概念
基于浏览器的基本安全功能(同源策略),只允许在同一域内的资源交互,一般而言同域指的是:相同协议(protocol)、相同主机(host)、相同端口(port)。如果请求跨域则可能出现一下几种情况。
- 无法读取Cookie、LocalStorage 和 IndexDB
- DOM 和 JS 对象无法获取
- Ajax请求失败
问题
下面结合笔者实践中遇到的一个问题来简要说明如何解决请求跨域。
场景
ajax提交psot请求,默认contentType为表单提交格式(application/x-www-form-urlencoded)请求可正常相应,当设置contentType为“application/json时”,请求头出现Provisional headers are shown
寻找问题
Provisional headers are shown出现有以下几种情况
- 请求被浏览器插件拦截
- 服务器出错或者响应超时
- 请求跨域被浏览器拦截
接下来慢慢分析的问题属于哪一种,本地chrome有插件,但在关闭插件后仍提示,第一种情况排除。请求对应的服务端逻辑几近于无只有接收参数并打印,且在请求过程中未报错,所以排除第二种情况。上面提到的“Ajax请求提示Provisional headers are shown”,正好和第三种情况对应。
到这里就知道问题是请求跨域导致的,但为什么在contentType默认时未发生跨域,请带着疑问往下看。
在请求跨域时会向服务端先发送PreFlight(option请求)验证是否能接收当前请求类型、是否被允许的域、是否需要Credentials(认证信息)。但有特殊情况需要注意,在请求是simple request(简单请求)时,则在跨域前不会发送PreFlight。
简单请求
- 请求类型必须是get、post、head其中之一
- HTTP Headers不能自定义
- contentType必须是application/x-www-form-urlencoded、multipart/form-data、text/plain其中之一
这就很好的说明了问题的原因。post请求设置contentType为application/json使请求成为非简单请求,导致PreFlight验证未通过。
解决方案
非简单请求PreFlight验证未通过,会导致请求失败。那反之让PreFlight验证通过就能很好的解决跨域的问题。刚刚提到PreFlight验证请求类型、域、认证信息,在http协议中由以下几个属性控制:
- Access-Control-Allow-Origin:表示请求允许的域,设置为“*”即表示允许所有host发来的请求
- Access-Control-Allow-Credentials:表示请求是否携带cookie,默认不携带。值只能为true,如果不要求携带cookie可删除此属性配置
- Access-Control-Allow-Methods:表示支持的请求类型,字符串请求类型之间逗号隔开。如:”POST,GET,PUT“
- Access-Control-Max-Age:表示本次验证的有效期,整型单位为秒
- Access-Control-Allow-Headers:表明服务器支持的所有请求头字段
鉴于笔者是后端开发,本文只讨论后端的处理方式
-
@CrossOrigin注解,Spring4.2及其以上版本可使用该注解,可以是类级也可以是方法级,注解打在controller类上则说明该controller下的所有方法都支持跨域,如果打在方法上则说明该方法支持跨域,示例代码如下
@Controller @RequestMapping(value = "test") @CrossOrigin public class TestController { @CrossOrigin @RequestMapping(value = "/test") public String test() { return "success"; } }
-
注解最大范围只能解决当个controller的跨域问题,如果想解决整个项目的跨域问题,我们还有更优雅的处理方式,那就是web三大组件之一的filter,通过自定义filter来返回上述请求头从而解决跨域,示例代码如下
@WebFilter(filterName = "CorsFilter") @Configuration public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Allow-Origin","*"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); chain.doFilter(req, res); } }