跨域
1. 什么是跨域
不同源之间的数据访问称之为跨域
当跨域访问数据时, 如果没有特地做过处理, 则无法获取不同源的数据(通过标签引用 js
, img
, css
是可以的, 因为虽然我们通过标签获取到了这些资源, 但是我们仍然不知道对应文件中的内容是什么)
1.1 什么是源
所谓的源是指 协议+域名+端口号
如果两个 url
之间的 协议, 域名, 端口号 中有任意一个不同, 则称这两个 url
不同源(即使是两个不同的域名指向同一个 IP 地址也一样是不同源的)
1.2 为什么会有同源策略
1995年, 同源策略由 Netscape 公司引入浏览器. 最初, 它的含义是指: A网页设置的 Cookie
, B网页不能打开, 除非这两个网页同源
如果没有同源策略的话, 那么在你访问B网站的同时, B网站也可以获取A网站的 Cookie
- 而如果此时的
Cookie
包含了好友信息, 账户余额等隐私, 就容易导致隐私的泄露. - 而且
Cookie
经常用来保存用户的登录状态, 如果B网站在用户登录A网站的同时获取了Cookie
, 那么B网站也能够登录A网站从而进行更多的操作
由此可见, 同源策略的存在是必要的. 否则 Cookie
能够共享, 那么上网将毫无安全性可言
1.3 同源策略的具体限制
随着时代的发展, 同源策略的限制也越来越严格
Cookie
,LocalStorage
和IndexDB
无法获取DOM
无法获得AJAX
请求的数据无法获取
虽然同源策略是必要的, 但很多时候还是会带来很多不便. 尤其是现在 web 服务器和资源服务器基本都是不同源的情况下. 所以出现了一些即使不同源, 也能获取数据的方法
2. 如何跨域
2.1 使用代理服务器进行跨域
只有浏览器上应用了同源策略, 所以即使浏览器与服务器之间的不同源访问会有限制, 但是服务器与服务器之间的不同源访问就不会有限制.
故而, 我们令代理服务器与浏览器访问的页面同源, 从而使得浏览器可以访问代理服务器, 而代理服务器又能够访问目的服务器, 最终使得浏览器能够获取目的服务器的资源. 大致流程如下图所示:
这是相应的代理服务器的 node.js
代码
// server.js 代码
const express = require('express')
const request = require('request')
const app = express()
app.listen(2333, () => {
console.log('listening 2333 port')
})
app.get('/resource', (req, res) => {
// 当收到来自浏览器的 /resourse 请求时, 将会请求指定url的资源, 并将其返还给浏览器
let url = `https://www.bilibili.com`
req.pipe(request(url)).pipe(res)
})
/*
- src
- index.html
- main.js
- style.css
- server.js
*/
app.use(express.static('./src/')) // 托管静态文件
// http://localhost:2333/index.html 相当于直接访问 index.html 页面
2.2 JSONP
回顾一下, 我们是可以通过 script
标签获取到对应的文件的, 主要的问题是我们无法通过这个方法获取到数据. 但如果我们获取的这个 js
文件可以返回数据呢
-
在客户端设置: 可以在
script
标签中的src
属性上添加一定的参数(比如说callback=fn
, 在这之前, 需要存在一个全局函数fn
) -
在服务端设置: 服务端可以根据相应的路径和参数, 获取对应的数据, 并返回
fn(returnData)
形式的数据. 而返回给浏览器的数据会被当做js
代码执行, 从而达到跨域的目的(当然也可以返回其他形式的数据, 只要前后端协调好就行了) -
前端页面代码
<script>
const getData = (data) => {
// ... 处理数据
}
</script>
<script src="xxx/resource?callback=getData" />
- 后端处理代码
app.get('/resource', (req, res) => {
let { callback } = req.query
fs.readFile('./resource.js', (err, data) => { // 这里请求的文件的类型可以是 js, 也可以是 css, img 等等, 并不局限于某一类
res.send(`${callback}(${data.toString()})`)
})
})
优点:
- 支持 IE
- 可以跨域
缺点:
- 只有
GET
方式, 并不支持POST
2.3 CORS
当浏览器向不同源发送数据请求时, 浏览器会自动给请求头添加 Origin
字段, 其值为当前源. 如果返回的相应头中没有包含 Access-Control-Allow-Origin
字段, 或者该字段的值并不包含当前源, 则浏览器会阻止前端获取该数据
通过在后端设置响应头的一些属性, 从而使得跨域能够成功, 如在响应头设置
Access-Control-Allow-Origin: *
: 该字段是必须的.*
表示允许来自任何源的资源请求. 同理, 只要相应的值设置为所允许访问的源, 则可实现跨域资源共享Access-Control-Allow-Credentials: true
: 该字段可选, 表示是否允许发送cookie
给服务器, 如果不允许直接不添加该字段即可Access-Control-Expose-Headers
: 该字段可选. CORS请求时,xhr
对象的getResponseHeader
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定 对应的属性即为浏览器额外能够获取的字段
代码示例:
- 前端
js
请求代码
// 前端设置了一个 div 块级元素, 当点击该元素, 会向后端发送请求
const div = document.querySelector('.container')
div.addEventListener('click', (e) => {
const service = axios.create({
baseURL: 'http://127.0.0.1:2333',
timeout: 12000,
headers: {
'Content-Type': 'application/json'
},
withCredentials: true // 发送的请求中携带 cookie
})
service.get('/cookie_test').then(result => {
console.log(result)
}).catch(e => {
console.log(e)
})
})
- 后端
nodejs
相应代码
const express = require('express')
const request = require('request')
const fs = require('fs')
const app = express()
app.listen(2333, () => {
console.log('listening 2333 port')
})
app.get('/resource', (req, res) => { // 通过 JSONP 获取数据
let {callback} = req.query // 获取参数
fs.readFile('./data.json', (err, data) => {
res.setHeader('Set-Cookie', ['name=saber; HttpOnly', 'gender=female; HttpOnly', 'age=20; HttpOnly']) // 为浏览器设置 cookie 缓存(不允许前端访问)
res.send(`${callback}(${data})`)
})
})
app.get('/cookie_test', (req, res) => { // 前端的 AJAX 请求
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500') // 设置允许相应源的访问
res.setHeader('Access-Control-Allow-Credentials', 'true') // 允许请求头中携带 cookie
console.log(req.headers['cookie'])
res.send('response')
})
access-control-allow-methods: GET, POST, OPTIONS
: 表示请求方法只允许GET
,POST
和OPTIONS
三种
在 CORS
跨域请求中, 浏览器发送请求时会在请求头中增加一个 origin
字段, 对应的值记录的是本次请求所来自的源
2.4 Nginx 反向代理
给出一个专门进行反向代理的服务器的 BASE_URL
- 当用户需要获取前端页面时, 直接通过 BASE_URL/path/querystring 访问
- 当前端页面需要与后端进行交互时, 可以通过 BASE_URL/specialTag/path/querystring 获取
Nginx 反向代理配置(/etc/nginx/nginx.conf
)
worker_processes 2; // 指定工作线程的数量
http {
server {
listen 8081; // nginx 的监听端口号
server_name localhost; // 服务器的域名或IP地址
location / {
// 如果访问的 URL 以 localhost/ 开头, 则访问该 location 块下的 proxy_pass 所对应的地址
proxy_pass http://localhost:8080;
}
location /api {
// 如果访问的 URL 以 localhost/api 开头, 则会访问该 location 块下的 proxy_pass 所指定的地址
proxy_pass http://localhost:8000;
// 将客户端请求中的 Host 头信息原封不动地传递给后端服务器
proxy_set_header Host $host;
}
}
}
关于 Nginx 的一些基本指令:
nginx
: 启动 Nginxnginx -s reload
: 重启 Nginxnginx -s stop
: 停止 Nginxnginx -t
: 检查 Nginx 的配置文件是否正确