Skip to content
On this page

HTTP 各种协议

CORS 跨域

js
// server-1.js
const http = require('http')
http.createServer(function (req, res) {
	console.log('request come', req.url)
	const html = fs.readFileSync('test.html', 'utf8')
	res.writeHead(200, {
		'Content-Type': 'text/html'
	})
	res.end(html)
}).listen(8888)
console.log('server listening on 8888')

// server-2.js
const http = require('http')
http.createServer(function (req, res) {
	console.log('request come', req.url)
	res.end('hello world')
}).listen(8887)
console.log('server listening on 8887')
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>
    <script>
        const xhr = new XMLHttpRequest()
        xhr.open('GET', 'http://127.0.0.1:8887/')
        xhr.send()
    </script>
</body>
</html>

在 8888 服务中,浏览器会出现跨域问题(服务器不会因为跨域而不响应,而是浏览器做的安全策略),解决问题如下:

js
// server-2.js
const http = require('http')
http.createServer(function (req, res) {
	console.log('request come', req.url)
	res.writeHead(200, {
		//'Access-Control-Allow-Origin': '*', // 任何服务都可访问
		//'Access-Control-Allow-Origin': 'http://baidu.com', // 允许百度可访问
		'Access-Control-Allow-Origin': 'http://127.0.0.1:8888/'
	})
	res.end('hello world')
}).listen(8887)
console.log('server listening on 8887')

难道真要使用 'Access-Control-Allow-Origin': '*' ? 答案是 JSONP(图片、iframe、script[src] 等等是可以跨域的)

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>
    <script src="http://localhost:8887/"></script>
</body>
</html>

'Access-Control-Allow-Origin': 'http://127.0.0.1:8888/' 如果有多个域呢?根据 request.url 区分然后分别在 head 设置 'Access-Control-Allow-Origin'

跨域请求的限制及预请求验证

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>
    <script>
	    fetch('http://127.0.0.1:8887/', {
		    methods: 'POST',
		    headers: {
			    'X-Test-Cors': '123'
		    }
	    })
    </script>
</body>
</html>

以上代码浏览器会抛出错误:Request header field X-Test-Cors is not allowed by Access-Control-Allow-Headers in preflight response. 但是这个预请求(preflight)是发送成功的(Request Methods: OPTIONS),是浏览器做处理进行报错了。浏览器怎么判断的?是根据响应头上的 Access-Control-Allow-Headers: xxx 判断的。如果允许自定义 header 的请求发送成功,那么这里需要设置新的请求头 Access-Control-Allow-Headers: X-Test-Cors

CORS 预请求:

  • 允许的方法:GET、POST、HEAD (除了这三个不需要,其他都要预请求进行验证)。 根据上面设置 Access-Control-Allow-Headers 使得自定义请求发送成功,那么也可以设置 'Access-Control-Allow-Methods': 'PUT, POST, Delete' 使得 PUT 和 Delete 方法可以发出请求
  • 允许的 Content-Type :text/plain | multipart/form-data | application/x-www-form-urlencoded (除了这三个,其他都需要预请求进行验证)。
  • 其他限制:请求头限制(设置了自定义的 header 字段),有哪些请求头不允许,这里官方文档有解释;XMLHttpRequestUpload 对象均没有注册任何事件监听器;请求中没有使用 ReadableStream 对象。 以上几点被称为复杂请求。

什么是预请求?

预请求就是复杂请求(可能对服务器数据产生副作用的 HTTP 请求方法,如 PUT 和 Delete 都会对服务器数据进行更修改,所以要先询问服务器,这也是为什么需要预请求)。
跨域请求中,浏览器自发的发起的预请求,浏览器会查询到两次请求,第一次的请求参数是 options ,以检测试实际请求是否可以被浏览器接受。 注意:Access-Control-Max-Age: 1000 表示这种允许跨域的请求在 1000 秒不需要再次预请求验证了,直接发起正式的请求就可以了。

带凭证的请求

跨域的 Ajax 请求是不会发送基于 Cookie 和 HTTP 认证信息的身份凭证,如果想要跨域的 Ajax 请求能够发送凭证信息需要在浏览器和服务器两端分别设置。

js
// browser
// 本质是在设置 xhr.withCredentials = true
axios.defaults.withCredentials = true

// server
res.setHeader('Access-Control-Allow-Credentials', true)

