跨域设置cookie

我们先从一个问题来引入接下来的内容,先看下以下代码

const koa = require('koa');
const Router = require('koa-router');

let server = new koa();
let router = new Router();
server.listen(8080);
server.use(router.routes());

// cookie循环秘钥
server.keys = [
    'sdf7as9d8f7asd7f9sdfa9s',
    'sdfasd6fgjhgjgdgjsfgsf5',
    'nk54h3k2klj78kh89kh5kh3',
];

router.get('/api/user', async (ctx, next) => {
    ctx.set("Access-Control-Allow-Origin", "*");
    ctx.cookies.set('username', 'daryl', {
        signed: true,
        maxAge: 86400 * 30,
    })
    ctx.body = {username: 'daryl', age: 18};
});

我们开启了一个服务器,当请求/api/user接口的时候,我们设置跨域,并且设置了一个cookiekeyusernamevaluedaryl

当我们直接在浏览器中请求这个接口,我们发现可以将cookie设置成功,响应头中有set-cookie响应头,并在application中可以看到已经设置成功

但是当我们使用ajax请求去调用接口,此时cookie无论如何也设置不上

客户端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
<button id="btn_get">get</button>
<script>
    // 直接fetch请求
    let btn_get = document.getElementById('btn_get');
    btn_get.addEventListener("click", () => {
        fetch('http://localhost:8080/api/user',
        )
            .then(response => {
                return response.json();
            })
            // 请求的数据
            .then(data => {
                console.log(data);
            }).catch(err => console.log(err));
    });
</script>
</body>
</html>

我们可以看到,在响应头中也有set-cookie字段,但是application中就是设置不成功

这是什么原因?实际上是因为当我们使用ajax去请求接口时,是发生了跨域的,而设置cookie是不允许跨域设置的,浏览器也不会跨域发送cookie

如果想要能够跨域设置cookie或发送cookie,需要服务端和ajax同时配置,才能允许跨域设置cookie

服务端需要添加一个Access-Control-Allow-Credentials响应头,客户端ajax请求时需要设置withCredentials属性为true(使用fetch的配置是{ credentials: 'include' },不同的库有不同的设置方法)

我们加上这两个设置,看能不能设置成功。

router.get('/api/user', async (ctx, next) => {
    ctx.set("Access-Control-Allow-Origin", "*");
    ctx.set("Access-Control-Allow-Credentials", true);
    ctx.cookies.set('username', 'daryl', {
        signed: true,
        maxAge: 86400 * 30,
    })
    ctx.body = {username: 'daryl', age: 18};
});
btn_get.addEventListener("click", () => {
    fetch('http://localhost:8080/api/user',
    {
        credentials: 'include'
    }
    )
      .then(response => {
          return response.json();
      })
      // 请求的数据
      .then(data => {
          console.log(data);
      }).catch(err => console.log(err));
});

我们来看看效果

直接报跨域错误了,咋回事,我们明明设置了Access-Control-Allow-Origin*啊。

记住,这是因为一旦当我们设置了Access-Control-Allow-Credentials请求头,我们Access-Control-Allow-Origin就不能直接设置为*了,需要指定明确的源,因此这里我们需要设置为http://127.0.0.1:5500(客户端的url是这个)

router.get('/api/user', async (ctx, next) => {
    ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
    ctx.set("Access-Control-Allow-Credentials", true);
    ctx.cookies.set('username', 'daryl', {
        signed: true,
        maxAge: 86400 * 30,
    })
    ctx.body = {username: 'daryl', age: 18};
});

我们再来看看效果

跨域错误没有了,但是在set-cookie头的这里出现了一个黄色的感叹号,application里面cookie也没有设置成功

这个警告是什么?

这个警告是告诉我们cookieSameSite属性当前默认是Lax,如果我们需要跨站点访问cookie,需要将其设置为None

这个SameSite用来限制第三方 Cookie,可以设置三个值

  • Strict

    Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie

    这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub链接,用户点击跳转就不会带有 GitHubCookie,跳转过去总是未登陆状态。

  • Lax

    Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。

    导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。

    请求类型 示例 正常情况 设置Lax后
    链接 <a href="..."></a> 发送 Cookie 发送 Cookie
    预加载 <link rel="prerender" href="..."/> 发送 Cookie 发送 Cookie
    GET 表单 <form method="GET" action="..."> 发送 Cookie 发送 Cookie
    POST 表单 <form method="POST" action="..."> 发送 Cookie 不发送
    iframe <iframe src="..."></iframe> 发送 Cookie 不发送
    AJAX $.get("...") 发送 Cookie 不发送
    Image <img src="..."> 发送 Cookie 不发送
  • None

    Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

    下面的设置无效。

    Set-Cookie: widget_session=abc123; SameSite=None
    

    下面的设置有效。

    Set-Cookie: widget_session=abc123; SameSite=None; Secure
    

