内容简介:同源策略(Same origin policy)是一种约定,是有在 localhost:1314 后台准备一个接口在 localhost:3000 开启一个 react 前端,并使用 axios 向后台发出请求
同源策略(Same origin policy)是一种约定,是有 Netscape 提出的一个著名的安全策略。所谓 同源 指的是 域名,协议,端口相同。同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。浏览器如果检查到资源属于非同源资源时,浏览器会在控制台中报一个异常,提示 拒绝访问 。
什么才算是同源?
请求方 | 被请求方 | 是否跨域 | 详细说明 |
---|---|---|---|
www.heheda.com | www.heheda.com/app.js | 否 | 属于同源 |
www.heheda.com | www.heheda.com/app.js | 是 | 协议不同( http | https ) |
www.heheda.com | blog.heheda.com/app.js | 是 | 子域名不同( www | blog ) |
www.heheda.com | www.hehe.com | 是 | 主域名不同( heheda | hehe ) |
www.heheda.com:8080 | www.heheda.com:8081 | 是 | 端口不同( 8080 | 8081 ) |
www.heheda.com | http://103.12.13.99 | 是 | 域名和通过 DNS 解析的 IP 也算跨域 |
什么情况下 AJAX 会产生跨域( 同时满足 )?
- 浏览器限制
- 跨域
- XHR (XMLHttpRequest)请求
准备工作
在 localhost:1314 后台准备一个接口
@RequestMapping(value = "testString") public String testString(){ log.info("request success"); return "testString"; } 复制代码
在 localhost:3000 开启一个 react 前端,并使用 axios 向后台发出请求
import React, {Component} from 'react'; import Axios from 'axios' class Test extends Component{ constructor(){ super(); this.state = { string: '' } } // 在第一次渲染后调用 componentDidMount(){ const _this = this; Axios.get("http://localhost:1314/testString").then(function(response){ _this.setState({ string: response.data }) }) } render(){ return( <h1>{this.state.string}</h1> ) } } export default Test; 复制代码
此时就会出现跨域问题,但是我们的请求是成功的
而浏览器的控制台打出了一个错误日志,这里可以发现,跨域是浏览器前台做的处理
针对浏览器的处理方法
我们让浏览器禁止同源策略来解决跨域问题,不过这是客户端的处理,存在一定的专业性,对用户很不友好。
例如 chrome 可以使用 --disable-web-security 参数来进行启动,禁止同源策略
这个时候再去访问 localhost:3000,成功跨域访问 后台接口
针对 XHR 请求的处理方法
当请求类型是 XHR 的时候后,就会出现跨域问题
这种情况下可以使用 JSONP 来解决,以下是 JSONP 的引用
JSONP(JSON with Padding)是 JSON 的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。由于同源策略,一般来说位于 server1.example.com 的网页无法与不是 server1.example.com的服务器沟通,而 HTML 的 script 元素是一个例外。利用 script 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP。用 JSONP 抓到的资料并不是 JSON,而是任意的JavaScript,用 JavaScript 直译器执行而不是用 JSON 解析器解析。
由于 axios 对 jsonp 不太支持,这里前端将会切换请求库 fetch-jsonp,同时改造后台接口
@RequestMapping(value = "testJSONP") public JSONPObject testJSONP(HttpServletRequest httpServletRequest){ String callback = httpServletRequest.getParameter("callback"); log.info("request JSONP success"); return new JSONPObject(callback,"testJSONP"); } 复制代码
使用 fetch-jsonp 发送请求
import React, {Component} from 'react'; import Axios from 'axios' import fetchJSONP from 'fetch-jsonp' class Test extends Component{ constructor(){ super(); this.state = { string: '' } } // 在第一次渲染后调用 componentDidMount(){ const _this = this; fetchJSONP("http://localhost:1314/testJSONP").then(function(response){ return response.json() }).then(function(json){ _this.setState({ string: json }) }).catch(function(error){ console.log(error) }) } render(){ return( <h1>{this.state.string}</h1> ) } } export default Test; 复制代码
此时发送请求就可以看到该请求不再是 XHR,而是 js
JSONP 请求后面会自动加上 callback,后台接收到 callback 参数,就会识别这是一个 jsonp,就会将返回值从 json 转换为 JS,至于约定的 callback 参数是可以更改的,但是需要后台进行重写 JSONP 基类进行修改。同时也需要修改 fetch-jsonp,默认是发送 callback 参数
服务端支持跨域
让你的服务器告诉浏览器支持跨域
当浏览器检测到请求的目的地和来源地非同源时,会自动在请求头加上请求的来源IP( Origin )
这个服务端可以在响应头中添加支持跨域的 Origin 的信息( Access-Control-Allow-Origin )来告诉浏览器,服务端支持以下 IP 的跨域请求,不包含在响应头中 IP 来源的请求将会被视为跨域请求
我们可以在后台使用 过滤器 来添加响应头信息,告诉浏览器 支持跨域的 IP 来解决跨域问题
// 该过滤器会添加响应头信息 @Component public class CorsFilter implements Filter{ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; // 支持前端的跨域请求 response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000"); filterChain.doFilter(servletRequest, servletResponse); } } 复制代码
可以发现此时的前端的跨域请求成功了,并且响应头多了 Access-Control-Allow-Origin 的信息
这样有一个问题,就是必须要手动在服务器后台的过滤器添加允许的 IP 才能跨域请求,这样不仅添加了工作量,也是不现实,我们可以使用 response.setHeader("Access-Control-Allow-Origin", "*")
来表示匹配所有 IP
理解 Options 预检命令
首先需要了解一下 简单请求 和 非简单请求 ,如果一个请求是简单请求,则浏览器会直接发送请求让服务器执行再去判断该请求是否跨域,如果是非简单请求,则会先发送一个 Options 请求去检测该请求是否跨域,跨域通过才会发送非简单请求去服务端执行。
只要同时满足以下两大条件,就属于简单请求:
- 请求方法是以下三种:
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
凡是不同时满足上面两个条件,就属于非简单请求
后台开发一个支持 PUT 请求接口用于测试
@PutMapping(value = "testPut") public String testPut(){ log.info("testPut"); return "testPut"; } 复制代码
前端发送请求
Axios.put("http://localhost:1314/testPut").then(function(response){ _this.setState({ string: response.data }) }) 复制代码
由于 PUT 请求不属于简单请求,这个时候浏览器会先发送一个预检命令 Options 来测试是否跨域
由于服务器没有返回支持的 PUT 请求方式的响应头信息,浏览器认定该请求跨域,请求失败
同样的需要在服务器端进行处理,添加支持 PUT 方法的响应头信息,对应的 Key 为 Access-Control-Allow-Methods
@Component public class CorsFilter implements Filter{ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT"); filterChain.doFilter(servletRequest, servletResponse); } } 复制代码
此时会发现前面的 PUT 请求在预检命令 Options 通过后,发送了 PUT 请求到后台执行,最终跨域请求成功
如果是HTTP的头信息包含额外信息导致请求为非简单请求,则对应需要添加的响应头信息是 Access-Control-Allow-Headers
,同时可以使用 Access-Control-Max-Age
来设置 Options 预检命令的缓存时长,在缓存期内,同一个 Origin 不会每次非简单请求都先发送预检命令
@Component public class CorsFilter implements Filter{ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT"); // 设置预检命令缓存时长,单位为 秒 response.setHeader("Access-Control-Max-Age", "3600"); // 允许自带请求头 Content-Type 的请求,这个时候就可以发送 Content-Type 为 application/json 的请求 response.setHeader("Access-Control-Allow-Headers", "Content-Type"); filterChain.doFilter(servletRequest, servletResponse); } } 复制代码
携带 Cookie 的请求需要做额外处理
在很多情况下,我们的请求是需要携带 Cookie,常见于携带权限认证用的 Token,SessionId 等,如果这个时候是跨域请求,则需要作出额外的处理:
withCredentials: true Access-Control-Allow-Headers Access-Control-Allow-Origin
后台开发返回 Cookie 信息接口
@GetMapping(value = "testCookie") public String testCookie(HttpServletRequest httpServletRequest){ Cookie[] cookies = httpServletRequest.getCookies(); if(cookies != null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("token")){ log.info("token : {}",cookie.getValue()); return cookie.getValue(); } } } return null; } 复制代码
后台添加 Access-Control-Allow-Headers
@Component public class CorsFilter implements Filter{ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Content-Type"); response.setHeader("Access-Control-Allow-Credentials","true"); filterChain.doFilter(servletRequest, servletResponse); } 复制代码
前端添加 Cookie
前端发送请求
componentDidMount(){ const _this = this; const config = { withCredentials : true } Axios.get("http://localhost:1314/testCookie", config).then(function(response){ _this.setState({ string: response.data }) }) } 复制代码
如果 服务端设置 Access-Control-Allow-Origin
为 true,则会跨域请求会被拒绝
将 Access-Control-Allow-Origin
设置为 具体地址来完成跨域请求
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Content-Type"); response.setHeader("Access-Control-Allow-Credentials","true"); filterChain.doFilter(servletRequest, servletResponse); } 复制代码
此时获取 Cookie 信息成功
我们使用一种 骚操作 来实现接收所有 Origin 跨域请求,可以先获取请求携带的 Origin,在添加到 Access-Control-Allow-Origin
当中
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; // 先获取请求头中的 Origin,在动态添加 响应头的 Access-Control-Allow-Origin String originHeader=((HttpServletRequest)servletRequest).getHeader("Origin"); if(originHeader != null){ response.setHeader("Access-Control-Allow-Origin", originHeader); } response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Content-Type"); response.setHeader("Access-Control-Allow-Credentials","true"); filterChain.doFilter(servletRequest, servletResponse); } 复制代码
参考文献
以上所述就是小编给大家介绍的《从现在开始认识跨域》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 从一个Bug开始,重新认识一个强大的 Gson
- 从零开始的程序逆向之路 第一章——认识OD(Ollydbg)以及常用汇编扫盲
- 认识认识LVS负载均衡集群
- 认识一下 SVG
- Netty初步认识
- MVP的初步认识
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。