注意:当响应头设置 Access-Control-Allow-Credentials 为 true 后,Access-Control-Allow-Credentials 不能设置为 * 必须设置为具体源。

缓存 Cache-Control

参考文章 HTTP的缓存机制以及原理 - 掘金 (juejin.cn)

  • 可缓存性 private:私有缓存只能用于单独的用户(默认值,就是发起请求的用户),不能被代理服务器缓存; public:共享缓存可以被多个用户使用; no-cache:可以在本地或 proxy 缓存(如果服务器返回说可以使用本地的缓存,才可以真正使用本地缓存),需要经过服务器验证。

  • 到期 max-age=<seconds>:缓存的过期时间; s-maxage=<seconds>:代替 max-age,只会在代理服务器生效(如果和 max-age 同时设置了,优先读取代理服务器的 s-maxage); max-stale=<seconds>max-age 过期了,如果返回资源有 max-stale 设置,那么 max-stale 是发起请求这一方主动带到头中,意思是即便缓存过期(超出 max-age)了,只要在 max-stale 时间内可以使用过期的缓存,不需要到服务器请求新的资源。这个在浏览器里使用不到,一般请求和静态资源请求过程中并不会设置,只有在发起端有用,在服务端返回内容中设置它没有作用;

  • 重新验证 must-revalidata :指定如果页面是过期(max-age)的,则去服务器进行重新获取。 proxy-revalidata:用于缓存服务器中,指定如果页面或资源是过期(max-age)的,则去源服务器进行重新获取。 这两个指令并不常用,就不做过多的讨论了。

  • 其他 no-store:一切都不缓存(Firfox需要cache-control:no-cache; no-store;),永远去服务器获取新资源; no-transform:用于 proxy 服务器,告诉 proxy 服务器不要随意改动原始内容(比如不允许压缩等等)

代码示例:

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>
    <script src="./script.js"></script>
</body>
</html>
js
// 对 script 文件缓存
const http = require('http')
http.createServer(function (req, res) {
	console.log('request come', req.url)
	if(request.url === '/') {
		const html = fs.readFileSync('test.html', 'utf8')
		res.writeHead(200, {
			'Content-Type': 'text/html'
		})
		res.end(html)
	}
	if(request.url === '/script.js') {
		res.writeHead(200, {
			'Content-Type': 'text/javascript',
			'Cache-Control': 'max-age=20', // 20秒
		})
		res.end('console.log("load script")')
		// res.end('console.log("load script changed")')
	}
}).listen(8888)
console.log('server listening on 8888')

如何处理 script 在缓存时间内再次更改时,请求的 script 是上次的内容?最佳方案是 对 script 文件名使用 content hash ,这样可以确保浏览的内容都是最新的。

问题:CORS 如何缓存 options 预检请求? 增加响应头 Access-Control-Max-Age 即可,这样在一定的时间内,后续请求就不会再去发 options 预检请求了。

缓存(资源)验证

参考文章 浏览器HTTP缓存机制 - 掘金 (juejin.cn)

验证头:

  • Last-Modified 意思是上次修改时间,配合 If-Modified-SinceIf-Unmodified-Since 使用,如果某个请求返回的头有 Last-Modified ,下一次这个请求会携带 If-Modified-SinceIf-Unmodified-Since 访问服务器资源时,服务器会检查 Last-Modified,如果 Last-Modified 的时间早于或等于 If-Modified-Since 则会返回一个不带主体的 304 响应,否则将重新返回资源,就是对比资源上次修改时间以验证资源是否需要更新。 If-Modified-Since 是一个请求首部字段,并且只能用在 GET 或者 HEAD 请求中。

  • Etag 更加严格的验证,主要通过数字签名,对资源内容会产生唯一的签名,只要有任何修改,会产生新的签名。配合 If-MatchIf-None-Match 使用,对应的值就是 Etag 给的值,下次请求就携带 If-Match 数据对比资源的签名判断是否使用缓存。 ETag 优先级比 Last-Modified 高,同时存在时会以 ETag 为准。

代码示例:

