AJAX同源
问题的来源
在开发过程中, 后台和前端分开开发, 那么就会存在一个问题, 无法使用AJAX访问后台的RESTful接口.
我在63343端口有一个WEB服务器, 8080端口有一个后台服务器.
服务端使用Spring, 有以下的Controller
@RestController
public class GreetingController {
@RequestMapping("/greeting")
public String greeting(HttpServletRequest request, HttpServletResponse response) {
System.out.println("it works");
return "hello world";
}
}
这段代码表示访问http://localhost:8080/greeting 时服务端控制台会输出it works
,而浏览器端会获得hello world
字符串.
然而服务器通过63343端口获取网页, 并在网页中运行以下JS代码访问上述资源时,
$(document).ready(
function () {
$.ajax(
{
url: "http://localhost:8080/greeting",
type: "POST",
async: true,
success: function (data) {
console.log(data)
}
}
);
}
)
会发现获取不了资源, 虽然服务器端控制台输出了it works
, 但是浏览器控制台出现以下警告
XMLHttpRequest cannot load http://localhost:8080/greeting. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:63343’ is therefore not allowed access.
这是因为出于安全的原因, 浏览器禁止AJAX获取不同域的资源.
假设浏览器支持跨域访问, 你现在正在访问A网站, A网站上有一个JS脚本发起AJAX请求访问你的银行网站B提供的接口, 并且设置带上cookie信息, 那么当AJAX请求访问成功后, 会带回你的银行信息. A网站上的脚本就可以使用这些信息, 你的信息就被盗窃了. 所以浏览器是禁止跨域请求的.
何为同源
AJAX的同源策略中的同源是指: 协议相同, 域名相同且端口也相同.
也就是说对于http://www.baidu.com/a.html ,以下网站
网站 | 是否同源 |
---|---|
http://www.baidu.com/b.html | 同源 |
https://www.baidu.com | 非同源,协议不同 |
http://wenku.baidu.com | 非同源,域名不同 |
http://www.baidu.com:8080 | 非同源,端口不同 |
非同源会受到以下限制:
(1) Cookie、LocalStorage 和 IndexDB 无法读取
(2) DOM 无法获得
(3) AJAX 请求不能发送
开发和部署过程时, 我们的SDK服务器和WEB服务器可能在不同的域上, 如果禁止AJAX获取, 那么会导致开发很困难.
CORS
解决AJAX同源带来的麻烦主要有两种解决方法:
1.JSONP
2.CORS
这里我讨论CORS.
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing).
它允许浏览器向源服务器发出XMLHttpRequest请求, 从而克服了AJAX只能同源使用的限制. CORS主要是通过对服务端的Response Header进行更改来实现AJAX跨域.
简单请求和非简单请求
浏览器将CORS请求分为两类:simple request和not-so-simple request.
(1) 请求方法是以下三种方法之一:HEAD, GET ,POST
(2)HTTP 的头信息不超出以下几种字段:Accept, Accept-Language, Content-Language, Last-Event-ID, Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
对于这两种请求, 浏览器的处理略有不同.
具体请参考阮一峰的博客跨域资源共享 CORS 详解
添加Access-Control-Allow-Origin头
在上面的浏览器错误提示中, 警告的原因是No 'Access-Control-Allow-Origin' header is present on the requested resource.
. 对上述Spring的Controller层代码进行更改, 输出Access-Control-Allow-Origin头
@RestController
public class GreetingController {
@RequestMapping("/greeting")
public String greeting(HttpServletRequest request, HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", "http://localhost:63343");
System.out.println("it works");
return "hello world";
}
}
此时发现63343端口的网页已经能够获取8080后台服务器的资源.
发送cookies
上述的代码只是可以跨域访问, 如果需要将http://localhost:8080 域的cookies在http://localhost:63343 域中发送, AJAX代码中需要添加withCredentials:true.
$(document).ready(
function () {
$.ajax(
{
url: "http://localhost:8080/greeting",
type: "POST",
async: true,
xhrFields:{
withCredentials:true
},
success: function (data) {
console.log(data)
}
}
);
}
);
同时Java浏览器端添加response.setHeader("Access-Control-Allow-Credentials", "true");
. 如下所示:
@RestController
public class GreetingController {
@RequestMapping("/greeting")
public String greeting(HttpServletRequest request, HttpServletResponse response) {
//允许跨域请求
response.setHeader("Access-Control-Allow-Origin", "http://localhost:63343");
//允许携带cookies的跨域请求
response.setHeader("Access-Control-Allow-Credentials", "true");
//输出从浏览器端发送过来的cookies
Cookie[] cookies = request.getCookies();
List<Cookie> cookiesList = Arrays.asList(cookies);
for (Cookie cookie : cookiesList) {
System.out.println(cookie.getName() + ":" + cookie.getValue());
}
System.out.println("it works");
//向浏览器返回数据
return "hello world";
}
}
控制台输出以下内容, 携带cookies跨域请求成功.
csrftoken:PDogYrk0PWnWJl4X3hK5c1mzdXJ34ORu
Idea-86dcbfa:b92f1458-3e7e-48a6-9489-75711e010768
Webstorm-1ec7cfac:f6719cf9-0132-4467-986b-c12575906f80
JSESSIONID:3D93362F1D8CD00A8222D27BA8A19112
it works
注意,如果要发送Cookie, Access-Control-Allow-Origin不能设置为*, 需要明确指定与请求网页一致的域名. 同时Cookie仍然遵守同源策略.
More
同时为了响应非简单请求以及浏览器可以通过响应获取更多的头部字段, 我们还需要在服务端添加以下的Response Header.
//非简单请求的预检
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
//预检缓存
response.setHeader("Access-Control-Max-Age", "3600");
//浏览器可以获取的更多的头部
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Authorization");
Spring中的实现
对于以上的设置, 我们不可能在每个controller中都setHeader, 有两种方法:
1. 继承Java EE的Filter
2. 继承Spring的HandlerInterceptor接口
这里使用第二种方法, 第一种和第二种其实是类似的.
public class CORSInteceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setHeader("Access-Control-Allow-Origin", "http://localhost:63343");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Authorization");
response.setHeader("Access-Control-Allow-Credentials", "true");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
并且在spring-mvc.xml中进行配置,拦截所有的.do访问.
<mvc:interceptor>
<!-- 匹配所有的以.do结尾的访问请求-->
<mvc:mapping path="/**/*.do"/>
<bean class="com.hodc.sdk.aspect.CORSInterceptor">
<property name="accessControlAllowOrigin" value="*"/>
</bean>
</mvc:interceptor>
以上方法对于Spring的3.x版本也适用, 而Spring 4.2对CORS添加了更多的支持.详情请见
CORS support in Spring Framework
总结
- 因为安全的原因, AJAX请求执行时, 遵守同源策略, 拒绝跨域访问
- 根据CORS标准, 可以对服务器响应进行更改, 以使浏览器允许跨域访问
- 浏览器将CORS请求分为两种, 一种是简单请求, 一种是非简单请求. PUT和DELETE方法是非简单请求, Content-Type为application/json的也为非简单请求 等.
- Access-Control-Allow-Origin:hostName1,hostName2 表示允许列表中的域上的网页进行跨域访问
- Access-Control-Allow-Credentials: true 表示允许携带Cookie
- Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE 非简单请求中Options预检, 返回允许的方法
- Access-Control-Max-Age, 3600 预检缓存设置
- Access-Control-Allow-Headers, x-requested-with,Authorization 允许浏览器获取更多的响应头部
- Spring MVC中可以通过实现Filter或者实现Inteceptor接口, 来拦截RESTful请求, 添加允许跨域的头部.
- Spring 4.2中对CORS添加了更细粒度的控制注解, 以及XML的 < mvc:cors > 命名空间配置, CORS support in Spring Framework