node实现代理服务器解决跨域
1、产生跨域
1.1、什么是同源策略及其限制内容?
-
同源策略
- 同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源
-
同源策略限制内容有
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
-
跨域场景
2、node 实现代理解决跨域
2.1、代理原理
-
同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略
-
实现步骤
- 使用node启动一个代理服务器
- 客户端发送请求到代理服务器
- 代理服务器去目标服务器请求数据
- 目标服务器将数据返回给代理服务器
- 代理服务器将数据返回给客户端
-
图解
2.2、CORS跨域资源共享
- 简介
- CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
- 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
- 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
- 两种请求
-
简单请求
-
满足以下条件是简单请求
-
1)、请求方法是以下三种方法之一:
- HEAD
- GET
- POST
-
2)、HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
-
-
CORS设置响应头
- 1)、Access-Control-Allow-Origin
- 2)、[Access-Control-Allow-Credentials]
- 3)、[Access-Control-Expose-Headers]
-
-
非简单请求
- 字面意思,不是简单请求就是非简单请求
- CORS设置响应头
- 1)、Access-Control-Allow-Origin
- 2)、Access-Control-Allow-Methods
- 3)、[Access-Control-Allow-Headers]
- 4)、[Access-Control-Allow-Credentials]
- 5)、[Access-Control-Max-Age]
-
- 响应头介绍
- 1)、Access-Control-Allow-Origin
- 该字段是必须的。它的值要么是请求时
Origin
字段的值,要么是一个*
,表示接受任意域名的请求。
- 该字段是必须的。它的值要么是请求时
- 2)、Access-Control-Allow-Credentials
- 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为
true
,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie,删除该字段即可。
- 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为
- 3)、Access-Control-Expose-Headers
- 该字段可选。CORS请求时,
XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定,getResponseHeader('FooBar')
可以返回FooBar
字段的值。
- 该字段可选。CORS请求时,
- 4)、Access-Control-Allow-Methods
- 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
- 5)、Access-Control-Allow-Headers
- 如果浏览器请求包括
Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
- 如果浏览器请求包括
- 6)、Access-Control-Max-Age
- 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
- 1)、Access-Control-Allow-Origin
- 更多信息参考阮一峰:https://www.ruanyifeng.com/blog/2016/04/cors.html
2.3、假设一个场景
- 客户端
http://localhost:8000
- 代理服务器
http://localhost:8001
- 目标服务器
http://opendata.baidu.com
- 场景
- 客户端想拿到目标服务器的日历信息,但是目标服务器没有设置CORS跨域资源共享,此时我们可以让代理服务器帮我们去目标服务器拿我们想要的数据。
2.4、实现过程
-
客户端实现
-
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="button">获取日历信息</button> </body> </html> <script> const button = document.getElementById('button') button.addEventListener('click',async ()=>{ fetch('http://localhost:8001/calendar',{method: 'PUT',}).then(res => res.text()).then(res =>{ console.log(res) }) }) </script>
-
client.js
// 提供一个http 服务 const http = require('http'); // fs 模块 用于读取文件的 const fs = require('fs') // 创建一个服务 const server = http.createServer((request,response)=>{ let pathname = request.url //先获取地址 pathname=pathname=='/'?'/index.html':pathname //根目录下定位到首页 console.log(pathname) // 读取目标文件 fs.readFile('.' + pathname, (err,data)=>{ if (err) { response.writeHead(404, { 'Content-Type': 'text/html;charset="utf-8"' }); response.end('<h3>404</h3>'); } else { response.writeHead(200, { 'Content-Type': 'text/html;charset="utf-8"' }); response.end(data); } } ) }) server.listen(8000)
-
node client.js 启动服务
-
-
代理服务器-nodejs实现
-
node-proxy.js
const http = require('http') const iconv = require('iconv-lite'); const port = 8001 const server = http.createServer((req,res)=>{ // 简单设置下cors res.setHeader("Access-Control-Allow-Origin","*") res.setHeader("Access-Control-Allow-Methods","*") if (req.url.split('/')[1] === 'calendar') { console.log('start calendar') const options = { hostname: 'opendata.baidu.com', path: '/api.php?query=2020%E5%B9%B45%E6%9C%88&resource_id=6018&format=json', method: 'GET', } const calendar = http.request(options) calendar.on('response',calendarRes => { res.setHeader('Content-Type', 'text/html;charset=utf-8') let d = null calendarRes.on('data',data=>{ // node 不支持 gbk编码 借助iconv转译 // 解码 const t = iconv.decode(data,'GBK') // 编码 const r = iconv.encode(t,'utf8') d +=r }) calendarRes.on('end',()=>{ res.end(d) }) }) calendar.on('error',calendarErr => { console.log(calendarErr) } ) calendar.end() } else { res.end('666') } }) server.listen(port,()=>{ console.log(`服务器运行在 localhost:${port}/`) })
-
效果
-
-
到这里我们的代理服务器完成了
- 应对复杂请求,代理多个域名没有支持,这里就不实现了,有兴趣可以自己玩玩
-
代理服务器-
express+http-proxy-middleware
实现-
客户端小修改index.html
button.addEventListener('click',async ()=>{ // http://localhost:8001/calendar fetch('http://localhost:8001/calendar/api.php?query=2020年5月&resource_id=6018&format=json',{method: 'PUT',}).then(res => res.text()).then(res =>{ console.log(res) document.body.innerText = res }) })
-
代理服务器
const express = require('express') const iconv = require('iconv-lite'); const port = 8001 const app = express() app.get('/', (req, res) => { res.send('Hello World!') }) const cors = function(req, res, next){ console.log('LOGGED') // get 方法会自动添加,其他方法需要手动添加 res.set({ "Access-Control-Allow-Origin":"*", "Access-Control-Allow-Methods":"*", // 'Content-Type':"text/html;charset=utf-8" }) next() } app.use(cors) // ------------------- 代理获取日历信息 function onProxyReq(proxyReq, req, res) { console.log('onProxyReq',req.method) proxyReq.method = 'GET' console.log('onProxyReq',proxyReq.method) } function onProxyRes(proxyRes, req, res) { console.log('onProxyRes',proxyRes.headers) res.set('Content-Type',"text/html;charset=utf-8") let d = null proxyRes.on('data',data => { // node 不支持 gbk编码 借助iconv转译 // 解码 const t = iconv.decode(data,'GBK') // 编码 const r = iconv.encode(t,'utf8') d +=r }) proxyRes.on('end',()=>{ res.send(d) }) } const httpProxyMiddleware = require('http-proxy-middleware') const proxyConfig = { target: 'http://opendata.baidu.com', changeOrigin: true, pathRewrite: { '^/calendar' : '/', // 重写请求, }, onProxyReq, // 处理请求 selfHandleResponse:true, // 手动处理响应 onProxyRes // 处理响应函数 } const proxy = httpProxyMiddleware.createProxyMiddleware(proxyConfig) app.use('/calendar',proxy) // app.put('/calendar',(req,res)=>{ // res.send('Hello Calendar!') // }) app.listen(port,()=>{ console.log(`服务器运行在 localhost:${port}/`) })
-
效果
-
-
我们使用
exprss
+http-proxy-middleware
也完成代理服务器的开发。- 同样通用性很差,这里就不继续整了,有兴趣可以自己玩玩。
2.5、收获
- 了解了代理解决跨域的原理
- 也知道了
webpack
代理请求的原理http-proxy-middleware
实现的
- CORS跨域资源共享,实现过程
- 还有一点点node的应用