js
// 对 script 文件缓存
const http = require('http')
http.createServer(function (req, res) {
	console.log('request come', req.url)
	if(request.url === '/') {
		const html = fs.readFileSync('test.html', 'utf8')
		res.writeHead(200, {
			'Content-Type': 'text/html'
		})
		res.end(html)
	}
	if(request.url === '/script.js') {
		res.writeHead(200, {
			'Content-Type': 'text/javascript',
			'Cache-Control': 'max-age=3600, no-cache',
			'Last-Modified': '20220312',
			'Etag': 'asd32gfhs2'
		})
		const etag = req.headers['if-none-match']
		if(etag === 'asd32gfhs2') {
			res.writeHeader(304, {
				'Content-Type': 'text/javascript',
				'Cache-Control': 'max-age=3600, no-cache',
				'Last-Modified': '20220312',
				'Etag': 'asd32gfhs2'
			})
			res.end('')
		} else {
			res.writeHeader(200, {
				'Content-Type': 'text/javascript',
				'Cache-Control': 'max-age=3600, no-cache',
				'Last-Modified': '20220312',
				'Etag': 'asd32gfhs2'
			})
			res.end('console.log("load script")')
		}
	}
}).listen(8888)
console.log('server listening on 8888')

问题:尝试去除 no-cache 或设置成 no-store ? 前者请求从 memory cache 获取,后者没有缓存

  • Cookie 通过服务端设置 Set-Cookie,浏览器会进行保存,下一次请求会自动在请求头中携带 Cookie 数据,它还是以键值对的方式存在,可以设置多个。

Cookie 有哪些属性呢? max-ageexpires 设置过期时间;Secure 只在使用 HTTPS 的时候发送;设置 HttpOnly 后 JS 无法通过 document.cookie 访问内容(考虑到安全问题,比如 CSRF 攻击)。

Cookie 使用方式:

js
const http = require('http')
http.createServer(function (req, res) {
	console.log('request come', req.url)
	if(request.url === '/') {
		const html = fs.readFileSync('test.html', 'utf8')
		res.writeHead(200, {
			'Content-Type': 'text/html',
			'Set-Cookie': ['id=121212', 'uuid=34']
		})
		res.end(html)
	}
}).listen(8888)
console.log('server listening on 8888')
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>Cookie</title>
</head>
<body>
    <script>
	    console.log(document.cookie) // id=121212
    </script>
</body>
</html>

在 Network 面板看到 Response Headers 里面有两个 Set-Cookie

Cookie 是有过期时间的,如果没有设置过期时间,会在浏览器关闭之前一直有效,关闭浏览器再次打开访问,在 Network 面板看到并没有携带 Cookie ,刷新会出现。

js
res.writeHead(200, {
	'Content-Type': 'text/html',
	'Set-Cookie': ['id=121; max-age=10', 'uuid=34'] // 10秒
})

10 秒后,再次请求,在 Network 面板看到 Request Headers 只有Cookie: uuid=34 ,没有 id=121 。 如果再次设置 HttpOnly 会怎样?

js
res.writeHead(200, {
	'Content-Type': 'text/html',
	'Set-Cookie': ['id=121; max-age=10', 'uuid=34; HttpOnly']
})
html
<script>
	console.log(document.cookie) // id=121 没有 uuid=34
</script>

Cookie 的 domain 设置: 用于指定域可以访问 Cookie 数据,其他域不可访问。 问题:b.test.com 访问 a.test.com 的 Cookie 怎么办?那 a.test.com 访问 test.com 的 Cookie 怎么办(二级域名访问一级域名)?

js
// 开发环境下 设置一下 hosts
// 127.0.0.1  a.test.com
// 127.0.0.1  b.test.com
// 127.0.0.1  test.com
const host = req.headers.host
// 问题1
if(host === 'a.test.com') {
	res.writeHead(200, {
		'Content-Type': 'text/html',
		'Set-Cookie': [
			'id=121; max-age=10', 
			'uuid=34'
		]
	})
}

// 问题2
if(host === 'test.com') {
	res.writeHead(200, {
		'Content-Type': 'text/html',
		'Set-Cookie': [
			'id=121; max-age=10', 
			'uuid=34; domain=test.com' //二级可以访问一级
		]
	})
}
  • Session 一般是使用 Cookie 保存 Session,把用户登录的信息存放到请求的 Cookie 中,保存到 Session 中,下一次请求时读取 Cookie 值与 Session 对比。

HTTP 长连接