因此,如果我们想要跨站点发送cookie,需要设置SameSite=None,并且同时设置Secure属性(即配置https

我们就来配置下

const koa = require('koa');
const Router = require('koa-router');
const fs = require('fs');

const https = require('https');
const sslify = require('koa-sslify').default;

var options = {
    key: fs.readFileSync('./private_key.pem'),  //私钥文件路径
    cert: fs.readFileSync('./ca-cert.pem')  //证书文件路径
};

let server = new koa();
server.use(sslify());

let router = new Router();
server.use(router.routes());


https.createServer(options, server.callback()).listen(8080, (err) => {
    if (err) {
      console.log('服务启动出错', err);
    } else {
      console.log('guessWord-server运行在' + 8080 + '端口');
    }
});

// cookie循环秘钥
server.keys = [
    'sdf7as9d8f7asd7f9sdfa9s',
    'sdfasd6fgjhgjgdgjsfgsf5',
    'nk54h3k2klj78kh89kh5kh3',
];

router.get('/api/user', async (ctx, next) => {
    ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
    ctx.set("Access-Control-Allow-Credentials", true);
    ctx.cookies.set('username', 'daryl', {
        signed: true,
        maxAge: 86400 * 30,
        sameSite: 'none',
        secure: true
    })

    ctx.body = {username: 'daryl', age: 18};
});

此时请求路径则需要修改为https

btn_get.addEventListener("click", () => {
    fetch('https://localhost:8080/api/user',
    {
        credentials: 'include'
    }
    )
      .then(response => {
          return response.json();
      })
      // 请求的数据
      .then(data => {
          console.log(data);
      }).catch(err => console.log(err));
});

来看看效果,可以看到警告没了,并且application里看cookie也成功设置上了

最后来看看设置成功后,请求会不会带上当前的cookie,发送给服务端(最终版)

const koa = require('koa');
const Router = require('koa-router');
const fs = require('fs');

const https = require('https');
const sslify = require('koa-sslify').default;

var options = {
    key: fs.readFileSync('./private_key.pem'),  //私钥文件路径
    cert: fs.readFileSync('./ca-cert.pem')  //证书文件路径
};

let server = new koa();
server.use(sslify());

let router = new Router();
server.use(router.routes());


https.createServer(options, server.callback()).listen(8080, (err) => {
    if (err) {
      console.log('服务启动出错', err);
    } else {
      console.log('guessWord-server运行在' + 8080 + '端口');
    }
});

// cookie循环秘钥
server.keys = [
    'sdf7as9d8f7asd7f9sdfa9s',
    'sdfasd6fgjhgjgdgjsfgsf5',
    'nk54h3k2klj78kh89kh5kh3',
];

router.get('/api/user', async (ctx, next) => {
    ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
    ctx.set("Access-Control-Allow-Credentials", true);
    ctx.cookies.set('username', 'daryl', {
        signed: true,
        maxAge: 86400 * 30,
        sameSite: 'none',
        secure: true
    })

    ctx.body = {username: 'daryl', age: 18};
});

router.get('/api/cookie', async (ctx, next) => {
    ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
    ctx.set("Access-Control-Allow-Credentials", true);
    console.log(ctx.cookies.get('username'));
    ctx.body = ctx.cookies.get('username'); // 将cookie的值作为返回值
});
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
<button id="btn_get">get</button>
<button id="btn_cookie">send cookie</button>
<script>
    // 直接fetch请求
    let btn_get = document.getElementById('btn_get');
    btn_get.addEventListener("click", () => {
        fetch('https://localhost:8080/api/user',
        {
            credentials: 'include'
        }
        )
            .then(response => {
                return response.json();
            })
            // 请求的数据
            .then(data => {
                console.log(data);
            }).catch(err => console.log(err));
    });

    let btn_cookie = document.getElementById('btn_cookie');
    btn_cookie.addEventListener('click', () => {
        fetch('https://localhost:8080/api/cookie',{
            credentials: 'include'
        }).then(response => {
                return response.json();
            })
            // 请求的数据
            .then(data => {
                console.log(data);
            }).catch(err => console.log(err));
    })
</script>
</body>
</html>

可以看到,最终cookie值会放到请求头中,发送给服务端,而接口的返回值也是服务器读到的cookie中的值;


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com