0
点赞
收藏
分享

微信扫一扫

【http】跨域 java


同源

说起跨域,首先不得不说同源,这是浏览器的一个策略,规定一个页面只能请求当前源的资源。

举个例子,访问www.baidu.com返回的页面里的js,只能访问www.baidu.com域名下的资源,不能访问其他比如www.qq.com的资源。这么做是出于安全的考虑。如果没有同源策略,那么某些不正当页面可能会访问用户打开的其他页面的资源,访问其他资源时,根据浏览器的规范,cookie等信息会被自动带上,这是不安全的。

如何认定为同源?

协议+域名+端口,三元组。三元组想用才是同源。

跨域

有了同源策略,那么再看下跨域。跨域本质上就是为了绕过同源。

举个例子,一家公司的api服务器域名一般都会不止一个,如果都是自己公司的产品,a域名的页面,无法访问b域名的api接口,这确实很蛋疼。比如,a域名可能是一个h5的前端域名,a域名返回的页面里js代码会调用域名b下的api取数据。

可以看下具体的例子:

前端h5的url:

http://localhost.charlesproxy.com:8081/springmvcxm/hello

直接返回一个h5的页面:

<html>
<body>
<h2>cos test</h2>
</body>
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost.charlesproxy.com:8081/springmvcxml1/api/get');
xhr.send();
</script>
</html>

该页面使用ajax向api服务器发起请求。api地址:

http://localhost.charlesproxy.com:8081/springmvcxml1/api/get

由于两个地址是不同的源,如果不做特殊处理,会被同源策略拦截:

【http】跨域 java_cors

但是,这里不同的源其实是互相信任的,应该允许绕过同源策略的。

浏览器当然也考虑到了这种情况,所以设计了跨域的一些方式,比如说jsonp,还有今天重点介绍的cors协议。

cors

其全称是cross-origin-resource-share,跨域资源共享。浏览器会在跨域请求的header中加入一个origin信息,来表明该请求是一个跨域请求,origin的值就是源。服务器在接收到这个请求后,根据origin信息判定是否允许该跨域请求,如果允许,那么服务器需要在返回的header中设置特殊的值来告诉浏览器,然后浏览器就知道跨域请求成功了。否则,浏览器就会让请求失败。

我们使用chales抓包看下之前那个跨域请求的header:

【http】跨域 java_跨域_02

可以看到,确实有origin字段。

那么服务器如果确定需要处理这个跨域请求,该如何返回?

Access-Control-Allow-Origin:  允许关于的源,可以是请求header里的origin,也可以是*,表示任何origin都可以跨域。浏览器拿到这个header字段时,就知道服务器允许该跨域请求。

下面我们写一个简单的java Filter过滤器来处理cors跨域返回

public class CorsFilter implements Filter {

private List<String> origins;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
origins = new ArrayList<>();

origins.add("localhost");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String origin = httpServletRequest.getHeader("Origin");
System.out.println("origin: " + origin);
if (origin != null && origins.stream().anyMatch(o -> origin.contains(o))) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.addHeader("Access-Control-Allow-Origin", origin);
} else {
System.out.println("no permit");
}
chain.doFilter(request, response);
}

@Override
public void destroy() {

}
}

<filter>
<filter-name>cors</filter-name>
<filter-class>com.liyao.filter.CorsFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

配置好跨域的filter即可。

这里只是做了简单的判定,如果判定通过,就设置Access-Control-Allow-Origin为当前的origin。

 

再重试之前的请求,可以看到跨域成功:

【http】跨域 java_字段_03

另外看下response:

【http】跨域 java_cors_04

有对应的header。

我们还可以试一下设置为*的情况:

【http】跨域 java_跨域请求_05

 

跨域cookie

如果想在跨域请求中带上cookie,那么需要额外设置。比如a域跨域访问b域,即使浏览器已经有了b域的cookie,但是如果不做特殊处理,几百年跨域成功,也无法带上b域的cookie。需要服务端和客户端做处理:
服务端需要多设置一个Access-Control-Allow-Credentials的header为true,并且之前的allow-origin不能为*,只能为具体的某一个域。

