最近在做一个项目,需要用到跨域访问,在这里将解决问题的过程与大家分享一下。
JavaScript出于安全方面的考虑,使用同源策略,不允许跨域调用其他页面的对象。但在安全限制的同时也给注入iframe或是ajax应用上带来了不少麻烦。
举例如下:
URL1 | URL2 | 说明 | 是否允许通信 |
---|---|---|---|
http://www.a.com/a.js | http://www.a.com/b.js | 同一域名下 | 允许 |
http://www.a.com/lab/a.js | http://www.a.com/script/b.js | 同一域名下不同文件夹 | 允许 |
http://www.a.com:8000/a.js | http://www.a.com/b.js | 同一域名,不同端口 | 不允许 |
http://www.a.com/a.js | https://www.a.com/b.js | 同一域名,不同协议 | 不允许 |
http://www.a.com/a.js | http://127.0.0.1/b.js | 域名和域名对应ip | 不允许 |
http://www.a.com/a.js | http://script.a.com/b.js | 主域相同,子域不同 | 不允许 |
http://www.a.com/a.js | http://a.com/b.js | 同一域名,不同二级域名(同上) | 不允许(cookie这种情况下也不允许访问) |
http://www.cnblogs.com/a.js | http://www.a.com/b.js | 不同域名 | 不允许 |
对于跨域的问题,在网上也有很多解决方案,很多人都建议使用jsonp
,这种方式我认为需要对服务端进行改造,代价稍高,所以在这篇博客中我们不讨论,有兴趣可以查看相关文档,今天我们主要来讨论如何使用响应头的方式来解决跨域的问题。
首先我们先来看看,跨域到底是个什么问题,我显现编写一个服务端的程序用于测试(使用Servlet
),
package com.gujin.web;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CrossDomain extends HttpServlet
{
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
PrintWriter writer = response.getWriter();
writer.write("{\"name\":\"jianggujin\"}");
writer.flush();
writer.close();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doGet(request, response);
}
}
在web.xml
中进行相关配置使其生效:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>web</display-name>
<servlet>
<description></description>
<display-name>CrossDomain</display-name>
<servlet-name>CrossDomain</servlet-name>
<servlet-class>com.gujin.web.CrossDomain</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CrossDomain</servlet-name>
<url-pattern>/cd</url-pattern>
</servlet-mapping>
</web-app>
配置本地host为:www.jianggujin.com
,使用浏览器访问http://www.jianggujin.com/web/cd
测试一下,浏览本期会显示:{"name":"jianggujin"}
。
接下来我们在编写一个本地的网页:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>跨域测试</title>
<script type="text/javascript">
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4) {
console.info(xmlHttp.responseText);
}
};
xmlHttp.open("GET", "http://www.jianggujin.com/web/cd", true);
xmlHttp.send();
</script>
</head>
<body>
</body>
</html>
访问页面,浏览器控制台显示如下信息(注:显示新格式可能会不同,以具体环境为准): [Web浏览器] "XMLHttpRequest cannot load http://www.jianggujin.com/web/cd. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:8020' is therefore not allowed access." /mui/crossdomain.html (0)
这就是跨域导致的问题,同样的地址我们使用浏览器可以直接访问,但是使用ajax请求的时候,由于同源策略就被限制了,请求不被允许。通过错误信息,我们也可以得到一点解决问题的消息,缺少:Access-Control-Allow-Origin
头。
在w3c中对该问题也做了描述,并列举了一些头,下述内容摘自原文:
5 Syntax
This section defines the syntax of the new headers this specification introduces. It also provides a short description of the function of each header.
The resource processing model section details how resources are to use these headers in a response. Likewise, the user agent processing model section details how user agents are to use these headers.
The ABNF syntax used in this section is from HTTP/1.1. [HTTP]
HTTP/1.1 is used as ABNF basis to ensure that the new headers have equivalent parsing rules to those introduced in that specification.
HTTP/1.1 currently does not make leading OWS implied in header value definitions but that form is assumed here.
5.1 Access-Control-Allow-Origin Response Header
The Access-Control-Allow-Origin header indicates whether a resource can be shared based by returning the value of the Origin request header, “*”, or “null” in the response. ABNF:
Access-Control-Allow-Origin = “Access-Control-Allow-Origin” “:” origin-list-or-null | “*”
In practice the origin-list-or-null production is more constrained. Rather than allowing a space-separated list of origins, it is either a single origin or the string “null”.5.2 Access-Control-Allow-Credentials Response Header
The Access-Control-Allow-Credentials header indicates whether the response to request can be exposed when the omit credentials flag is unset. When part of the response to a preflight request it indicates that the actual request can include user credentials. ABNF:
Access-Control-Allow-Credentials: “Access-Control-Allow-Credentials” “:” true
true: %x74.72.75.65 ; “true”, case-sensitive
5.3 Access-Control-Expose-Headers Response HeaderThe Access-Control-Expose-Headers header indicates which headers are safe to expose to the API of a CORS API specification. ABNF:
Access-Control-Expose-Headers = “Access-Control-Expose-Headers” “:” #field-name
5.4 Access-Control-Max-Age Response HeaderThe Access-Control-Max-Age header indicates how long the results of a preflight request can be cached in a preflight result cache. ABNF:
Access-Control-Max-Age = “Access-Control-Max-Age” “:” delta-seconds
5.5 Access-Control-Allow-Methods Response HeaderThe Access-Control-Allow-Methods header indicates, as part of the response to a preflight request, which methods can be used during the actual request.
The
Allow
header is not relevant for the purposes of the CORS protocol. ABNF:Access-Control-Allow-Methods: “Access-Control-Allow-Methods” “:” #Method
5.6 Access-Control-Allow-Headers Response HeaderThe Access-Control-Allow-Headers header indicates, as part of the response to a preflight request, which header field names can be used during the actual request. ABNF:
Access-Control-Allow-Headers: “Access-Control-Allow-Headers” “:” #field-name
5.7 Origin Request HeaderThe Origin header indicates where the cross-origin request or preflight request originates from. [ORIGIN]
5.8 Access-Control-Request-Method Request Header
The Access-Control-Request-Method header indicates which method will be used in the actual request as part of the preflight request. ABNF:
Access-Control-Request-Method: “Access-Control-Request-Method” “:” Method
5.9 Access-Control-Request-Headers Request HeaderThe Access-Control-Request-Headers header indicates which headers will be used in the actual >request as part of the preflight request. ABNF:
Access-Control-Request-Headers: “Access-Control-Request-Headers” “:” #field-name
看不懂?不着急,我们急需解决问题,上面的一大串内容的意思就是提供了这些响应头用具解决ajax的跨域问题。当ajax进行跨域访问时,浏览器首先会进行一次OPTION请求判断该请求是否被循序跨域,当响应结果为允许访问时,再进行真正的请求。
下面我们对Servlet进行修改使其允许跨域:
package com.gujin.web;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CrossDomain extends HttpServlet
{
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setHeader("Access-Control-Allow-Origin", "*");
super.service(request, response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
PrintWriter writer = response.getWriter();
writer.write("{\"name\":\"jianggujin\"}");
writer.flush();
writer.close();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doGet(request, response);
}
}
再次打开网页,观察控制台输出:
通过输出,我们发现这时候已经可以正常访问了,这样我们就满足了吗?如果有Cookie之类的信息也可以正常提交吗?我们来继续测试,在此更改服务端代码:
package com.gujin.web;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CrossDomain extends HttpServlet
{
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setHeader("Access-Control-Allow-Origin", "*");
super.service(request, response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// 使用Session
request.getSession();
Cookie[] cookies = request.getCookies();
System.out.println("========================================");
if (cookies != null)
{
for (Cookie cookie : cookies)
{
System.out.println(cookie.getName() + "=" + cookie.getValue());
}
}
PrintWriter writer = response.getWriter();
writer.write("{\"name\":\"jianggujin\"}");
writer.flush();
writer.close();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doGet(request, response);
}
}
我们先用浏览器直接访问地址,访问两次,观察控制台输出:
然后我们在通过网页访问看看结果:
两次结果对比,我们可以发现,虽然我们解决了跨域,但是Cookie信息并没有提交,当然了我们也是有办法解决的:
服务端:
package com.gujin.web;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CrossDomain extends HttpServlet
{
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// 不可以直接使用*
response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:8020");
response.setHeader("Access-Control-Allow-Credentials", "true");
super.service(request, response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// 使用Session
request.getSession();
Cookie[] cookies = request.getCookies();
System.out.println("========================================");
if (cookies != null)
{
for (Cookie cookie : cookies)
{
System.out.println(cookie.getName() + "=" + cookie.getValue());
}
}
PrintWriter writer = response.getWriter();
writer.write("{\"name\":\"jianggujin\"}");
writer.flush();
writer.close();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doGet(request, response);
}
}
网页:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>跨域测试</title>
<script type="text/javascript">
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4) {
console.info(xmlHttp.responseText);
}
};
//设置为true
xmlHttp.withCredentials = true;
xmlHttp.open("GET", "http://www.jianggujin.com/web/cd", true);
xmlHttp.send();
</script>
</head>
<body>
</body>
</html>
这个时候我们再访问网页就会达到我们想要的结果了。
到这里,我们基本上就解决了跨域的问题了,在实际应用中,我们可能还会遇到其他的问题,比如请求头不允许跨域等,解决方法都是类似的,我们只要添加相应的头信息就可以了,因为测试的原因,响应头信息我是直接放在了Servlet中进行处理,在实际应用中,这样做就很麻烦了,我们可以编写一个过滤器用于跨域访问,在这里就不贴代码了,大家可以自己思考,对该问题进行为你善解决。