HTTP 的请求建立在 TCP 连接之上,TCP 的连接分为长连接和短连接(相应的文章 TCP 长连接与短连接 - 知乎 (zhihu.com))。 打开 baidu.com 在 Network 面板看到有不少相同的 Connection ID (复用 TCP 连接)。HTTP1.1 中连接在 TCP 连接上面发送请求是有先后顺序的,比如说有 10 个请求不可以并发地在一个 TCP 连接上发送,在这一个 TCP 连接上只能一个接着一个发送请求(串行),好在浏览器可以并发地创建 TCP 连接(Chrome 一次性并发 6 个,如果有 10 个,剩下的 4 个需要等前面 6 个中的 TCP 连接空出来使用)。 怎么知道这个连接是长连接?Connection: Keep-Alive

代码示例:

html
<body>
	<img src="./img-1.png" alt="" />
	<img src="./img-2.png" alt="" />
	<img src="./img-3.png" alt="" />
	<img src="./img-4.png" alt="" />
	<img src="./img-5.png" alt="" />
	<img src="./img-6.png" alt="" />
	<img src="./img-7.png" alt="" />
</body>
js
const http = require('http')
http.createServer(function (req, res) {
	console.log('request come', req.url)
	const html = fs.readFileSync('test.html', 'utf8')
	const img = fs.readFileSync('img.png', 'utf8')
	if(request.url === '/') {
		
		res.writeHead(200, {
			'Content-Type': 'text/html',
		})
		res.end(html)
	} else {
		res.writeHead(200, {
			'Content-Type': 'image/png',
		})
		res.end(img)
	}
}).listen(8888)
console.log('server listening on 8888')

截图如下:

稍微调整一下:

html
<body>
	<img src="./img-1.png" alt="" />
	<img src="./img-2.png" alt="" />
	<img src="./img-3.png" alt="" />
	<img src="./img-4.png" alt="" />
	<img src="./img-5.png" alt="" />
	<img src="./img-6.png" alt="" />
	<img src="./img-7.png" alt="" />
	<img src="./img-10.png" alt="" />
	<img src="./img-20.png" alt="" />
	<img src="./img-30.png" alt="" />
	<img src="./img-40.png" alt="" />
	<img src="./img-50.png" alt="" />
	<img src="./img-60.png" alt="" />
	<img src="./img-70.png" alt="" />
	<img src="./img-11.png" alt="" />
	<img src="./img-21.png" alt="" />
	<img src="./img-31.png" alt="" />
	<img src="./img-41.png" alt="" />
	<img src="./img-51.png" alt="" />
	<img src="./img-61.png" alt="" />
	<img src="./img-71.png" alt="" />
</body>

结果更明显,有不少连接复用之前的 TCP 连接。请求头里面 Request Headers 的 connection 是 keep-alive ;如果为 close ,则表示这个请求连接完成,TCP 连接会关闭掉。 看一下 close 的效果:

js
if(request.url === '/') {
		res.writeHead(200, {
			'Content-Type': 'text/html',
			'Connection': 'close',
		})
		res.end(html)
	} else {
		res.writeHead(200, {
			'Content-Type': 'image/png',
			'Connection': 'close',
		})
		res.end(img)
	}

结果是没有重复的 Connection ID

在 HTTP2 中,会复用信道,在一个 TCP 连接上面可以并发地发送 HTTP 请求,或许请求一个网页只需要一个 TCP 连接就够了(可以打开 google.com 查看 Connection ID)。

数据协商

客户端发送请求给服务端,客户端会声明请求希望拿到的数据的格式和限制,服务端会根据请求头信息,来决定返回的数据。 分为两类:请求(Accept)和返回(Content)。

请求类: Accept (指定声明想要的数据类型,根据 MIME-Type 类型决定)。 Accept-Encoding 定义以什么样的数据编码进行传输,主要限制服务端如何进行数据的压缩(压缩算法很多)。 Accept-Language 指定使用什么语言。 User-Agent 表示浏览器相关的信息,移动端和 PC 端的浏览器不一样。

返回类: Content-Type 对应 AcceptAccept 可以设置好几种数据类型,Content-Type 从中选择真正的数据类型进行返回。 Content-Encoding 对应 Accept-EncodingContent-Language 对应 Accept-Language

代码示例:

js
// server.js
const http = require('http')
const fs = require('fs')
const zlib = require('zlib')

