你好,我是YourBatman。
创新互联成立十载来,这条路我们正越走越好,积累了技术与客户资源,形成了良好的口碑。为客户提供网站设计、网站建设、网站策划、网页设计、域名申请、网络营销、VI设计、网站改版、漏洞修补等服务。网站是否美观、功能强大、用户体验好、性价比高、打开快等等,这些对于网站建设都非常重要,创新互联通过对建站技术性的掌握、对创意设计的研究为客户提供一站式互联网解决方案,携手广大客户,共同发展进步。
上篇文章(Cors跨域(一):深入理解跨域请求概念及其根因)用超万字的篇幅把Cors几乎所有概念都扫盲了,接下来将逐步提出解决方案等实战性问题以及查漏补缺。
本文主角是大家耳熟能详的Cookie,聊聊它在跨域情况下如何实现“共享”?大家都知道Cookie是需要遵守同源策略(SameSite)的,本文将以跨域Cookie信息共享为场景,进一步加深对Cors的了解。
Cookie是做web开发绕不过去的一个概念,即使随着JWT技术的出现它早已褪色不少,但依旧有其发光发热之地。譬如一些内网后台管理系统、Portal门户、SSO统一登录等场景...
如若你是新时代的程序员朋友,可能从未使用过Cookie,但肯定听过它的“传说”。作为本文的主角,那么我们就来先认识下这位“老朋友”吧。
Cookie中文名:曲奇饼干。
当然,我们在与他人沟通时可不要使用中文名,还是使用Cookie本名吧~
一个看似简单,实则不好回答的一个问题。
众所周知,Http是无状态协议(Tips:不要问我什么叫无状态哈),每次请求都是对等的(从0开始的),服务器不知道用户上一次做了什么,这严重阻碍了 交互式 Web应用程序的实现。有些场景服务端需要知道用户的访问状态(如登录状态),这个时候怎么办?
针对这种场景其实很容想到解决办法:你来访问我服务端的时候,我给你一个“东西”,然后下次你再访问我(注意是访问我才携带哦)的时候把它带过来我就知道是你啦,简单交互图如下:
这里交互中所指的“东西”,在Web领域它就是Cookie。Cookie就是用来绕开HTTP的无状态性的手段,它是Web的标准技术(是web标准而不局限于只是Servlet),隶属于RFC6265,现今的所有的浏览器、服务器均实现了此规范。
用一个20年前就用的比喻再补充解释下:你去银行卡里存钱,第一次去银行银行会给你办一张银行卡(里面存放着你的姓名、身份证、余额等信息)。下次你再去银行的时候,只需带着这张银行卡银行就可以“识别”你,从而就可以存/取钱了。这里的银行卡就类同于Http请求里的Cookie概念。
基于此银行(卡)的比喻举一反三,类比解释同域Cookie、不同域Cookie、跨域Cookie共享的含义:
说明:Cookie实现跨域共享要求根域必须是一样才行,比如都是www.baidu.com和map.baidu.com的根域都是 baidu.com。这道理就相当于只有加入了银联的银行才能用银行卡去任意一家银联成员行取钱一样
下面这张图完整的说明了Cookie的交互机制,共四个步骤:
由此可见,Cookie用于保持请求状态,而这个状态依赖于浏览器端(客户端)的本地存储。
概念聊了有一会了,写几句代码放松放松。下面演示一下这个交互过程:
服务端代码:首次请求种植Cookie,以后(请求携带了)就只打印输出Cookie内容
- /**
- * 在此处添加备注信息
- *
- * @author YourBatman. Send email to me
- * @site https://yourbatman.cn
- * @date 2021/6/9 10:36
- * @since 0.0.1
- */
- @Slf4j
- @WebServlet(urlPatterns = "/cookie")
- public class CookieServlet extends HttpServlet {
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- String requestURI = req.getRequestURI();
- String method = req.getMethod();
- String originHeader = req.getHeader("Origin");
- log.info("收到请求:{},方法:{}, Origin头:{}", requestURI, method, originHeader);
- // 读取Cookie
- List
myCookies = new ArrayList<>(); - if (req.getCookies() != null) {
- myCookies = Arrays.stream(req.getCookies()).filter(c -> c.getName().equals("name") || c.getName().equals("age")).collect(toList());
- }
- if (myCookies.isEmpty()) { // 种植Cookie
- Cookie cookie = new Cookie("name", "YourBatman");
- // cookie.setDomain("baidu.com");
- cookie.setMaxAge(3600);
- resp.addCookie(cookie);
- cookie = new Cookie("age", "18");
- cookie.setMaxAge(3600);
- resp.addCookie(cookie);
- } else {
- myCookies.stream().forEach(c -> {
- log.info("name:{} value:{} domain:{} path:{} maxAge:{} secure:{}", c.getName(), c.getValue(), c.getDomain(), c.getPath(), c.getMaxAge(), c.getVersion(), c.getSecure());
- });
- }
- resp.getWriter().write("hello cookie...");
- }
- }
浏览器访问:http://localhost:8080/cookie,可以看到响应里带有Cookie头信息Set-Cookie告知浏览器要保存此Cookie,如下所示:
浏览器收到响应,并且依照Set-Cookie这个响应头,在本地存储上此Cookie(至于存在内存还是硬盘上,请参照文下的生命周期部分分解):
说明:除了name和age之外的cookie键值对不用关心,由于使用IDEA作为服务器交互的缘故才产生了它们再次发送本请求,它会将此域的Cookie全都都携带发给后端服务器,如下图所示:
服务端打印输出:可以看到服务端收到浏览器发送过来的Cookie了
- INFO c.y.cors.java.servlet.CookieServlet - 收到请求:/cookie,方法:GET, Origin头:null
- INFO c.y.cors.java.servlet.CookieServlet - name:name value:YourBatman domain:null path:null maxAge:-1 secure:0
- INFO c.y.cors.java.servlet.CookieServlet - name:age value:18 domain:null path:null maxAge:-1 secure:0
这就是Cookie一次完整的交互过程。
这里有个细节需要特别注意:name和age的maxAge属性值均为-1,表示这套cookie是会话级别的。也就是说你若换一个会话(如:重新打开浏览器的一个无痕窗口(不是标签页)),发送一个同样的请求http://localhost:8080/cookie,请求头里将看不到Cookie的任何踪影,服务端会给其生成一套新Cookie。如下图所示:
缺省情况下,Cookie的生命周期是Session级别(会话级别)。若想用Cookie进行状态保存、资源共享,服务端一般都会给其设置一个过期时间maxAge,短则1小时、1天,长则1星期、1个月甚至永久,这就是Cookie的生命(周期)。
Cookie的存储形式,根据其生命周期的不同而不同。这由maxAge属性决定,共有这三种情况:
Tips:请注意maxAge<0(负数)和maxAge=0的区别。前者会存在于内存,只有关闭浏览器or重启才失效;后者是立即删除当然啦,Cookie的生命周期除了受到后端设置的Age值来决定外,还有两种方式可“改变”它:
JavaScript操作Cookie
- // 取cookie:
- function getCookie(name) {
- var arr = document.cookie.split(';');
- for (var i = 0; i < arr.length; i++) {
- var arr2 = arr[i].split('=');
- var arrTest = arr2[0].trim(); // 此处的trim一定要加
- if (arrTest == name) {
- return arr2[1];
- }
- }
- }
- // 删cookie:
- function delCookie(name) {
- var exp = new Date();
- exp.setTime(exp.getTime() - 1);
- var cval = getCookie(name);
- if (cval != null) {
- document.cookie = name + "=" + cval + ";expires=" + exp.toGMTString();
- }
- }
浏览器的开发者工具操作Cookie
Cookie存储在客户端,正所谓客户端的所有东西都认为不是安全的,因此敏感的数据(比如密码)尽量不要放在Cookie里。Cookie能提高访问服务端的效率,但是安全性较差!
Cookie虽然有不少优点,但它也有如下明显劣势:
由于Cookie有不安全性和众多劣势,所以现在JWT大行其道。当然喽,很多时候Cookie依旧是最好用的,比如内网的管理端、Portal门户、UUAP统一登录等。
Cookie是不可以跨域的,隐私安全机制禁止网站非法获取其他网站(域)的Cookie。概念上咱不用长篇大论,举个例子你应该就懂了:
淘宝有两个页面:A页面a.taotao.com/index.html和B页面b.taotao.com/index.html,默认情况下A页面和B页面的Cookie是互相独立不能共享的。若现在有需要共享(如单点登录共享token ),我们只需要这么做:将A/B页面创建的Cookie的path设置为“/”,domain设置为“.taobtao.com”,那么位于a.taotao.com和b.taotao.com域下的所有页面都可以访问到这个Cookie了。
注:端口和域无关,也就是说Cookie的域是不包括端口的
三个关键词:跨域、Cookie、共享。Cookie是数据载体,跨域是场景,共享是需求。
前端页面:发送跨域请求,为了方便模拟这里发送跨域的简单请求即可(还不知道什么叫简单请求?戳这里)
Cookie交互机制(跨域)
前端页面托管在本地的63342端口上:http://localhost:63342/...
后端代码:后端接口托管在8080端口上:http://localhost:8080/...
这就是最简单的一个跨域场景,两个域具有相同的domain,因此才有共享Cookie的可能。
- /**
- * 在此处添加备注信息
- *
- * @author YourBatman. Send email to me
- * @site https://yourbatman.cn
- * @date 2021/6/9 10:36
- * @since 0.0.1
- */
- @Slf4j
- @WebServlet(urlPatterns = "/corscookie")
- public class CorsCookieServlet extends HttpServlet {
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- String requestURI = req.getRequestURI();
- String method = req.getMethod();
- String originHeader = req.getHeader("Origin");
- log.info("收到请求:{},方法:{}, Origin头:{}", requestURI, method, originHeader);
- // 读取Cookie
- List
myCookies = new ArrayList<>(); - if (req.getCookies() != null) {
- myCookies = Arrays.stream(req.getCookies()).filter(c -> c.getName().equals("name") || c.getName().equals("age")).collect(toList());
- }
- if (myCookies.isEmpty()) { // 种植Cookie
- Cookie cookie = new Cookie("name", "YourBatman");
- // cookie.setDomain("baidu.com");
- cookie.setMaxAge(3600);
- resp.addCookie(cookie);
- cookie = new Cookie("age", "18");
- cookie.setMaxAge(3600);
- resp.addCookie(cookie);
- } else {
- myCookies.stream().forEach(c -> {
- log.info("name:{} value:{} domain:{} path:{} maxAge:{} secure:{}", c.getName(), c.getValue(), c.getDomain(), c.getPath(), c.getMaxAge(), c.getVersion(), c.getSecure());
- });
- }
- setCrosHeader(resp);
- resp.getWriter().write("hello cookie...");
- }
- private void setCrosHeader(HttpServletResponse resp) {
- resp.setHeader("Access-Control-Allow-Origin", "http://localhost:63342");
- }
- }
点击按钮,发送请求:
注意看,服务端代码虽然resp.addCookie(cookie);添加了Cookie,但是Response响应里并没有Set-Cookie这个头哦。查看浏览器发现木有Cookie:
也许你会说,当然没有啦,因为Response里没有Set-Cookie头嘛,但我们代码里明明已经addCookie了呀。
这半截理论当然没问题,现在我在服务端程序里补充一个响应头:
- private void setCrosHeader(HttpServletResponse resp) {
- resp.setHeader("Access-Control-Allow-Origin", "http://localhost:63342");
- resp.setHeader("Access-Control-Allow-Credentials", "true");
- }
(重启服务端应用),再次发送请求,响应如下:
可以看到响应中已经有Set-Cookie响应头了,再次查看Cookie是否已被浏览器保存,同样的比比脸还干净:
浏览器没有存储Cookie。What?难道翻车了?No,下面教你如何解释以及怎么破?
这里要讨论的是跨域中Cookie的存储问题:默认情况下,浏览器是不会去为你保存下跨域请求响应的Cookie的。具体现象是:跨域请求的Response响应了即使有Set-Cookie响应头(且有值),浏览器收到后也是不会保存此cookie的。
要实现Cookie的跨域共享,有3个关键点:
为了满足这三个关键点,在实施层面就有三要素来指导我们开发来解决此类问题。
首先确保服务端能正确的在响应中有Set-Cookie响应头,这由Access-Control-Allow-Credentials: true来保证。因此服务端只需要做多加这一步即可:
- resp.setHeader("Access-Control-Allow-Credentials", "true");
Access-Control-Allow-Credentials该头是可选的,是个bool值,它若为true就有两个作用:
在跨域请求的响应中允许Set-Cookie响应头
浏览器收到响应后,浏览器根据此头判断是否让自己的withCredentials属性生效
所以就来到了第二个要素:XMLHttpRequest对象的withCredentials属性。该属性是一个Boolean类型,它指示了是否该使用类似cookies,authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求。
- var xhr = new XMLHttpRequest();
- ...
- xhr.withCredentials = true;
Jquery的Ajax写法与此不同,但底层原理一样官方的语言理解起来总是那么晦涩,翻译成人话:当异步对象设置了withCredentials=true时,浏览器会保留下响应的Cookie等信息,并且下次发送请求时将其携带。因此要指示浏览器存储Cookie并且每次跨域请求都携带,仅需加上此参数即可:
- $.ajax({
- url: "http://localhost:8080/corscookie",
- type: "GET",
- xhrFields: {
- withCredentials: true
- },
- crossDomain: true
- });
以上两个要素完成后,影响“结果”的还有最后一个要素。这个要素比较隐晦,也是很多同学/文章忽略的点。
服务端的Access-Control-Allow-Origin这个响应头的值不能是通配符*,而只能是具体的值。否则出现报错:
换句话讲:浏览器端跨域请求对象一旦开启withCredentials=true属性,服务端跨域Origin将不能再用*通配符,否则CORS error!
三要素都满足后(Access-Control-Allow-Credentials:true;Access-Control-Allow-Origin:http://localhost:63342;withCredentials=true),再次点击发送请求,结果如下:
完美。
总结上篇文章对Cors进行了全面介绍,本文以跨域Cookie共享为场景,很好的对跨域知识点进行了补充,并且也补足了Cors里一个重要的响应头Access-Control-Allow-Credentials的解释,相信通过本文同学你能加深对Web中Cookie的了解,以及跨域情况下Cookie信息如何共享。
本文转载自微信公众号「BAT的乌托邦」,可以通过以下二维码关注。转载本文请联系BAT的乌托邦公众号。
文章题目:Cors跨域(二):实现跨域Cookie共享的三要素
文章源于:http://www.shufengxianlan.com/qtweb/news20/539170.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联