文/面包理想
面試問到數據交互的時候,經常會問跨域如何處理。大部分人都會回答JSONP,然后面試官緊接著就會問:“JSONP缺點是什么啊?”這個時候坑就來了,如果面試者說它支持GET方式,然后面試官就會追問,那如果POST方式發送請求怎么辦?基礎扎實一些的面試者會說,使用CORS跨域,不扎實的可能就搖搖頭了。
這還沒結束,如果公司比較正規或者很在乎技術功底,你面試的又是重要崗位,HR還想砍你的工資,就會再補一刀,CORS跨域有什么問題呢?這時候能回答上來的就沒幾個了,就算是你答出來兼容性不好,需要IE10+瀏覽器,對方依然有話說,那兼容性怎么處理呢?應試者就沒話了,要么被Pass掉,即便留下來,談工資的時候就沒底氣了。
CORS跨域實在是面試官pass一個人的利器。
為什么會這樣呢?
1.遇到CORS請求的情況不多,開發者使用這個場景的很少,大部分都JSONP搞定了。
2.開發者自身技能不扎實,偷懶心態,平常沒有意識和意愿去提升自己的技術水平。
3.相關的學習資料少、純前端小白搭建可測試的環境難度大。
面對這條攔路虎,我們今天就徹底解決掉它,讓它不再是我們的軟肋,而是彰顯我們技術實力的亮點。
首先,什么是CORS?
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。 它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
優缺點
優點:
1.支持POST以及所有HTTP請求 2.安全性相對JSOP更高 3.前端要做的事兒比較少
缺點:
1.不兼容老版本瀏覽器,如IE9及其以下 2.需要服務端支持 3.使用起來稍微復雜了些
怎么用?
前端部分:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>CORS跨域請求</title> <script> function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { xhr = new XDomainRequest(); xhr.open(method, url); } else { xhr = null; } return xhr; } window.onload = function () { var oBtn = document.getElementById('btn1'); oBtn.onclick = function () { var xhr = createCORSRequest("get", "http://wpdic.com/cors.php"); if (xhr) { xhr.onload = function () { var json = JSON.parse(xhr.responseText); alert(json.a); }; xhr.onerror = function () { alert('請求失敗.'); }; xhr.send(); } }; }; </script> </head> <body> <input type="button" value="獲取數據" id="btn1"> </body> </html>
注意點:
1.上面代碼兼容IE8,因為用了XDomainRequest
2.其它代碼你就當成XMLHttpRequset用,別考慮什么2.0不2.0的
3.如果你想post數據,可以往 xhr.send()里面搞
4.這里不建議大家研究"simple methdod"之類的知識,代碼弄懂了會用就行,遇到問題了再查也不晚
后臺部分:
<?php header('content-type:application:json;charset=utf8'); header('Access-Control-Allow-Origin:*'); header('Access-Control-Allow-Methods:GET,POST'); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Headers:x-requested-with,content-type'); $str = '{"a":1,"b":2,"c":3,"d":4,"e":5}'; echo $str; ?>
注意點:
1.Access-Control-Allow-Origin:* 表示允許任何域名跨域訪問,如果需要指定某域名才允許跨域訪問,只需把Access-Control-Allow-Origin:*改為Access-Control-Allow-Origin:允許的域名,實際工作也要這么做2.Access-Control-Allow-Methods:GET,POST 規定允許的方法,建議控制嚴格些,不要隨意放開DELETE之類的權限
2.Access-Control-Allow-Credentials
該字段可選。它的值是一個布爾值,表示是否允許發送Cookie。默認情況下,Cookie不包括在CORS請求之中。設為true,即表示服務器明確許可,Cookie可以包含在請求中,一起發給服務器。這個值也只能設為true,如果服務器不要瀏覽器發送Cookie,刪除該字段即可。
最后,面試常考問題:
CORS和JSONP的應用場景區別?
者:寫bug
來源:https://segmentfault.com/a/1190000015597029
跨域這兩個字就像一塊狗皮膏藥一樣黏在每一個前端開發者身上,無論你在工作上或者面試中無可避免會遇到這個問題。為了應付面試,我每次都隨便背幾個方案,也不知道為什么要這樣干,反正面完就可以扔了,我想工作上也不會用到那么多亂七八糟的方案。
到了真正工作,開發環境有webpack-dev-server搞定,上線了服務端的大佬們也會配好,配了什么我不管,反正不會跨域就是了。日子也就這么混過去了,終于有一天,我覺得不能再繼續這樣混下去了,我一定要徹底搞懂這個東西!于是就有了這篇文章。
確實,我們這種搬磚工人就是為了混口飯吃嘛,好好的調個接口告訴我跨域了,這種阻礙我們輕松搬磚的事情真惡心!為什么會跨域?是誰在搞事情?為了找到這個問題的始作俑者,請點擊瀏覽器的同源策略[1]。
這么官方的東西真難懂,沒關系,至少你知道了,因為瀏覽器的同源策略導致了跨域,就是瀏覽器在搞事情。
所以,瀏覽器為什么要搞事情?就是不想給好日子我們過?對于這樣的質問,瀏覽器甩鍋道:“同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用于隔離潛在惡意文件的重要安全機制。”
這么官方的話術真難懂,沒關系,至少你知道了,似乎這是個安全機制。所以,究竟為什么需要這樣的安全機制?這樣的安全機制解決了什么問題?別急,讓我們繼續研究下去。
據我了解,瀏覽器是從兩個方面去做這個同源策略的,一是針對接口的請求,二是針對Dom的查詢。試想一下沒有這樣的限制上述兩種動作有什么危險。
有一個小小的東西叫cookie大家應該知道,一般用來處理登錄等場景,目的是讓服務端知道誰發出的這次請求。
如果你請求了接口進行登錄,服務端驗證通過后會在響應頭加入Set-Cookie字段,然后下次再發請求的時候,瀏覽器會自動將cookie附加在HTTP請求的頭字段Cookie中,服務端就能知道這個用戶已經登錄過了。
知道這個之后,我們來看場景:
1.你準備去清空你的購物車,于是打開了買買買網站www.maimaimai.com,然后登錄成功,一看,購物車東西這么少,不行,還得買多點。
2.你在看有什么東西買的過程中,你的好基友發給你一個鏈接www.nidongde.com,一臉yin笑地跟你說:“你懂的”,你毫不猶豫打開了。
3.你饒有興致地瀏覽著www.nidongde.com,誰知這個網站暗地里做了些不可描述的事情!由于沒有同源策略的限制,它向www.maimaimai.com發起了請求!聰明的你一定想到上面的話“服務端驗證通過后會在響應頭加入Set-Cookie字段,然后下次再發請求的時候,瀏覽器會自動將cookie附加在HTTP請求的頭字段Cookie中”,這樣一來,這個不法網站就相當于登錄了你的賬號,可以為所欲為了!如果這不是一個買買買賬號,而是你的銀行賬號,那……
這就是傳說中的CSRF攻擊淺談CSRF攻擊方式[2]。
看了這波CSRF攻擊我在想,即使有了同源策略限制,但cookie是明文的,還不是一樣能拿下來。
于是我看了一些cookie相關的文章聊一聊 cookie[3]、Cookie/Session的機制與安全[4],知道了服務端可以設置httpOnly,使得前端無法操作cookie,如果沒有這樣的設置,像XSS攻擊就可以去獲取到cookieWeb安全測試之XSS[5];設置secure,則保證在https的加密通信中傳輸以防截獲。
1.有一天你剛睡醒,收到一封郵件,說是你的銀行賬號有風險,趕緊點進www.yinghang.com改密碼。你嚇尿了,趕緊點進去,還是熟悉的銀行登錄界面,你果斷輸入你的賬號密碼,登錄進去看看錢有沒有少了。
2.睡眼朦朧的你沒看清楚,平時訪問的銀行網站是www.yinhang.com,而現在訪問的是www.yinghang.com,這個釣魚網站做了什么呢?
// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于沒有同源策略的限制,釣魚網站可以直接拿到別的網站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你輸入賬號密碼的Input')
console.log(`拿到了這個 ${node},我還拿不到你剛剛輸入的賬號密碼嗎`)
由此我們知道,同源策略確實能規避一些危險,不是說有了同源策略就安全,只是說同源策略是一種瀏覽器最基本的安全機制,畢竟能提高一點攻擊的成本。其實沒有刺不穿的盾,只是攻擊的成本和攻擊成功后獲得的利益成不成正比。
經過對同源策略的了解,我們應該要消除對瀏覽器的誤解,同源策略是瀏覽器做的一件好事,是用來防御來自邪門歪道的攻擊,但總不能為了不讓壞人進門而把全部人都拒之門外吧。沒錯,我們這種正人君子只要打開方式正確,就應該可以跨域。
下面將一個個演示正確打開方式,但在此之前,有些準備工作要做。為了本地演示跨域,我們需要:
1.隨便跑起一份前端代碼(以下前端是隨便跑起來的vue),地址是http://localhost:9099。
2.隨便跑起一份后端代碼(以下后端是隨便跑起來的node koa2),地址是http://localhost:9971。
1.JSONP在HTML標簽里,一些標簽比如script、img這樣的獲取資源的標簽是沒有跨域限制的,利用這一點,我們可以這樣干:
后端寫個小接口
// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
static async jsonp (ctx) {
// 前端傳過來的參數
const query = ctx.request.query
// 設置一個cookies
ctx.cookies.set('tokenId', '1')
// query.cb是前后端約定的方法名字,其實就是后端返回一個直接執行的方法給前端,由于前端是用script標簽發起的請求,所以返回了這個方法后相當于立馬執行,并且把要返回的數據放在方法的參數里。
ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, 'success'))})`
}
}
module.exports = CrossDomain
簡單版前端
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type='text/javascript'> // 后端返回直接執行的方法,相當于執行這個方法,由于后端把返回的數據放在方法的參數里,所以這里能拿到res。
window.jsonpCb = function (res) {
console.log(res)
}
</script>
<script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
</body>
</html>
簡單封裝一下前端這個套路
/**
* JSONP請求工具
* @param url 請求的地址
* @param data 請求的參數
* @returns {Promise<any>}
*/
const request = ({url, data}) => {
return new Promise((resolve, reject) => {
// 處理傳參成xx=yy&aa=bb的形式
const handleData = (data) => {
const keys = Object.keys(data)
const keysLen = keys.length
return keys.reduce((pre, cur, index) => {
const value = data[cur]
const flag = index !== keysLen - 1 ? '&' : ''
return `${pre}${cur}=${value}${flag}`
}, '')
}
// 動態創建script標簽
const script = document.createElement('script')
// 接口返回的數據獲取
window.jsonpCb = (res) => {
document.body.removeChild(script)
delete window.jsonpCb
resolve(res)
}
script.src = `${url}?${handleData(data)}&cb=jsonpCb`
document.body.appendChild(script)
})
}
// 使用方式
request({
url: 'http://localhost:9871/api/jsonp',
data: {
// 傳參
msg: 'helloJsonp'
}
}).then(res => {
console.log(res)
})
2.空iframe加form細心的朋友可能發現,JSONP只能發GET請求,因為本質上script加載資源就是GET,那么如果要發POST請求怎么辦呢?
后端寫個小接口
// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
static async iframePost (ctx) {
let postData = ctx.request.body
console.log(postData)
ctx.body = successBody({postData: postData}, 'success')
}
}
module.exports = CrossDomain
前端
const requestPost = ({url, data}) => {
// 首先創建一個用來發送數據的iframe.
const iframe = document.createElement('iframe')
iframe.name = 'iframePost'
iframe.style.display = 'none'
document.body.appendChild(iframe)
const form = document.createElement('form')
const node = document.createElement('input')
// 注冊iframe的load事件處理程序,如果你需要在響應返回時執行一些操作的話.
iframe.addEventListener('load', function () {
console.log('post success')
})
form.action = url
// 在指定的iframe中執行form
form.target = iframe.name
form.method = 'post'
for (let name in data) {
node.name = name
node.value = data[name].toString()
form.appendChild(node.cloneNode())
}
// 表單元素需要添加到主文檔中.
form.style.display = 'none'
document.body.appendChild(form)
form.submit()
// 表單提交后,就可以刪除這個表單,不影響下次的數據發送.
document.body.removeChild(form)
}
// 使用方式
requestPost({
url: 'http://localhost:9871/api/iframePost',
data: {
msg: 'helloIframePost'
}
})
3.CORS
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)跨域資源共享 CORS 詳解[6]。看名字就知道這是處理跨域問題的標準做法。CORS有兩種請求,簡單請求和非簡單請求。
這里引用上面鏈接阮一峰老師的文章說明一下簡單請求和非簡單請求。瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
只要同時滿足以下兩大條件,就屬于簡單請求。(1) 請求方法是以下三種方法之一:
(2)HTTP的頭信息不超出以下幾種字段:
1.簡單請求后端
// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
static async cors (ctx) {
const query = ctx.request.query
// *時cookie不會在http請求中帶上
ctx.set('Access-Control-Allow-Origin', '*')
ctx.cookies.set('tokenId', '2')
ctx.body = successBody({msg: query.msg}, 'success')
}
}
module.exports = CrossDomain
前端什么也不用干,就是正常發請求就可以,如果需要帶cookie的話,前后端都要設置一下,下面那個非簡單請求例子會看到。
fetch(`http://localhost:9871/api/cors?msg=helloCors`).then(res => {
console.log(res)
})
2.非簡單請求非簡單請求會發出一次預檢測請求,返回碼是204,預檢測通過才會真正發出請求,這才返回200。這里通過前端發請求的時候增加一個額外的headers來觸發非簡單請求。
后端
// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
static async cors (ctx) {
const query = ctx.request.query
// 如果需要http請求中帶上cookie,需要前后端都設置credentials,且后端設置指定的origin
ctx.set('Access-Control-Allow-Origin', 'http://localhost:9099')
ctx.set('Access-Control-Allow-Credentials', true)
// 非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)
// 這種情況下除了設置origin,還需要設置Access-Control-Request-Method以及Access-Control-Request-Headers
ctx.set('Access-Control-Request-Method', 'PUT,POST,GET,DELETE,OPTIONS')
ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, t')
ctx.cookies.set('tokenId', '2')
ctx.body = successBody({msg: query.msg}, 'success')
}
}
module.exports = CrossDomain
一個接口就要寫這么多代碼,如果想所有接口都統一處理,有什么更優雅的方式呢?見下面的koa2-cors。
const path = require('path')
const Koa = require('koa')
const koaStatic = require('koa-static')
const bodyParser = require('koa-bodyparser')
const router = require('./router')
const cors = require('koa2-cors')
const app = new Koa()
const port = 9871
app.use(bodyParser())
// 處理靜態資源 這里是前端build好之后的目錄
app.use(koaStatic(
path.resolve(__dirname, '../dist')
))
// 處理cors
app.use(cors({
origin: function (ctx) {
return 'http://localhost:9099'
},
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['t', 'Content-Type']
}))
// 路由
app.use(router.routes()).use(router.allowedMethods())
// 監聽端口
app.listen(9871)
console.log(`[demo] start-quick is starting at port ${port}`)
前端
fetch(`http://localhost:9871/api/cors?msg=helloCors`, {
// 需要帶上cookie
credentials: 'include',
// 這里添加額外的headers來觸發非簡單請求
headers: {
't': 'extra headers'
}
}).then(res => {
console.log(res)
})
4.代理想一下,如果我們請求的時候還是用前端的域名,然后有個東西幫我們把這個請求轉發到真正的后端域名上,不就避免跨域了嗎?這時候,Nginx出場了。Nginx配置
server{
# 監聽9099端口
listen 9099;
# 域名是localhost
server_name localhost;
#凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}
前端就不用干什么事情了,除了寫接口,也沒后端什么事情了
// 請求的時候直接用回前端這邊的域名http://localhost:9099,這就不會跨域,然后Nginx監聽到凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871
fetch('http://localhost:9099/api/iframePost', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
msg: 'helloIframePost'
})
})
Nginx轉發的方式似乎很方便!但這種使用也是看場景的,如果后端接口是一個公共的API,比如一些公共服務獲取天氣什么的,前端調用的時候總不能讓運維去配置一下Nginx,如果兼容性沒問題(IE 10或者以上),CROS才是更通用的做法吧。
1.postMessagewindow.postMessage() 是HTML5的一個接口,專注實現不同窗口不同頁面的跨域通訊。為了演示方便,我們將hosts改一下:127.0.0.1 crossDomain.com,現在訪問域名crossDomain.com就等于訪問127.0.0.1。
這里是http://localhost:9099/#/crossDomain,發消息方
<template>
<div>
<button @click="postMessage">給http://crossDomain.com:9099發消息</button>
<iframe name="crossDomainIframe" src="http://crossdomain.com:9099"></iframe>
</div>
</template>
<script>export default {
mounted () {
window.addEventListener('message', (e) => {
// 這里一定要對來源做校驗
if (e.origin === 'http://crossdomain.com:9099') {
// 來自http://crossdomain.com:9099的結果回復
console.log(e.data)
}
})
},
methods: {
// 向http://crossdomain.com:9099發消息
postMessage () {
const iframe = window.frames['crossDomainIframe']
iframe.postMessage('我是[http://localhost:9099], 麻煩你查一下你那邊有沒有id為app的Dom', 'http://crossdomain.com:9099')
}
}
}</script>
這里是http://crossdomain.com:9099,接收消息方
<template>
<div>
我是http://crossdomain.com:9099
</div>
</template>
<script>export default {
mounted () {
window.addEventListener('message', (e) => {
// 這里一定要對來源做校驗
if (e.origin === 'http://localhost:9099') {
// http://localhost:9099發來的信息
console.log(e.data)
// e.source可以是回信的對象,其實就是http://localhost:9099窗口對象(window)的引用
// e.origin可以作為targetOrigin
e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,這就是你想知道的結果:${document.getElementById('app') ? '有id為app的Dom' : '沒有id為app的Dom'}`, e.origin);
}
})
}
}</script>
結果可以看到:
2.document.domain這種方式只適合主域名相同,但子域名不同的iframe跨域。比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,這種情況下給兩個頁面指定一下document.domain即document.domain = crossdomain.com就可以訪問各自的window對象了。
3.canvas操作圖片的跨域問題這個應該是一個比較冷門的跨域問題,張大神已經寫過了我就不再班門弄斧了解決canvas圖片getImageData,toDataURL跨域問題[7]
希望看完這篇文章之后,再有人問跨域的問題,你可以嘴角微微上揚,冷笑一聲:“不要再問我跨域的問題了。” 揚長而去。
如果學到了可以點在看讓更多的小伙伴看到哦 。
參考資料
[1]瀏覽器的同源策略: https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
[2]淺談CSRF攻擊方式: http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html
[3]聊一聊 cookie: https://segmentfault.com/a/1190000004556040#articleHeader6
[4]Cookie/Session的機制與安全: https://harttle.land/2015/08/10/cookie-session.html
[5]Web安全測試之XSS: https://www.cnblogs.com/TankXiao/archive/2012/03/21/2337194.html
[6]跨域資源共享 CORS 詳解: http://www.ruanyifeng.com/blog/2016/04/cors.html
[7]解決canvas圖片getImageData,toDataURL跨域問題: https://www.zhangxinxu.com/wordpress/2018/02/crossorigin-canvas-getimagedata-cors/
同源策略瀏覽器設計的一個功能限制CORS突破同源策略的一個方法JSONPIE時代的補救辦法
(一)同源的定義:
舉例:https://qq.com 和 https://www.baidu.com 不同源https://baidu.com 和 https://www.baidu.com 也不同源(二)同源策略定義
(三)關鍵問題
(四)同源策略的目的
原文 https://segmentfault.com/a/1190000021686919
喜歡小編的可以點個贊關注小編哦,小編每天都會給大家分享文章。
我自己是一名從事了多年的前端老程序員,小編為大家準備了新出的前端編程學習資料,免費分享給大家!
如果你也想學習前端,可以觀看【置頂】文章。也可以私信【1】 領取最新前端練手實戰項目
*請認真填寫需求信息,我們會在24小時內與您取得聯系。