cookie

技术背景

http协议是一个无状态的协议,无状态就意味着服务器不知道这次请求的人,跟之前请求的人是否是同一个。

所以cookie出现的主要应用于记录用户的部分信息,这部分信息让客户端存储起来,然后每次请求的时候将这部分信息带给服务端,这样服务端就能区分用户的神分信息,还有一些偏好设置。

场景

假设服务器有一个接口,通过请求这个接口,可以添加一个系统管理员,但是,并不是系统中的用户都有权力进行这个操作,由于http协议无状态的特性,所以就需要用到一种身份验证的解决方案。这个解决方案就是cookie

具体方案

  1. 客户端登录成功之后,服务器会颁发一个令牌给客户端
  2. 后续客户端每次请求,都需要携带这个令牌

cookie的组成

cookie是浏览器中特有的一个概念,他就像浏览器的专属卡包,管理着各个网站的身份信息。每一个cookie就相当于是某个网站的一个令牌,它记录了这么些信息:

  • key:键,比如 身份编号

  • value:值,可以是任何信息

  • domin:域,表达这个cookie所属的网站,比如clesbit.top,表示这个cookie是属于clesbit.top这个网站的。

  • path:路径,表示这个cookie是属于该网站的哪个基路径的,就好比同一家公司不同的部门的权限一样,不同的部门的权限不一样。比如 /path

表示这个cookie属于这个 /path 路径

  • secure:是否使用安全传输
  • expire:过期时间

当浏览器向服务器发送一个请求的时候,他会自动携带合适的令牌带给服务端,如果一个cookie同时满足以下的条件,则这个cookie会被附带到请求头当中

  • cookie没有过期
  • cookie中的域和这次请求的域是匹配的
  • cookie中的path和这次请求的path是匹配的
    • 比如cookie中的path是/admin,则可以匹配的请求路径是/admin/admin/detail/admin/a/b/c等,但是不能匹配/blogs
    • 如果cookie的path是/,可以匹配所有的路径
  • 验证cookie的安全传输
    • 如果cookie的secure属性是true,则请求协议必须是https,否则不会发送该cookie
    • 如果cookie的secure属性是false,则请求的协议可以是http,也可以是https

如果一个cookie满足了上述的所有条件,则浏览器会自动把他加入到这次请求中

具体加入的方式是,浏览器会将符合条件的cookie,自动放置到请求头当中,例如,当我访问语雀的时候,它自动在请求头中附带了下面的cookie

属性值

一段node服务端代码

1
2
3
4
5
6
7
8
9
10
11
router.post("/login", async (req, res) => {
try {
const result = await admServ.login(req.body.loginId, req.body.loginPwd)
if(result){
res.header("set-cookie",`token=${result.id}; path=/api/admin; domain=localhost; max-age=3600;httponly`) //在请求头设置cookie
}
res.send(sendMsg.getResult(result))
} catch (err) {
res.send(sendMsg.getErr(err))
}
})

跨域

:warning:跨域的主要原因是浏览器的同源策略。

JSONP跨域解决

原理是script标签的加载不受同源策略的限制

客户端部分代码

1
2
3
4
5
6
7
<script src="./js/index.js"></script>
<script>
function callback(data){
console.log(data)
}
</script>
<script src="http://localhost:9527/api/student/"></script> //api

服务端部分代码,服务端将请求的数据以js脚本的形式响应给客户端,客户端调用对应的回调函数即可拿到数据。

1
2
3
4
5
6
7
8
9
10
11
router.get("/", async (req, res) => {
const page = req.query.page || 1
const limit = req.query.limit || 10
const sex = req.query.sex || -1
const name = req.query.name || ''
const result = await stuServ.getStudents(page, limit, sex, name)
const json = JSON.stringify(result)
const script = `callback(${json})`
res.header("content-type","application/javascript").send(script)
// res.send(script)
})

缺点:

验证影响服务器的响应格式

仅局限于get请求

CORS

概述

cors是基于http1.1的一种跨域解决方案,全称是cross-origin-resource-sharing,跨域资源共享。解决跨域问题的思路如下:

当某个网站发起请求,浏览器会放行,让请求到达指定的服务器,但是响应回来的时候,浏览器就会做同源策略的检查,如果不符合同源策略,他就会给你拦截下来,但是如果服务器的响应中表明自己,能够被该网站访问。那浏览器就不会拦截这个请求的响应。

  • 服务器:对的他是我的朋友,让他接到我的回应吧
  • 浏览器:好的
  • 用户:nice

但是往往一个请求的处理的流程总是复杂的,不同的请求方法,对于服务器的影响程度是不同的。

针对不同的请求,CORS规定了三种不同从处理模式

  • 简单模式
  • 需要预检的请求
  • 附带身份凭证的请求

从上往下请求处理的模式越来越复杂,对于相关的条件也越来越严格。

简单请求的判定(同时满足以下的条件)

1. 请求方法属于下面的一种

  • get
  • post
  • head

2. 请求头仅包含安全的字段,常见的安全字段如下:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type
  • DPR
  • DownLink
  • Save-Data
  • Viewport-width
  • Width

3. 请求头如果包含Content-Type,仅限下面的值之一:

  • text/plain
  • mutipart/form-data
  • application/x-www-urlencoded

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//	简单请求
fetch("http://跨域域名/api/xxx")

// 不是简单请求
fetch("http://跨域域名/api/xxx",{
method:"PUT"
})
// 添加了额外的请求头
fetch("http://跨域域名/api/xxx",{
headers:{
a:1
}
})
// 简单请求
fetch("http://跨域域名/api/xxx",{
method:"POST"
})
// Content-Type不符合要求
fetch("http://跨域域名/api/xxx",{
method:"POST"
headers:{
"content-type":"application/json"
}
})

当浏览器判定某个ajax跨域请求是简单请求的时候,会发生下面的事情。

  1. 会在请求头自动添加Origin字段,这个字段表明请求来自哪一个域
  2. 服务器响应头中应包含Access-Control-Allow-Origin,这个字段就表明服务器允许的访问的源

需要预检的请求

简单的请求一般对于服务器的影响不大,所以简单请求的交互模式比较简单,但是如果浏览器认为这个不是一个简单的请求,就会按照下面的流程进行:

  1. 浏览器发送预检请求,询问服务器是否允许
  2. 服务器允许
  3. 浏览器发送真实的请求
  4. 服务器完成真实的响应

例如

1
2
3
4
5
6
7
8
9
10
//需要预检的请求
fetch("http://localhost:9527/api/student", {
method: "POST",
headers: { //设置请求头
"h1": 1,
"h2": 2,
"Content-Type": "application/json" //不是简单请求了
},
body: JSON.stringify({ name: "bit", age: 22,sex:false,mobile:"18172264572",birthday:"2002-05-17",ClassId:2 })//设置请求体
}).then(res => res.json()).then(res => { console.log(res) })

特别注意

预检请求示例

1
2
3
4
5
6
OPTIONS /api/student HTTP/1.1
HOST:请求域
...
Origin:源域
Access-Control-Request-Method: POST
Access-Control-Request-Headers: h1,h2,Content-Type

预检请求是没有请求体的,它包含后续真实请求要做的事情

预检请求有如下的特征:

  • 请求方法未OPTIONS
  • 没有请求体
  • 请求头中包含
    • Origin:请求的源,和简单请求的含义一致
    • Access-Control-Request-Method:后续的真实请求将使用的请求方法
    • Access-Control-Request-Headers:后续真实请求会改动的请求头

服务器收到预检的请求之后,可以检查预检请求中包含的信息,如果允许这样的请求,需要响应下面的消息格式

一断服务端处理预检请求的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//这是一个中间件,专门用于处理跨域请求的
module.exports = (req, res, next) => {
if(req.method === "OPTIONS"){
console.log("这是一个预检请求")
res.header('access-control-allow-method',req.headers['access-control-request-method'])
res.header('access-control-allow-headers',req.headers['access-control-request-headers'])
}

//处理简单请求
if("origin" in req.headers && allowOrigins.includes(req.headers.origin)){
res.header("access-control-allow-origin",req.headers.origin)
}


next()
}
1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
DATE:GMT时间
...
Access-Control-Allow-Origin: 源域 #和简单请求一样表示允许的源
Access-Control-Allow-Headers: h1,h2,Content-Type #表示允许的请求头
Access-Control-Allow-Method: POST #表示允许的请求方法
Access-Control-Max-Age: 86400 #表示多少秒之内,下次发出的请求不再需要发送预检请求
...

后续请求和简单请求差不多。

附带身份凭证的请求

Credentials(凭据)

默认情况下,ajax的跨域请求并不会附带cookie,这样一来,某些需要权限的操作就无法进行,不过可以通过简单的配置就可以实现附带cookie

1
2
3
4
5
6
7
8
//xhr
const xhr = new XMLHttpRequest()
xhr.withCredentials = true

// fetch api
fetch(url,{
credentials:"include"
})

这样一来,该跨域的ajax请求就是一个附带身份凭证的请求,当一个请求需要附带cookie的时候,无论他是简单请求,还是预检请求,都会在请求头中添加cookie字段。

而服务器响应的时候,需要明确告知客户端:服务器允许这样的凭据。

告知的方式也很简单,只需要在响应头中添加:Access-Control-Allow-Credentials:true即可

对于一个附带身份凭据的请求,如果服务器没有明确的告知,浏览器依然会把其视为跨域。

对于附带身份凭证的请求,服务器不得设置Access-Control-Allow-Origin的值为*