http.createServer(function (request, response) {
  console.log('request come', request.url)
  
  const html = fs.readFileSync('test.html') // 这里没有设置 utf8,读出来是 buffer,zlib.gzipSync 需要这个 buffer 格式
  response.writeHead(200, {
    'Content-Type': 'text/html', // 可以搜索 MIME Type
    // 'X-Content-Options': 'nosniff' // 很少使用 
    // 'Content-Encoding': 'gzip' // 还有 deflate、br
  })
  // response.end(zlib.gzipSync(html))
  response.end(html)
}).listen(8888)

console.log('server listening on 8888')

如何发送 Content-Type ?

html
<body>
	<form action="/form" enctype="application/x-www-form-urlencoded">
		<input type="text" name="name" />
		<input type="password" name="password" />
		<input type="submit" />
	</form>
	
	<form action="/form" method="POST" enctype="application/x-www-form-urlencoded">
		<input type="text" name="name" />
		<input type="password" name="password" />
		<input type="submit" />
	</form>
	
	<form action="/form" id="form" method="POST" enctype="multipart/form-data">
		<input type="text" name="name" />
		<input type="password" name="password" />
		<input type="file" name="file" />
		<input type="submit" />
	</form>
	<!-- 上面的请求在浏览器里面会进行跳转,请求的具体信息看不到,所以下面脚本需要手动触发请求 -->
	<script>
		let form = document.getElementById('form')
		form.addEventListener('submit', function(e) {
			e.preventDefault()
			let formData = new FormData(form)
			fetch('/form', {
				method: 'POST',
				body: formData
			})
		})
	</script>
</body>

使用 multipart/form-data 结果如下:

js
Request Headers
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary39Ug3FSPIBvDYZd6

Request Payload
------WebKitFormBoundary39Ug3FSPIBvDYZd6
Content-Disposition: form-data; name="name"

sdfs
------WebKitFormBoundary39Ug3FSPIBvDYZd6
Content-Disposition: form-data; name="password"

sdfs
------WebKitFormBoundary39Ug3FSPIBvDYZd6
Content-Disposition: form-data; name="file"; filename="1536973449110.png"
Content-Type: image/png


------WebKitFormBoundary39Ug3FSPIBvDYZd6--

form[enctype] 有三种: application/x-www-form-urlencoded:如果 form 表单没有设置 method ,那么默认发出的请求方法是 GET 请求,参数是以 query 的格式组织。如果 method 设置成 POST ,那么发出的请求头里 Content-Type: application/x-www-form-urlencoded ,请求体里面是 Form Data 其内容是 name=hello&password=123456

multipart/form-data:表示请求分为多个部分(比如上传文件),发出的请求头里 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryxxxxxxboundary 用来分割表单提交数据的各个部分(服务端拿到表单数据后,根据这个分割字符串,进行数据分割)。

text/plain:将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。

问题:text/plaintext/html 的区别? text/html 将文件的 Content-Type 设置为 text/html 的形式,浏览器在获取到这种文件时会自动调用 html 解析器对文件进行相应的处理。

Redirect

通过 url 访问某个路径请求资源时,发现资源不在 url 所指定的位置,这时服务器要告诉浏览器,新的资源地址,浏览器再重新请求新的 url,从而拿到资源。 若服务器指定了某个资源的地址,现在需要更换地址,不应该立刻废弃掉 url,如果废弃掉可能直接返回 404,这时应该告诉客户端新的资源地址,所以有了 Redirect 。

代码示例:

js
// server.js
const http = require('http')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    response.writeHead(302, {  // or 301
      'Location': '/new' // 这里是同域跳转,只需要写路由
    })
    response.end()
  }
  if (request.url === '/new') {
    response.writeHead(200, {
      'Content-Type': 'text/html',
    })
    response.end('<div>this is content</div>')
  }
}).listen(8888)
console.log('server listening on 8888')

问题:Redirect 301 和 302 的区别? 302 临时跳转,每次请求仍然需要经过服务端指定跳转地址; 301 永久跳转。

使用 302 的效果:

js
const http = require('http')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    response.writeHead(302, {  
      'Location': '/new' 
    })
    response.end()
  }
  if (request.url === '/new') {
    response.writeHead(200, {
      'Content-Type': 'text/html',
    })
    response.end('<div>this is content</div>')
  }
}).listen(8888)
console.log('server listening on 8888')

每次访问 locahost:8888,都要经过服务端跳转,服务端通过 console.log 可以看到 //new 两次请求。

使用 301 的效果:

js
response.writeHead(301, {
  'Location': '/new'
})

