同源策略是网站安全的基石,https://a.com只能存取自己网站的资源,不允许网站https://b.com来存取。
目的是为了保证用户信息的安全,防止恶意的网站窃取数据。
只要url当中的协议scheme、域名domain、端口port都一样,就会被视为就会被视为同源 (same-origin),
其他则是不同源。
http://domain-a.com → 不同源.scheme 不同
https://domain-a.com/mike → 同源
https://news.domain-a.com → 不同源.domain 不同
https://domain-a.com:81 → 不同源.port 不同
https://domain-b.com → 不同源.domain 不同
1、在某些情況下跨来源是被允许的,比如跨来源嵌入。
script、link、img、iframe、video、audio、font-face等等。
2、如非同源,跨来源读取通常被禁止。
只要 domain 跟 Path 与Cookie 上的一样就会被视为同源。若经过一些设定才会判断 scheme 要是 http 或 https。
// 加了 Secure 会限定此 Cookie 只能以 https 传送
Set-Cookie: id=1234567; domain=hello.com; Secure
下面这样设置,domain
为一级域名,二级域名和三级域名不用做任何设置,都可以读取这个Cookie
。
Set-Cookie: key=value; domain=.example.com; path=/
跨域请求如何携带cookie
withCredentials
**XMLHttpRequest.withCredentials **属性是一个Boolean类型,它指示了是否该使用类似cookies,authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求。在同一个站点下使用
withCredentials
属性是无效的。
不同域下的
XmlHttpRequest
响应,不论其Access-Control-header
设置什么值,都无法为它自身站点设置cookie
值,除非它在请求之前将withCredentials
设为true
。
比如127.0.0.1:3000
请求127.0.0.1:8000
ctx.cookies.set("user", "forguo", {
domain: '127.0.0.1', // 写cookie所在的域名,默认主域名不带.,也就是子域名不生效
path: '/', // 写cookie所在的路径
maxAge: 10 * 60 * 1000, // cookie有效时长 10分钟
expires: new Date('2022-03-15'), // cookie失效时间
httpOnly: true, // 是否只用于http请求中获取
overwrite: false // 是否允许重写
});
查看浏览器 Application
> Cookie
可以看到Cookie
信息已经存储成功
此时,接口返回的Response Headers
会设置Cookie
Set-Cookie: user=forguo; path=/; expires=Fri, 11 Mar 2022 06:21:27 GMT; domain=127.0.0.1; httponly
设置好前后端配置后,我们通过跨端口从 http://127.0.0.1:3003向 http://127.0.0.1:3009 发起一个请求就会携带上Cookie
数据。
以上是同域名不同端口,如果是不同域名和端口,需要设置**samesite**
为**none**
,但是设置为none有一个要求,就是必须**secure**
属性为true,也就是必须使用**https**
。
而且不能是nginx
的proxy_pass
之后的api,否则会有以下报错,
Cannot send secure cookie over unencrypted connection
这样设置过后的cookie,实际是存在于接口所在的domain,下次请求接口会带上该cookie数据
ctx.cookies.set("user", params.user, {
// domain: '127.0.0.1', // 写cookie所在的域名
// path: '/', // 写cookie所在的路径
maxAge: 60 * 60 * 1000 * 24 * 7, // cookie有效时长 7天
expires: new Date(Date.now() + 60 * 60 * 1000 * 24 * 7), // cookie失效时间
httpOnly: false, // 是否只用于http请求中获取(设置为true的话,客户端在控制台就获取不到)
overwrite: false, // 是否允许重写
secure: true, // ++新增
sameSite: 'none', // ++新增
});
成功设置cookie
成功携带cookie
此时Cookie
存放于接口所在域名,同时下次请求该域名下的接口,会带上该Cookie
在前端请求的时候设置request
对象的属性withCredentials
为true
// axios添加withCredentials
axios({
method: "get",
withCredentials: true, // ++ 携带cookie数据
url: "http://127.0.0.1:3009/user/info",
}).then((res) => {
console.log(res);
});
此时,我们从 http://127.0.0.1:3003/ 向 http://127.0.0.1:3009/user/info 去发起一个请求,发现报错
意思是需要设置header的Access-Control-Allow-Origin
属性:
携带了Cookie
,但是报错CORS error
**跨域发送 Cookie 还要求 ***Access-Control-Allow-Origin**
不允许使用通配符(跨域设置Cookie会报错),而且只能指定单一域名:可以使用**ctx.headers.origin**
const cors = async (ctx, next) => {
// 设置跨域
ctx.set('Access-Control-Allow-Origin', ctx.headers.origin);
// ctx.set('Access-Control-Allow-Headers', 'X-Auth-Token, Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
// ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
// 允许携带cookie
// ctx.set('Access-Control-Allow-Credentials', 'true');
if (ctx.method === 'OPTIONS') {
ctx.body = 200;
} else {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
}
};
如果是下面这个错误,就说明配置的Origin
和当前前端所在Origin
不是同一个
that is not equal to the supplied origin.
重复1的请求,发现还是报错
意思是Access-Control-Allow-Credentials
这个属性应该设置为true
当然如果前端没有设置withCredentials
,是不会携带Cookie的,也不会有这个错误的
const cors = async (ctx, next) => {
// 设置跨域
ctx.set('Access-Control-Allow-Origin', ctx.headers.origin);
// ctx.set('Access-Control-Allow-Headers', 'X-Auth-Token, Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
// ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
// 允许携带cookie
ctx.set('Access-Control-Allow-Credentials', 'true');
if (ctx.method === 'OPTIONS') {
ctx.body = 200;
} else {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
}
};
再去发起1的请求,会发现请求成功,并且返回了cookie
数据
接口处理是解析了Cookie内容,并返回
.get('/info',async ctx => {
let user = {};
if (ctx.headers.cookie) {
// req.headers.cookie: user=forguo
user = ctx.headers.cookie.split("=")[1];
}
ctx.body = {
data: {
user,
cookie: ctx.headers.cookie,
},
code: 200,
serverTime: Date.now(),
};
})
同源政策规定,ajax
请求只能发给同源的网址,否则就报错。
解决方案
实现一个简易jsonp:https://github.com/f2e-learning/daily-code/tree/master/cors
基本思路是:向网页动态插入
/**
* @desc js原生实现jsonp
*/
function jsonpRequest () {
// 创建一个script标签,并插入页面
let script = document.createElement('script');
// 获取到跨域资源后的回调
window.handleFn = function (res) {
// JSONP跨域成功返回的资源
console.log(res);
//代码执行后,删除插入的script标签
document.getElementsByTagName('head')[0].removeChild(script);
}
let url = `http://127.0.0.1:3003/common/wechat/sdk?callback=handleFn`;
script.setAttribute('src', url);
// 将script标签插入到网页中
document.getElementsByTagName('head')[0].appendChild(script);
}
jsonpRequest();
// koa2
router
.get('/jsonp',async ctx => {
let method = ctx.request.method || 'GET';
let params = {};
if (method === 'GET') {
params = ctx.request.query;
}
if (method === 'POST') {
params = ctx.request.body;
}
try {
if (params.callback) {
// 返回结果
let res = params.callback + '(' + JSON.stringify({
data: {
now: Date.now(),
},
code: 200,
}) + ')';
// 需要注明Content-Type
ctx.response.type = 'application/javascript;charset=utf-8';
ctx.body = res;
} else {
ctx.response.type = 'application/json;charset=utf-8';
ctx.body = {
data: {
now: Date.now(),
},
code: 200,
};
}
} catch (e) {
console.log(e);
throw e;
}
})
server {
# 转发api到node服务
location /api/ {
proxy_pass http://127.0.0.1:3333/api/;
}
}
// koa2
/* 代理配置 start */
const proxy = require('koa2-proxy-middleware'); //引入代理模块
const options = {
targets: {
// (.*) means anything
'/api/(.*)': {
target: 'http://test02.com/',
changeOrigin: true,
},
}
}
app.use(
proxy(options)
);
const bodyparser = require('koa-bodyparser')
app.use(bodyparser({
enableTypes: ['json', 'form', 'text']
}))
/* 代理配置 end */
跨源资源分享(Cross-Origin Resource Sharing),通过相应的请求头与响应头来实现跨域资源访问。
如果将Access-Control-Allow-Origin
的值设置为*,则会接受所有域的请求。这时的客户端不需要任何配置即可进行跨域访问。
与Access-Control-Allow-Origin
相配套的,还有一个叫Access-Control-Allow-Credentials
的响应头,如果设置为true
则表明服务器允许该请求内包含cookie
信息。
同时,在客户端,还需要在ajax
请求中设置withCredentials
属性为true
。
// axios 拦截器
instance.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
config.headers.common['Authorization'] = 'Bearer ' + token;
config.withCredentials = true;
return config;
}, err => {
return Promise.reject(err);
});
该字段是一个逗号分隔的字符串,指定浏览器CORS
请求会额外发送的头信息字段,上例是X-Custom-Header。
该字段是必须的,用来列出浏览器的CORS
请求会用到哪些HTTP
方法。
完整的cors
配置
// koa2
const cors = async (ctx, next) => {
// 设置跨域
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'X-Auth-Token, Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
ctx.set('Access-Control-Allow-Credentials', 'true');
if (ctx.method === 'OPTIONS') {
ctx.body = 200;
} else {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
}
};
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
(2)HTTP的头信息不超出以下几种字段:
jQuery的ajax默认Content-Type
为application/x-www-form-urlencoded
,所以是一个简单请求
Axios的默认Content-Type
是没有的,所以是一个非简单请求