现场两套系统,集成同一个单点登录。其中一个系统跳转到另外一个系统时浏览器会刷新两次。
奇怪的是打开F12,问题就不能复现。
打开控制台问题就解决了?真是奇怪!可能是控制台打开后,静态文件在浏览器端不再缓存造成的。
打开F12禁止控制台Network --> Disable cache
设置,果然问题能够复现,前端js
的请求确实是缓存的。
初步判断两次刷新原因:前端js
缓存,发送异步权限数据请求接口时没有权限(第一次请求刷新),然后重定向单点登录服务获取service ticket
,重新登录后,再次请求权限数据接口(第二次请求刷新),页面成功展示。
笔者对浏览器的行为不熟,这里只是猜测。
笔者系统单点登录实现的
CAS
接口,所以应用session
过期或者失效后需要从新从单点服务处获取service ticket
票据。
浏览器刷新两次fiddler
抓包如图:
第一次异步请求后,由于没有权限,302
重定向访问单点登录服务。这里控制台会提示跨域请求,跨域在跨域详解部分详细介绍。
前端明确说了,不是前端的问题,解决不了。
笔者公司的前端就是硬气。
在后台处理,后台是springboot
项目,增加配置:
spring:
resources:
cache:
cachecontrol:
max-age: 0
前端文件不会缓存,问题解决。
禁用缓存后,不会出现地址栏刷新两次现象,fiddler
抓包如图:
问题解决了,笔者对那个跨域的报错产生了兴趣。之前也看过不少跨域的文章,始终对跨域云里雾里。春节找出收藏的跨域文章,好好研读了一下,有所获,赶紧借此文分享出来。
做web
开发,工作中肯定接触过如下浏览器控制台报错:
No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
请求的跨域资源response
的header
中没有Access-Control-Allow-Origin
信息。
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://example.com/
浏览器同源策略禁止读取跨域资源。
Access to fetch at ‘https://example.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy.
在域http://localhost:3000
下访问域https://example.com
资源被禁止。
了解后面的内容后,就会明白这些报错的真正含义,也就能处理跨域问题了。
满足三个条件是同源,否则就是不同的域。
注意:
localhost
主机名虽然在网络层最终会解析为127.0.0.1
,但是对于浏览器同源策略来说,localhost
和127.0.0.1
是不同的主机名,二者不同则为跨域。
1989诞生的World Wide Web最初的html
只有纯文本。
世界上第一个web页面,只包含纯文本和超链接。
1993年引入<img>
标签,在html
渲染时允许加载图片资源,这样纯文本中就可以展示图片。像这样在html
中允许加载子资源(subresource
)的tag
还有:
<ifame>
<link>
<form>
<audio>
<video>
<script>
诚如上面
html
标签语义,所谓的“子资源(subresource
)”就是例如表单、文件、音视频、脚本等外部资源。
当一个域中包含有上面tag
的html
渲染时,就会加载subresource
,当这个subresource
和当前域不同源时,跨域请求就发生了。例如,在一个域中xmlhttprequest
(ajax
)请求另外一个域的接口时就是跨域请求。
一个域中加载另一个域的文件、音视频、脚本等subresource
时,大部分情况不会产生什么安全问题,但是有一些情况如果不做限制,就存在安全隐患。
例如一个域中提供了基于cookie/session
权鉴的发送邮件接口,该域允许任何域对该接口的跨域请求。那么恶意网站有可能在获取有效cookie
后任意调用发送邮件接口攻击网站。
可能有同学疑问:发送邮件接口如果需要权鉴认证才能成功调用,别人没有认证信息如何能成功调用呢?
实际上用户在浏览器端完成登录后,用户信息就存储在浏览器端(cookie
),这时打开恶意网站就有可能被恶意脚本携带用户信息完成攻击。
既然跨域请求有安全的问题,浏览器端就做了相关限制,称之为“浏览器同源策略”。
同源策略阻止读取跨域请求得到的资源。
这是广义的一个定义,实际上浏览器针对不同
subresource
有不同的限制策略,下面有做详细说明。
同源策略在1995年网景浏览器2.02
中引入,最开始是为了保护跨域DOM
而设计的。
跨域请求有三种形式:
同源策略的规则定义如下:
<ifame>
:跨域内嵌允许(需要合适的X-Frame-Options
)。<link>
:跨域内嵌允许(需要合适的Content-Type
)。<form>
:跨域写允许。<audio>
:跨域内嵌允许。<video>
:跨域内嵌允许。<script>
:跨域内嵌允许,某些api的调用可能会被禁止(例如ajax
跨域调用)。<img>
:跨域内嵌允许,通过JavaScript跨域读或者在<canvas>
中加载被禁止。浏览器的同源策略能解决很多安全的问题,但是其限制也带来了不便。
CORS(Cross-origin resource sharing
)跨域资源共享就是来放宽浏览器同源策略的严格限制,便于某些场景的使用。
同域请求,如图:
跨域请求,如图:
图中涉及到
preflight
请求下面详解。
这里重点讲述ajax
跨域请求(使用浏览器内置fetch()函数
)时,其请求过程和解决办法。
一个域中ajax
跨域请求另一个域的接口时,该请求的生命历程是由客户端和被请求资源服务端共同决定的。客户端的行为是浏览器同源策略指定的,被请求资源服务端行为由资源提供者具体实现提供。具体来说:
所谓的请求“生命历程”是指:该请求从浏览器发起,到服务端响应,再到浏览器读取响应结果并展示这个过程。
如果是“简单”的ajax
跨域请求,那么浏览器会放行该请求,如果服务端没有包含Access-Control-Allow-Origin
的header
信息,则浏览器会限制对请求到资源reponse
的读取。
如果是“复杂”的ajax
跨域请求,那么浏览器会先自行触发一个preflight
请求,根据服务端的相应header
信息决定是否放行客户端请求。
这里所谓的“简单”和“复杂”请求是相关规范定义的,一个“复杂”请求要至少满足如下其中一个条件:
GET
、POST
或者HEAD
请求。Accept
、Accept-Language
或者Content-Language
外的头信息。Content-Type
的值不是application/x-www-form-urlencoded
、 multipart/form-data
或者text/plain
。
2.
中说的头信息不包括浏览器自动给请求加入的header
信息,例如origin
。
接下来用Crystal启动http接口服务,看看不同跨域请求的生命历程:
Crystal
安装参考官方文档,脚本basic_greet.cr
为:
require "kemal"
port = 4000
get "/" do
"Hello world!"
end
get "/greet" do
"Hey!"
end
post "/greet" do |env|
name = env.params.json["name"].as(String)
"Hello, #{name}!"
end
post "/greet_str" do |env|
name = env.params
"Hello, 成功了!"
end
Kemal.config.port = port
Kemal.run
使用命令sudo crystal run src/basic_greet.cr
启动接口服务。
0). 同域下请求
从http://xx.22.27.215:4000/greet
接口域下发送“简单”的ajax
请求,如图:
同域下请求,一切正常,接口能发起成功并且浏览器能读取响应接口数据。
不同浏览器控制台实现方式不大相同(但实现的规范是一样的),这里以FireFox浏览器为测试浏览器。
1). “简单”的post
跨域请求
从天涯bbs论坛域下发送“简单”的ajax
请求,如图:
接口能发起成功。但是,如上图控制台报错,浏览器同源策略禁止读取远端资源,提示CORS header ‘Access-Control-Allow-Origin’ missing
,也就是说响应头信息中缺少Access-Control-Allow-Origin
信息。
这里之所以是“简单”请求,是因为
Content-Type
是text/plain
,参考上面“复杂”请求规则,不满足任意一个。
这里所以找一个
http
服务,是因为Crystal
接口是http
的,如果在https
域下调用,浏览器会直接禁止https
域下请求http
资源。
2). “复杂”的post
跨域写入
从天涯bbs论坛域下发送“复杂”的ajax
请求。
控制台报错如图:
网络抓包如图:
图中1.
为preflight
请求,请求方法为OPTIONS
。服务端目前没有实现OPTIONS
方法实现,提示404 Not Found
。
图中2.
为真正的POST
请求,因为1.
的preflight
请求没有获得同源策略规定的头信息,所以2.
的真正POST
请求被浏览器级别blocked
。
注意图中
2.
的OPTIONS
请求是浏览器发起的,浏览器会带上一些header
信息,比如:origin
、Access-Control-Reqest-Method
和Access-Control-Reqest-Headers
。
这种情况下的请求生命历程为:先行的preflight
请求404 Not Found
(“身先死”),真正的POST
请求没有发起成功(“出师未捷”)。也就是所谓的:“出师未捷身先死”。
那,“复杂”的跨域请求preflight
要求怎样的实现呢,才能满足浏览器CORS
协议的要求呢?
浏览器在发送preflight
后会寻找响应中的2个header
:
Access-Control-Allow-Methods
:CORS
协议允许的请求方法,例如GET
、POST
等。Access-Control-Allow-Headers
:CORS
协议允许的请求header
,例如Content-Type
等。针对“复杂”请求的生命历程来说,上面2个header
必须匹配客户端实际请求
信息,否则客户端实际请求
可能会被浏览器级别blocked
。说白了,服务端允许发送什么样方法的请求、什么样的头信息,客户端才能够成功发送。
preflight
的响应信息还可以返回2个header
,告诉客户端某些信息:
Access-Control-Max-Age
:设置preflight
请求能够缓存的秒数(默认值是5)。超过设置时间,“复杂”请求发起时浏览器会重新发起preflight
;在设置时间内,不再发起preflight
请求(使用缓存的preflight
请求)。Access-Control-Allow-Credentials
:设置客户端实际请求
是否能携带用户信息(例如cookie
)。如果服务端没有返回上面2个header
信息,不影响请求的生命历程。
也就是说,根据CORS
协议,preflight
请求响应头信息中要明确返回客户端实际请求
的方法(通过响应头信息Access-Control-Allow-Methods
值)和头信息(通过响应头信息Access-Control-Allow-Headers
值),这样浏览器才会同意发送客户端实际请求
。而preflight
请求响应头Access-Control-Max-Age
可以指定preflight
请求缓存的时间,默认就是5秒钟;preflight
响应头Access-Control-Allow-Credentials
告诉客户端,客户端实际请求
能够能携带用户信息,否则不能携带。
这里对
客户端实际请求
进行了代码块标注,是为了强调该请求避免和preflight
请求混为一谈。当一个“复杂”的跨域请求发起的时候,首先,浏览器会发送一个preflight
请求,“试探”一下服务端是否允许该跨域请求,如果允许,浏览器才允许该“复杂”请求(也就是这里所谓的客户端实际请求
)紧随preflight
请求之后发起,否则就会被浏览器blocked
。
那,按照要求实现下preflight
请求吧。
修改basic_greet.cr
,增加OPTIONS
实现:
options "/greet" do |env|
# Allow `POST /greet`...
env.response.headers["Access-Control-Allow-Methods"] = "POST"
# ...with `Content-type` header in the request...
env.response.headers["Access-Control-Allow-Headers"] = "Content-type"
end
重启接口服务,控制台重新请求,如图:
图中通过Status 200 OK
可以看出preflight
请求是成功的,但是下面控制台报错:响应头信息中缺少Access-Control-Allow-Origin
信息。也就是说,preflight
请求是成功了,CORS
协议要求必须存在的preflight
请求响应头信息也存在,但是由于Access-Control-Allow-Origin
头信息的缺失,浏览器同源策略限制读取请求响应内容。
修改basic_greet.cr
,响应信息头增加env.response.headers["Access-Control-Allow-Origin"] = "http://bbs.tianya.cn"
:
options "/greet" do |env|
# Allow `POST /greet`...
env.response.headers["Access-Control-Allow-Methods"] = "POST"
# ...with `Content-type` header in the request...
env.response.headers["Access-Control-Allow-Headers"] = "Content-type"
# ...from https://www.google.com origin.
env.response.headers["Access-Control-Allow-Origin"] = "http://bbs.tianya.cn"
end
重新请求:
preflight
请求成功,如图:
控制台还是有客户端实际请求
报错,不过这个错误就很熟悉了:
响应头信息中缺少Access-Control-Allow-Origin
信息被浏览器禁止读取响应内容。接口中增加响应header
信息:
post "/greet" do |env|
name = env.params.json["name"].as(String)
env.response.headers["Access-Control-Allow-Origin"] = "http://bbs.tianya.cn"
"Hello, #{name}!"
end
重新请求:
“复杂”请求的preflight
和客户端实际请求
都成功了,实现了“复杂”请求的跨域资源共享。
“复杂”请求整个生命历程,概括如图: