昨天(第4天),我们实现了第一个APIecho,并通过httpie成功调用。今天我们来尝试一下用浏览器来调用是否还能成功调通?答案是否定的。原因就是我们今天要学习的浏览器同源策略导致的,同时引出了CORS实现跨域访问。本文主要内容包括:CORS同源策略支持CORS跨域访问预检请求PreflightRequest 浏览器调用API的尝试 先来回顾一下,在day4文章的实例中,我们已经通过httpie成功的调用了echo接口,如下图: 非浏览器成功调用 下面我们来写个JavaScript脚本,通过浏览器来调用echo接口。 直接运行HTML文件调用接口 新建一个html文件,代码如下:!DOCTYPEhtmlhtmllangenheadmetacharsetUTF8metahttpequivXUACompatiblecontentIEedgemetanameviewportcontentwidthdevicewidth,initialscale1。0titleDocumenttitleheadbodybuttononclickcallEcho()callapiutilechobuttonbodyhtml 保存后,双击用浏览器打开该HTML,点击按钮即可触发调用echo接口,调用结果如下: 执行失败,报被CORS策略阻塞 通过HTTP服务器调用接口 下面是VUE代码,添加到VUE项目中,执行yarndev命令运行,点击按钮,触发调用echo接口。templateelbuttontypeprimaryclickcallApiEcho调用apiutilechoelbuttontemplate 这种方式运行是有HTTP服务器的,调用的结果如下图,它更清晰的指出了源域IP: 执行失败,报被CORS策略拒绝 跨域(CORS)和同源策略(SOP) CORS:CrossOriginResourceSharing,俗称跨域,全称跨域资源共享,是每个WEB项目开发人员,不管是前端还是后端,都会遇到的问题。 跨域问题是浏览器为了安全才有的,使用其它客户端工具,比如httpie、curl等都没有该问题。 Web浏览器实现了一种被称为同源策略的安全机制,防止网页在不同域中访问资源,包括API;而CORS提供了一种安全的方式,允许一个域(源域,用origin表示)调用另一个域中的资源,即允许在一个域下运行的web应用程序访问另一个域。 SOP:SameOriginPolicy,同源策略。同源是指协议、域名和端口都相同,任何一个不相同都不算同源。 跨域请求和响应 CORSRequest有两类:simplerequests和preflightrequests,浏览器自己会决定使用哪种请求,无需我们人为的干预。我们需要了解该机制即可。简单请求Simplerequests(GET,POST,HEAD) 当请求满足下面条件时,浏览器将该请求视为simple请求: (1)使用GET、POST或HEAD请求 (2)使用CORSsafelistedheader (3)使用ContentTypeheader值为applicationxwwwwformurlencoded、multipartformdata或textplain (4)没有在任何XMLHttpRequestUpload对象上注册事件侦听器 (5)请求中未使用ReadableStream对象 满足这些条件的请求,则被允许继续正常执行,不会被阻止,并且在返回响应时检查AccessControlAllowOriginheader。预检请求Preflightrequests(OPTIONS) 如果不是simplerequest,浏览器将使用HTTPOPTIONS方法自动发出预检请求。预检请求用于确定服务端确切的CORS能力,判断服务端是否理解预期的CORS协议。如果OPTIONS调用的结果指示无法请求,则不会再发起对服务端的实际请求。 预检请求将请求模式设置为OPTIONS,并设置一组header来描述接下来的请求: (1)AccessControlRequestMethod:请求的预期方法(如GET、POST) (2)AccessControlRequestHeaders:将随请求一起发送的自定义header的名称 (3)Origin:currentorigin 预检请求举例:curliXOPTIONSlocalhost:3031apiechoHAccessControlRequestMethod:GETHAccessControlRequestHeaders:ContentType,AcceptHOrigin:http:localhost:3030 这个例子表示,客户端向服务端询问:我想向从http:localhost:3030向http:localhost:3031apiecho发起一个Get请求,该请求包含ContentType,Acceptheader,是否可以? 服务器判断后,在响应中包含一些类似AccessControl的header,以指示是否允许随后的请求。这些header有如下几种: (1)AccessControlAllowOrigin:表示允许发请求的源,表示允许所有源访问 (2)AccessControlAllowMethods:允许的HTTPmethods,以逗号分隔 (3)AccessControlAllowHeaders:允许发送的customheaders,以逗号分隔 (4)AccessControlMaxAge:preflightrequest预请求响应结果的缓存时长,在这段时长内,再次调用该接口不用进行预请求调用。 一个preflight请求的Response可能是像下面这样的:HTTP1。1204NoContentAccessControlAllowOrigin:AccessControlAllowMethods:GET,HEAD,PUT,PATCH,POST,DELETEVary:AccessControlRequestHeadersAccessControlAllowHeaders:ContentType,AcceptContentLength:0Date:Fri,05Apr202311:41:08GMTConnection:keepalive 看到这里,是不是已经明白为什么在浏览器调试工具网络窗口中经常看到发出一次请求,有两条log的原因了吧? 一次调用显示两条log 对的,没错就是因为其中一条是预检请求,另外一条才是真实请求。 后端跨域的实现添加跨域配置 后端跨域配置 添加CorsConfig。java文件,代码如下。importorg。springframework。context。annotation。Configuration;importorg。springframework。web。servlet。config。annotation。CorsRegistry;importorg。springframework。web。servlet。config。annotation。WebMvcConfigurer;ConfigurationpublicclassCorsConfigimplementsWebMvcConfigurer{OverridepublicvoidaddCorsMappings(CorsRegistryregistry){registry。addMapping()WhenallowCredentialsistrue,allowedOriginscannotcontainthespecialvaluesincethatcannotbesetontheAccessControlAllowOriginresponseheader。Toallowcredentialstoasetoforigins,listthemexplicitlyorconsiderusingallowedOriginPatternsinstead。。allowedOrigins()。allowedOriginPatterns()。allowedMethods(GET,POST,DELETE)。allowedHeaders()restfulapi是无状态的,无需缓存cookie等信息。allowCredentials(true)AccessControlMaxAgeheader表明预检请求响应的有效时间。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。在预检中,浏览器发送的头中包含有HTTP方法和真实请求中会用到的头。也就是说对于同样的请求,在maxage规定的时间内就不用再次通过预检了,就可以直接请求了,单位s。maxAge(1800);}}跨域调用 支持跨域之后,我们再分别用httpie和浏览器来调用echo接口看看有什变化。 跨域前后httpie调用结果对比 跨域前后chrome调用结果对比 从上面实践可以看到,支持跨域后,浏览器能成功调用echo接口了。 预检请求(PreflightRequest)实战 最后我们再来增加一个delete接口,来亲自见识一下预检请求。 从上面的解释,我们知道预检请求不能是GET,POST这种请求,而我们已有的echo接口是一个POST请求,所以需要新增一个符合条件的接口,这里我们增加一个HTTPDELETE接口来演示。 增加delete接口添加接口 增加一个新的Controller,并添加delete接口,采用DeleteMapping表示使用HTTPDELETE方法来请求。importcom。example。springdemo。dto。ProductQueryDto;importcom。example。springdemo。model。Result;importorg。springframework。web。bind。annotation。;RestControllerRequestMapping(apiproduct)publicclassProductController{DeleteMapping(delete)publicResultStringdelete(RequestBodyProductQueryDtoparam){System。out。printf(〔product〕〔del〕s,param。getId());ResultStringresnewResult();returnres。setData(param。getId());}} 增加ProductQueryDto,用于接口传参。这里大家先不用去管什么是DTO,什么是Model,后面的分享会逐一说明的。importlombok。Getter;importlombok。Setter;GetterSetterpublicclassProductQueryDto{privateStringid;}添加调用delete接口的JavaScript代码buttononclickcallDeleteProduct()callapiproductdeletebutton运行并调用delete接口 一次调用,有两条log 点击这两条数据,查看两次调用的请求头和请求响应,对比如下图: 预检请求和真正请求的对比 小结,今天掌握了浏览器的同源策略SOP,实现跨域访问CORS的方法,学习了预检请求PreflightRequest和SimpleRequest,自己定义了一个符合需要PreflightRequest的接口,通过代码亲自做了实践。