访问 locahost:8888,第一次经过服务端跳转,服务端通过 console.log 可以看到 //new 两次请求;第二次经过服务端 console.log 只显示 /new ,没有再次经过服务器指定新的 Location

注意:使用 301 要慎重,一旦使用,服务端更改路由设置,用户如果不清理浏览器缓存,就会一直重定向。设置了 301,locahost 会从缓存中读取,并且这个缓存会保留到浏览器,当我们访问 8888 都会进行跳转。此时,就算服务端改变设置也是没有用的,浏览器还是会从缓存中读取。

CSP

全称:Content-Security-Policy ,内容安全策略。 有什么作用:限制资源获取、报告资源获取越权。

限制方式:default-src 限制全局、制定资源类型。

有哪些资源类型:connect-src img-src font-src media-src frame-src script-src manifest-src style-src ...

代码示例:

  • 限制内联 script
html
<body>
  <div>This is content</div>
  <!-- 内联脚本 -->
  <script>
    console.log('inline js')
  </script>
</body>
js
const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    const html = fs.readFileSync('test.html', 'utf8')
    response.writeHead(200, {
      'Content-Type': 'text/html',
      'Content-Security-Policy': 'default-src http: https:' // 只能通过 http 或 https 的方式加载
    })
    response.end(html)
  } else {
    response.writeHead(200, {
      'Content-Type': 'application/javascript'
    })
    response.end('console.log("loaded script")')
  }
}).listen(8888)
console.log('server listening on 8888')

浏览器控制台会报错: Refused to execute inline script because it violates the following Content Security Policy directive: "default-src http: https:". Either the 'unsafe-inline' keyword, a hash ('sha256-KU4m2rqHAFwi569te1RE5P3qW1O/qJ+m+gVo66Frm4k='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'script-src' was not explicitly set, so 'default-src' is used as a fallback.

  • 限制外链加载 script
js
// server.js
'Content-Security-Policy': 'script-src \'self\'' // 限制外链 script 只能是本域名下的
html
<script src="test.js"></script>  // 本域名下的可以使用
<script src="https://cdn.bootcss.com/jquery/3.3.1/core.js"></script>

可以看到报错信息 Refused to load the script 'https://cdn.bootcss.com/jquery/3.3.1/core.js' because it violates the following Content Security Policy directive: "script-src 'self'". 查看 Network 面板,发现在浏览器端就被 block 掉了,没有发送请求。

  • 限制指定某个网站
js
'Content-Security-Policy': 'script-src \'self\' https://cdn.bootcss.com' // 限制外链 script 只能是本域名下的,允许指定域名script加载

Network 看到 core.js 加载成功。

  • 限制 form 表单提交范围 form 不接受 default-src 的限制,可以通过 form-action来限制。 下面代码中 form 会调转到 http://baidu.com,通过 form-action限制浏览器会报错。
js
// server.js
'Content-Security-Policy': 'script-src \'self\'; form-action \'self\'' // 限制表单提交只能在本域下
html
<form action="http://baidu.com">
	<button type="submit">click me</button>
</form>

有报错信息: Refused to send form data to 'http://baidu.com/' because it violates the following Content Security Policy directive: "form-action 'self'".

  • 限制图片链接 通过全局限制 default-src 就可以实现。
js
'Content-Security-Policy': 'default-src \'self\'; form-action \'self\'' 
  • 限制 ajax 请求 通过 connect-src 。
js
'Content-Security-Policy': 'connect-src \'self\'; form-action \'self\'; report-uri / report' 
html
<script> 
	fetch('http://baidu.com')
</script>
  • 汇报
js
// server.js
'Content-Security-Policy': 'default-src \'self\'; form-action \'self\'; report-uri / report' 

Network 看到,发送的内容,是标准的 csp report 的内容。 报错信息: Refused to connect to 'http://baidu.com/' because it violates the following Content Security Policy directive: "connect-src 'self'".

  • 允许加载但汇报 使用 Content-Security-Policy-Report-Only
js
// server.js
'Content-Security-Policy-Report-Only': 'default-src \'self\'; form-action \'self\'; report-uri / report' 

资源会正常加载,但是汇报 Report-Only 相关的错误提醒。

  • 在 html 中使用 csp 和在服务端使用效果相同,最好在服务端做。
html
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; form-action 'self';">

report-uri 不允许在 html 的 meta 中使用,只能在服务端通过 head 进行设置。

Released under the MIT License.