另外,客户端ajax请求必须也设置withCredentials属性为true,跨域请求时,浏览器才会带上cookie。

上面的代码做一下修改:

<html>
<body>
<h2>cos test</h2>
</body>
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost.charlesproxy.com:8081/springmvcxml1/api/get');
xhr.withCredentials = true;
xhr.send();
</script>
</html>

filter:

HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.addHeader("Access-Control-Allow-Origin", origin);
httpServletResponse.addHeader("Access-Control-Allow-Credentials", "true");

api:

@RequestMapping("/api/get")
public String get(HttpServletRequest request, HttpServletResponse response) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
System.out.println("cookies: " + Arrays.stream(cookies).map(c -> c.getName() + ":" +c.getValue()).collect(Collectors.joining(", ")));
} else {
Cookie cookie = new Cookie("cname1", "test2");
cookie.setPath("/");
cookie.setMaxAge(30 * 60);
response.addCookie(cookie);
System.out.println("add cookie succeed");
}
return "succeed1";
}

如果有跨域请求带上了cookie,就打印,否则就下发cookie。

【http】跨域 java_cors_06

可以通过抓包看到,cookie已经被带上了。

 

简单与非简单

cors协议针对简单跨域与非简单跨域的处理其实是不同的,上面的例子都是简单跨域请求。

先看下定义:

简单请求必须满足以下两个条件

1)方法是get post head之一;

2)header字段只能如下几个

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值​​application/x-www-form-urlencoded​​​、​​multipart/form-data​​​、​​text/plain​

只要不满足任意一个条件,就是非简单请求。

简单请求,之请求一次,入前面的例子。浏览器第一次发送某一个非简单请求时,会在真正请求之前先发送一个预请求,是一个option方法。

服务端先要先响应这个请求,真正的请求才能被浏览器发出来。并且在有效期内,多不在需要发送预请求了。

下面看一个例子。

我们在跨域请求的header里加入一个新的字段:

X-MY-HEADER。

<html>
<body>
<h2>cos test</h2>
</body>
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost.charlesproxy.com:8081/springmvcxml1/api/get');
xhr.withCredentials = true;
xhr.setRequestHeader("X-MY-HEADER", "ly");
xhr.send();
</script>
</html>

此时该跨域请求变为了非简单请求。

服务端处理额外的header字段,需要多下发一个字段:

Access-Control-Allow-Headers,值为允许的额外的请求header名。显然,我们需要下发:Access-Control-Allow-Headers:X-MY-HEADER。

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String origin = httpServletRequest.getHeader("Origin");
System.out.println("origin: " + origin);
if (origin != null) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.addHeader("Access-Control-Allow-Origin", origin);
httpServletResponse.addHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.addHeader("Access-Control-Allow-Headers", "X-MY-HEADER");
httpServletResponse.addHeader("Access-Control-Max-Age", "60");
} else {
System.out.println("no permit");
}
chain.doFilter(request, response);
}

另外,Access-Control-Max-Age字段表明了预请求的有效期,在这个时间段内,非简单请求不需要再发起预请求。

看个例子:

【http】跨域 java_cors_07

抓包看下:

【http】跨域 java_跨域_08

可以看到,在我们get请求前多了option的预请求。

【http】跨域 java_跨域请求_09

在预请求中,多了一个Access-Control-Request-Header字段,值就是多出的额外的header。

响应中,下发了Access-Control-Allow-Headers字段,告知浏览器额外的header是允许的。

【http】跨域 java_字段_10

后续的get跨域请求中,确实加上了我们的header。

紧接着,再发一次get跨域请求,就不再有option预请求了。

【http】跨域 java_跨域请求_11

直到过了预请求的有效期,之前我们设置的是一分钟。之后会再次出现预请求。

举报

相关推荐

0 条评论