前端跨域是每个前端人绕不过的坎,也是必须了解的一个知识点。我记得第一次遇到前端跨域这个坑的时候,真是无语到极点,对于一个移动端出身的人来说,觉得这个玩意无可理喻。但是后来慢慢了解,觉得前端的同源策略是非常有必要的。同源策略就是浏览器默认让 www.baidu.com 不能加载来自 www.google.com 的数据。对于现在来说,所有数据都是同源的可能性基本上很小,比如我们公司静态资源 www.image.com 和前端资源 www.htmlCSS.com 的 CDN 路径都不一样,前端获取后台数据 www.apidata.com 又是另一个地址。如何解决这个坑呢?我们公司通过两种方式来避开。具体就是通过设置
来做 POST 请求,用 JSONP 来实现 GET 请求, 因为 JSONP 只能实现 GET 请求。
- Access-Control-Allow-Origin
有些人肯定就纳闷了,我就喜欢跨域,我就不关注安全,难道就没有办法了吗?当然是否定的。你需要做的,只是让服务器在返回的 header 里面加上
这个域就可以了。这样浏览器在接收到服务器返回的数据,就不会因为违反同源策略限制你拿到数据了。下面就用抓包来具体看一下:
- Access-Control-Allow-Origin
当我打开 这里点开 h5 链接 这个链接的时候。会去 https//m.ctrip.com 通过 POST 请求数据,这里就用到了跨域。
- : method: POST: authority: m.ctrip.com: scheme: https: path: /restapi/xyz content - length: 290 pragma: no - cache cache - control: no - cache accept: application / json origin: https: //pages.ctrip.com
- user - agent: Mozilla / 5.0(Linux; Android 6.0; Nexus 5 Build / MRA58N) ApplewebKit / 537.36(KHTML, like Gecko) Chrome / 63.0.3239.84 Mobile Safari / 537.36 content - type: application / json;
- charset = UTF - 8 referer: https: //pages.ctrip.com/ztrip
- accept - encoding: gzip,
- deflate,
- br accept - language: zh - CN,
- zh;
- q = 0.9,
- zh - TW;
- q = 0.8,
- en;
- q = 0.7
- {请求体,post请求的参数
- }
服务器返回的响应头如下:
- :status: 200
- server: Tengine/2.1.2
- date: Thu, 28 Dec 2017 11:01:29 GMT
- content-type: application/json;charset=utf-8
- access-control-allow-origin: *
- access-control-expose-headers: RootMessageId
- cache-control: private
- vary: Accept-Encoding
- clogging_trace_id: 8196881814119217567
- rootmessageid: 921812-0a0e0de1-420683-219524
- x-powered-by: CTrip/SOA2.0 Win32NT/.NET
- soa20-response-status: Success
- x-aspnet-version: 4.0.30319
- x-powered-by: ASP.NET
- x-gate: ctrip-gate
- x-gate-instance: unknown
- x-originating-url: http://m.ctrip.com/xyz
- x-gate-remote-call-cost: 9
- content-encoding: gzip
- slb-http-protocol-version: HTTP/2.0
- access-control-expose-headers: slb-http-protocol-version
- {服务器返回的有用数据}
我们可以看到,这里有
这个响应域就解决了问题。这个方法是最简单的,而且前端 POST 请求最常见的方法 (不确定还有其他好的解决方案)。这种方式最好就是通过他获取服务数据,不要加载 js 脚本。小心被别人注入攻击。
- access-control-allow-origin
讲 JSONP 之前,我先亮出一段常见的代码。下面这个方法主要就是动态的创建一个 script 标签,然后设置 src 属性。并且添加到 document 的第一个 script 标签之前。也就是说动态去加载一个 javscript 脚本。
- function loadJs(src, attrs = {}) {
- return new Promise((resolve, reject) => {
- const ref = document.getElementsByTagName('script')[0]
- //创建一个scrpt标签
- const script = document.createElement('script')
- //设置script标签的资源路径
- script.src = src
- script.async = true
- //设置属性
- for (let key in attrs) {
- script.setAttribute(key, attrs[key])
- }
- //script标签加入document中
- ref.parentNode.insertBefore(script, ref)
- script.onload = resolve
- script.onerror = reject
- })
- }
最有意思的是 script 标签的 src 不受跨域限制。也就是说 wwww.baidu.com 的文件可以通过上面这个方法无限制的加载 www.google.com 的 js 文件。这个就是 JSONP 的实现的最基本原理。每一个 JSONP 请求就是动态的创建 script 元素,然后通过 src 属性去加载数据,而且一般是通过 callback 这个回调方法来返回服务器数据,然后再把 script 标签移除。如此周而复始的循环,想想都累啊。下面看一个 JSON 的标准格式, 服务器会获取到 callback 这个回调方法。然后通过方法调用的方式把数据返回来,也就是执行 callbackFun 方法。serverdata 就是服务器给客户端的数据。至于 callback 这个名字,可以自己定义,有客户端和服务器商量决定。
- function callbackFun(serverdata){
- console.log(serverdata)
- }
- <script src="http://wwww.baidu.com/jsonp.js?callback=callbackFun"></script>
下面我会对 JSONP 做一个最基本的实现。使用 vue 和 node.js 分别实现客户端和服务端, 代码地址 。
首先我们先看客户端的实现:
- //获取header的第一个子元素
- let container = document.getElementsByTagName("head")[0];
- /**
- * 生成随机字符串
- */
- function makeid() {
- var text = "";
- var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- for (var i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length));
- return text;
- }
- /**
- * jsonp请求的实现。返回一个promise对象对应请求成功和请求失败。
- * @param {*请求的url} url
- * @param {*请求的参数} options
- */
- function jsonpRequest(url, options) {
- return new Promise((resolve, reject) = >{
- try {
- if (!url) {
- reject({
- err: new Error("url不能为空"),
- result: null
- });
- }
- if (!document || !global) {
- reject({
- err: new Error("系统环境有问题"),
- result: null
- });
- }
- //创建一个script元素
- let scriptNode = document.createElement("script");
- //请求参数
- let data = options || {};
- //回调函数的具体值,服务器和客户端就根据这个方法名来确定请求与返回数据之间的对应。
- let fnName = "jsonp" + makeid();
- // 把callback加入请求参数中
- data["callback"] = fnName;
- // 拼接url
- var params = [];
- //参数的拼接与处理
- for (let[key, value] of Object.entries(data)) {
- params.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
- }
- url = (url.indexOf("?")) > 0 ? (url + "&") : (url + "?");
- url += params.join("&");
- //把处理好的url赋值给script元素的src属性。
- scriptNode.src = url;
- // 把回调函数暴露为全局方法。script加载回来以后,会执行fnName对应的这个方法。
- global[fnName] = function(ret) {
- resolve({
- err: null,
- result: ret
- })
- //请求完成。删除script元素
- container.removeChild(scriptNode);
- //全局对象中删除已经请求完成的回调方法
- delete global[fnName];
- }
- // script元素遇到错误
- scriptNode.onerror = function(err) {
- reject({
- err: err,
- result: null
- })
- //删除script元素和全局回调方法
- container.removeChild(scriptNode);
- global[fnName] && delete global[fnName];
- }
- //指定元素类型
- scriptNode.type = "text/javascript";
- //把script元素添加到header元素中。到这里script元素就会自动加载src。也就是我们的请求发出去了。
- container.appendChild(scriptNode)
- } catch(error) {
- //异常处理捕获
- reject({
- err: error,
- result: null
- });
- }
- });
- }
- export
- default jsonpRequest;
这段代码主要做了如下几件事:
客户端的使用如下, 是不是感觉简洁明了,比 ES5 的回调爽多了:
- import jsonpRequest from "../lib/jsonpRequest.js";
- async sendJSONPRequest() {
- //参数
- let params = {
- name: "老黄",
- site: "www.huangchengdu.com"
- };
- this.showLoading();
- //发送请求
- let {
- err,
- result
- } = await jsonpRequest(
- "https://www.huangchengdu.com/jsonp/jsonpRequest",
- params
- );
- //处理返回的数据
- this.hiddenLoading();
- if (err) {
- alert(err.message || "请求出错了");
- this.serverData.err = JSON.stringify(err);
- } else {
- this.serverData = result;
- }
- }
服务端的实现如下。
- let express = require('express');
- let router = express.Router();
- //JSONP请求
- router.get('/jsonpRequest',
- function(req, res, next) {
- //console.log("=====================" + JSON.stringify(req.query));
- //获取name和site参数的值
- let name = req.query.name;
- let site = req.query.site;
- //拼接回调值
- let serverres = {
- serverReceive: {
- name: name,
- site: site
- },
- serverSend: "hello," + name + ".your site is https://" + site
- }
- //返回值。其实就是callback....()种种类型javascript字符串
- res.end(req.query.callback + "(" + JSON.stringify(serverres) + ")")
- });
- module.exports = router;
服务端代码说明如下:
这种类型。
- callback随机数(服务端数据)
这个数据以后,会自动按照 javascript 脚本解析执行。具体就是一个全局方法调用,方法名是 callback 随机数, 参数是服务端数据。这样就实现了服务端数据的回调。
- callback随机数(服务端数据)
这一行。
- global[fnName] = function(ret) {
JSONP 本质上就是一个普通的 GET 请求。无非就是这个请求是通过 script 标签来发送的。而且请求参数里面必定会有一个 callback 参数。
下面我们具体抓包看一下我们的请求报文:
- GET / jsonp / jsonpRequest ? name = %E8 % 80 % 81 % E9 % BB % 84 & site = www.huangchengdu.com & callback = jsonpiFuL4 HTTP / 1.1 Host: www.huangchengdu.com Accept: *
- /*
- Connection: keep-alive
- Cookie: session=s:not8KTW5FiTLY0VNgrrKksXY96AE2kWT.hrQeyL+Vjt8ICJjfFqoFdV8JV3lx0IsDntx++c/EM98
- User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7
- Accept-Language: zh-cn
- Referer: http://localhost:8081/
- Accept-Encoding: br, gzip, deflate*/
返回报文:
- HTTP/1.1 200 OK
- Server: nginx/1.6.2
- Date: Fri, 29 Dec 2017 03:26:31 GMT
- X-Powered-By: Express
- Transfer-Encoding: chunked
- Connection: Keep-alive
- jsonpiFuL4({"serverReceive":{"name":"��","site":"www.huangchengdu.com"},"serverSend":"hello,��.your site is https://www.huangchengdu.com"})
从上面的报文我们可以返现。请求的 callback 参数的值和返回的响应体的名称是一样的。响应提就是一个普通的函数。服务器返回的数据作为函数的参数。
XSS 的全称是
, 翻译过来就是跨站脚本。script 可以跨域加载脚本这个特性,合理利用比如 JSONP。如果不合理利用,比如某个坏人通过某种方式,让你的浏览器去加载恶意的 javascrpt 脚本,必然就会导致敏感信息被盗或者财务损失。最常见的就是 XSS 攻击,其实就是注入恶意脚本。真是凡事都有利有弊,就看如何使用了。常用的 XSS 攻击手段和目的有如下几种:
- Cross-site scripting
如果某一个字符串里面有
这种类型的字符串。而且我们刚好要通过 script 标签加载。那么他就会弹出一个我是你大爷。避免的方式就是把存在这种可能性的地方都处理过,如果包含类似
- var a = 1;<script>alert('我是你大爷')</script>;var b = 2;
这玩意我了解不多,也无法做出模拟操作。跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。[1] 跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
我的理解就是,比如你刚去淘宝买了东西,并且浏览器有你的 session 护着 cookie 之类的信息。然后你马上又进入一个不该去的网站,并且点击了里面的一个淘宝链接,然后在你不知情的情况下做一些违法操作。这样阿里后台是不知道的,因为你刚刚通过合法手段买了东西,从而达到在你不知情的情况下,而且淘宝也信任你的情况下,畏畏缩缩偷偷摸摸的干坏事。
检查 Referer 字段, 通过这个字段来判断用户是从那个地址跳转到当前地址的。HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer 字段地址通常应该是转账按钮所在的网页地址,应该也位于 www.examplebank.com 之下。而如果是 CSRF 攻击传来的请求,Referer 字段会是包含恶意网址的地址,不会位于 www.examplebank.com 之下,这时候服务器就能识别出恶意的访问。这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 http 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
添加校验 token, 这个就最常见了,现在那个前端网站还不加一个验证码啊。不管你如何千变万化,你验证码中是用户数据的吧,而且现在好像越来越流行手机号码验证了。CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 token 的值为空或者错误,拒绝这个可疑请求。
来源: http://www.jianshu.com/p/81efb4d188d7