前言
你好,我是YourBatman。
上篇文章(Cors跨域(一):深入理解跨域请求概念及其根因)用超万字的篇幅把Cors几乎所有概念都扫盲了,接下来将逐步提出解决方案等实战性问题以及查漏补缺。
本文主角是大家耳熟能详的Cookie,聊聊它在跨域情况下如何实现“共享”?大家都知道Cookie是需要遵守同源策略(SameSite)的,本文将以跨域Cookie信息共享为场景,进一步加深对Cors的了解。
本文提纲
版本约定
- JDK:8
- Servlet:4.x
- Tomcat:9.x
正文
Cookie是做web开发绕不过去的一个概念,即使随着JWT技术的出现它早已褪色不少,但依旧有其发光发热之地。譬如一些内网后台管理系统、Portal门户、SSO统一登录等场景…
如若你是新时代的程序员朋友,可能从未使用过Cookie,但肯定听过它的“传说”。作为本文的主角,那么我们就来先认识下这位“老朋友”吧。
重识Cookie
Cookie中文名:曲奇饼干。
当然,我们在与他人沟通时可不要使用中文名,还是使用Cookie本名吧~
什么是Cookie
一个看似简单,实则不好回答的一个问题。
众所周知,Http是无状态协议(Tips:不要问我什么叫无状态哈),每次请求都是对等的(从0开始的),服务器不知道用户上一次做了什么,这严重阻碍了 交互式 Web应用程序的实现。有些场景服务端需要知道用户的访问状态(如登录状态),这个时候怎么办?
针对这种场景其实很容想到解决办法:你来访问我服务端的时候,我给你一个“东西”,然后下次你再访问我(注意是访问我才携带哦)的时候把它带过来我就知道是你啦,简单交互图如下:
这里交互中所指的“东西”,在Web领域它就是Cookie。Cookie就是用来绕开HTTP的无状态性的手段,它是Web的标准技术(是web标准而不局限于只是Servlet),隶属于RFC6265,现今的所有的浏览器、服务器均实现了此规范。
用一个20年前就用的比喻再补充解释下:你去银行卡里存钱,第一次去银行银行会给你办一张银行卡(里面存放着你的姓名、身份证、余额等信息)。下次你再去银行的时候,只需带着这张银行卡银行就可以“识别”你,从而就可以存/取钱了。这里的银行卡就类同于Http请求里的Cookie概念。
基于此银行(卡)的比喻举一反三,类比解释同域Cookie、不同域Cookie、跨域Cookie共享的含义:
- 同域Cookie:每次访问的是同一个域下的不同页面、API(每次去的是同一家银行的不同网点,带上这家银行卡即可识别身份)
- 不同域Cookie:同一个浏览器窗口内可能同时访问A网站和B网站,它们均有各自的Cookie,但访问A时只会带上A的Cookie(你可能有不同银行的多张银行卡,而去某个银行时只有带着他们家的银行卡才去有用嘛)
- 跨域Cookie共享:访问A站点时已经登录从而保存姓名、头像等基本信息,这时访问该公司的B站点时就自然而然的能显示出这些基本信息,也就是实现信息共享(在银联体系中A银行办理的卡也能在B银行能取出钱来,也就是实现余额“共享”)
说明:Cookie实现跨域共享要求根域必须是一样才行,比如都是www.baidu.com和map.baidu.com的根域都是 baidu.com。这道理就相当于只有加入了银联的银行才能用银行卡去任意一家银联成员行取钱一样
Cookie的交互机制
下面这张图完整的说明了Cookie的交互机制,共四个步骤:
- 浏览器(客户端)发送一个请求到服务器
- 服务器响应。并在HttpResponse里增加一个响应头:Set-Cookie
- 浏览器保存此cookie在本地,然后以后每次请求都带着它,且请求头为:Cookie
- 服务器收到请求便可读取到此Cookie,做相应逻辑后给出响应
由此可见,Cookie用于保持请求状态,而这个状态依赖于浏览器端(客户端)的本地存储。
代码示例
概念聊了有一会了,写几句代码放松放松。下面演示一下这个交互过程:
服务端代码:首次请求种植Cookie,以后(请求携带了)就只打印输出Cookie内容
/** * 在此处添加备注信息 * * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> * @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<Cookie> 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的生命周期
缺省情况下,Cookie的生命周期是Session级别(会话级别)。若想用Cookie进行状态保存、资源共享,服务端一般都会给其设置一个过期时间maxAge,短则1小时、1天,长则1星期、1个月甚至永久,这就是Cookie的生命(周期)。
Cookie的存储形式,根据其生命周期的不同而不同。这由maxAge属性决定,共有这三种情况:
- maxAge > 0:cookie不仅内存里有,还会持久化到硬盘,也叫持久Cookie。这样的话即使你关机重启(甚至过几天再访问),这个cookie依旧存在,请求时依旧会携带
- maxAge < 0:一般值为-1,也就临时Cookie。该Cookie只在内存中有(如session级别),一旦管理浏览器此Cookie将不复存在。值得注意的是:若使用无痕模式访问也是不会携带此Cookie的哟
- maxAge = 0:内存中没有,硬盘中也没有了,也就立即删除Cookie。此种case存在的唯一目的:服务浏览器可能的已存在的cookie,让其立马失效(消失)
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虽然有不少优点,但它也有如下明显劣势:
- 每次请求都会携带Cookie,这无形中增加了流量开销,这在移动端对流量敏感的场景下是不够友好的
- Http请求中Cookie均为明文传输,所以安全性成问题(除非用Https)
- Cookie有大小限制,一般最大为4kb,对于复杂的需求来讲就捉襟见肘
由于Cookie有不安全性和众多劣势,所以现在JWT大行其道。当然喽,很多时候Cookie依旧是最好用的,比如内网的管理端、Portal门户、UUAP统一登录等。
Cookie的域和路径
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了。
- domain:创建此cookie的服务器主机名(or域名),服务端设置。但是不能将其设置为服务器所属域之外的域(若这都允许的话,你把Cookie的域都设置为baidu.com,那百度每次请求岂不要“累死”)
- 注:端口和域无关,也就是说Cookie的域是不包括端口的
path:域下的哪些目录可以访问此cookie,默认为/,表示所有目录均可访问此cookie
跨域Cookie共享
三个关键词:跨域、Cookie、共享。Cookie是数据载体,跨域是场景,共享是需求。