跨域问题是前后端开发过程中经常会碰到的问题,那么什么是跨域,为什么前端会出现跨域问题。要了解跨域,先要说说同源策略。同源策略/SOP(Same origin policy)是一种约定,是由 Netscape 公司提出的一个著名的安全策略。它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所有支持 JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同。当页面在执行一个脚本时会检查访问的资源是否同源,如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。同源策略一般又分为以下两种
- OM同源策略:禁止对不同源页面DOM进行操作。主要场景是iframe跨域的情况,不同域名的iframe限制互相访问。
- XmlHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。
跨域原理
跨域:指的是从一个域名去请求另外一个域名的资源。即跨域名请求!跨域时,浏览器不能执行其他域名网站的脚本,是由浏览器的同源策略造成的,是浏览器施加的安全限制。跨域的严格一点来说就是只要协议,域名,端口有任何一个的不同,就被当作是跨域。那么如果一个页面调用跨域请求,实际到底发生了什么,请求到底有没有发出去?跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。例如下面的表达 action 中请求百度的地址
<from action="baidu.com">
// you form filed
</from>
这个表单提交后,剩余的操作就交给了action里面的域 baidu.com,本页面的逻辑和这个表单没啥关系,由于不关系请求的响应,所以浏览器认为是安全的。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。在前端中并不是所有的资源都必须同源,< script >、< img >、< iframe >、< link >
这些包含 src
属性的标签可以加载跨域资源。但浏览器限制了JavaScript的权限,所以就算这些标签可以获取跨域资源,但JavaScript却不能读、写加载的其内容。常见的解决方案分为三种:
- Nginx代理(前端实现)
- JSONP(前端实现)
- 后台设置(后端实现)
Nginx就不多说了,通过服务代理的方式可以随意请求到任何其他服务资源,现在主流的前后端分离技术都是通过Nginx实现的,所以现在基本上不会出现跨域问题,下面主要了解一下不使用Nginx的情况下,怎么解决跨域问题。
JSONP方式
JSONP(JSON with Padding)不是一种新技术,而是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。其原理主要是利用< script >
标签src属性中的链接可以访问跨域的JS脚本的原理。使用JSONP时需要后端配合,服务端不再返回JSON格式的数据,而是返回一段可执行的JavaScript代码,在src中进行了调用,这样实现了跨域。JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。JSONP实现流程如下
客户端代码
<!doctype html> <html lang="zh"> <head> <meta charset="utf-8"> <title></title> <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"></script> <script type="text/javascript"> $.ajax({ async: false, type : "get", dataType:'jsonp', jsonpCallback:"callback",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据 url:"http://127.0.0.1:8081/test/jsonp", success:function(data){ console.log(data); } }) </script> </head> <body> <div>测试jsonp</div> </body> </html>
服务端代码
@RestController public class MyController { @GetMapping("/test/jsonp") public void testJsonp(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=utf-8"); PrintWriter out = response.getWriter(); String callback = request.getParameter("callback"); System.out.println("callback = "+callback); JSONObject obj = new JSONObject(); obj.put("name","test jsonp"); obj.put("text","测试JSONP"); out.println(callback+"("+obj.toJSONString()+")"); } }
需要注意的是服务端返回的数据需要特别的处理一下,上面代码中obj是需要返回的数据,而callback是前端传给我们的前端需要回调的函数名,接口返回给前端的结果如下结构,其中 callback 对应前端的一个可执行函数
最终前端控制台打印结果如下
刚才的例子是原始的方式去实现后端支持JSONP返回,这样可以让我们只观的知道前端需要什么样的数据以及它的原理。当然对于Spring Boot来说,有更好的实现方式,通过 @ControllerAdvice 增强注解即可
@CrossOrigin注解
使用CrossOrigin注解是后端配置去实现跨域请求,这里虽然指SpringBoot但是SpringMVC也是一样的,要求在Spring4.2及以上的版本,其实现原理也很简单,只是通过这个注解在响应头上加上了允许跨域的报文信息,这样的响应才会被浏览器接受
客户端代码
<!doctype html> <html lang="zh"> <head> <meta charset="utf-8"> <title></title> <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"></script> <script type="text/javascript"> $.ajax({ async: false, type : "get", dataType:'json', url:"http://127.0.0.1:8081/test/json", success:function(data){ console.log(data); } }) </script> </head> <body> <div>测试jsonp</div> </body> </html>
后端代码
@RestController public class MyController { //实现跨域注解 //origin="*"代表所有域名都可访问 //maxAge Cookie的有效期 单位为秒 //若maxAge是负数,则代表为临时Cookie,不会被持久化,Cookie信息保存在浏览器内存中,浏览器关闭Cookie就消失 //@CrossOrigin(origins = "*",maxAge = 3600) @CrossOrigin @GetMapping("/test/json") public String testMethod(){ JSONObject obj = new JSONObject(); obj.put("name","test json"); obj.put("text","测试JSON"); return obj.toJSONString(); } }
关于CrossOrigin注解的参数解释如下
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CrossOrigin { String[] DEFAULT_ORIGINS = { "*" }; String[] DEFAULT_ALLOWED_HEADERS = { "*" }; boolean DEFAULT_ALLOW_CREDENTIALS = true; long DEFAULT_MAX_AGE = 1800; /** * 同origins属性一样 */ @AliasFor("origins") String[] value() default {}; /** * 所有支持域的集合,例如"http://domain1.com"。 * <p>这些值都显示在请求头中的Access-Control-Allow-Origin * "*"代表所有域的请求都支持 * <p>如果没有定义,所有请求的域都支持 * @see #value */ @AliasFor("value") String[] origins() default {}; /** * 允许请求头重的header,默认都支持 */ String[] allowedHeaders() default {}; /** * 响应头中允许访问的header,默认为空 */ String[] exposedHeaders() default {}; /** * 请求支持的方法,例如"{RequestMethod.GET, RequestMethod.POST}"}。 * 默认支持RequestMapping中设置的方法 */ RequestMethod[] methods() default {}; /** * 是否允许cookie随请求发送,使用时必须指定具体的域 */ String allowCredentials() default ""; /** * 预请求的结果的有效期,默认30分钟 */ long maxAge() default -1; }
对于允许跨域的响应报文主要是 Access-Control-Allow-Origin 起作用,通过刚刚的代码,可以看到请求的响应报文如下
Access-Control-Allow-Origin是标识允许哪个域的请求。如果服务器不通过,响应报文中根本没有这个字段,接着会触发同源策略,再接着你就看到浏览器的提示xxx的服务器没有响应Access-Control-Allow-Origin字段