最近被朋友问起,Cookie 是什么?一时语塞,无法给出一个定义,我知道,该写一篇博客了........
Cookie
Cookie 是网景公司(Netscape)创造的一种网络会话状态跟踪技术。
根据以上定义,首先我们要明白什么是会话。会话指的是一个终端用户和交互系统进行通讯的过程。
对于 web 开发而言,会话基本上指的是用户打开浏览器第一次访问服务器到关闭浏览器的过程。因此通常会话会持续一段较长的时间,它由一组请求与响应组成,而这些请求和响应之间一定是需要数据传递的,即需要进行会话状态跟踪。然而 HTTP 协议是一种无状态的协议,即在不同的请求间是无法进行数据传递的。此时就需要一种可以进行请求间数据传递的会话跟踪技术,这就是 Cookie。
举个例子,如果没有会话跟踪技术,浏览器和服务器之间对话将会是下面这样:
浏览器:给我一个苹果。(浏览器第一次向服务器请求数据)
服务器:这是您的苹果。(服务器将浏览器请求的数据返回)
浏览器:你刚刚卖我的苹果都烂了,退钱!(浏览器第二次向服务器请求数据)
服务器:你谁?(服务器无法理解浏览器发送请求的含义)
浏览器:奸商 。。。。。。。
而在 Cookie 的加持下,它们的对话是这样的:
浏览器:给我一个苹果。(浏览器第一次向服务器请求数据)
服务器:这是您的苹果还有收据,请收好。(服务器返回浏览器请求的数据以及 Cookie)
浏览器:你刚刚卖我的苹果都烂了,这是收据。退钱!(浏览器带着 Cookie 第二次向服务器请求数据)
服务器:您的申请已处理,钱款已退回账号。(服务器结合 Cookie 解析浏览器的请求,返回处理结果)
浏览器:处理速度真快!
Cookie 生命周期
简单来说,Cookie 的生命周期是这样的:
- 浏览器第一次访问服务器后,服务器生成 Cookie 并响应给浏览器。
- 浏览器需要保存 Cookie ,不得轻易删除。
- 之后浏览器每次访问该服务器,都必须带上 Cookie。
- 用户关闭浏览器后,Cookie 从浏览器中消失。
以 JavaWeb 为例,在服务端生成 Cookie 并添加到响应中:
@WebServlet("/generateCookie")
public class GenerateCookie extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Cookie name = new Cookie("name", "zhangsan");
Cookie role = new Cookie("role", "admin");
response.addCookie(name);
response.addCookie(role);
}
}
使用浏览器发送相应的请求,就可以看到响应头中用 Set-Cookie 标志标识的 Cookie 信息。
浏览器在解析响应数据时,就可以通过 Set-Cookie 标志知道需要保存的 Cookie 信息,然后保存 Cookie。
Cookie 绑定路径
默认情况下,浏览器访问同一站点(也就是项目路径,本例中的站点为 /TestCookie)下的路径时,会自动带上该站点颁发的 Cookie。即使访问的是一个不存在路径。
可以通过为 Cookie 绑定路径,实现访问指定路径时才带上相应的 Cookie(需要精确到项目路径)。
name.setPath(request.getContextPath() + "/aaa");
role.setPath(request.getContextPath() + "/aaa/bbb");
再次生成 Cookie 之后,我们就可以看到 Cookie 所绑定的路径:
此时访问 Cookie 绑定的路径,浏览器就会只携带相应的 Cookie 信息:
到此为止,我们也就可以理解为什么前文说:
默认情况下,浏览器访问同一站点下的路径时,会自动带上该站点颁发的 Cookie。
因为项目路径是每个 Cookie 默认绑定的路径。
Cookie 有效期
如果我们不为 Cookie 设置有效期,那么默认情况下 Cookie 的有效期是会话结束。也就是用户关闭浏览器后 Cookie 就会消失,这种情况下,浏览器将 Cookie 保存在内存中。
可以通过为 Cookie 设置有效期让浏览器将 Cookie 保存到硬盘。
name.setMaxAge(30); // 30 秒有效期
role.setMaxAge(60 * 60); // 1 小时有效期
这里的有效期是一个整型值,单位是秒。
- 该值大于 0,表示将 Cookie 存放到客户端的硬盘;
- 该值小于 0,与不设置效果相同,表示将 Cookie 存放到浏览器的内存中。
重新生成 Cookie 后,就可以看到 Cookie 的有效期发生了变化:
Cookie 过期后,浏览器访问发送相应的请求时就不会带上 Cookie。
需要注意的是,即使 Cookie 过期后,还是存在于硬盘上。
Session
Session 即会话,是 web 开发中的一种会话状态跟踪技术。是的,你没有看错,这个定义在介绍 Cookie 时已经出现过了。
Cookie 和 Session 的不同之处在于,Cookie 将会话状态保存在客户端,而 Session 将会话状态保存在服务端。Cookie 和 Session 是同一个技术在两个地方的不同体现。
Session 使用
在 Java 中 Session 是一个接口(javax.servlet.http.HttpSession),我们无法创建只能通过 HttpServletRequest 对象提供的 getSession(boolean create) 方法获取 Session 对象。换而言之,Session 对象由 web 服务器负责管理。
getSession() 方法需要接收一个布尔值,含义为:
- true:若 Session 对象已存在,返回该 Session 对象;若不存在,生成新的 Seesion 对象返回。
- false:若 Session 对象已存在,返回该 Session 对象;若不存在,返回 null。
一般 getSession(true) 用于获取新生成 Session 对象,并为该对象设置属性。
@WebServlet("/generateSession")
public class GenerateSession extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(true);
session.setAttribute("logged", true);
}
}
而 getSession(false) 用于获取已存在 Session 对象,并获取其中的信息。
@WebServlet("/showSession")
public class ShowSession extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
boolean logged = false;
if (session != null) {
logged = (boolean) session.getAttribute("logged");
}
response.getWriter().print(logged ? "logged" : "not logged");
}
}
完成以上服务端代码的编写后,我们先使用 firefox 浏览器发送请求:/TestSession/generateSession,此时服务器会生成含有 logged 属性的 Session 对象。接着我们再次使用 firefox 浏览器发送请求:/TestSession/showSession,浏览器显示的页面如下所示:
此时如果我们使用另一款浏览器(例如 Edge)发送请求:/TestSession/showSession,那么我们将会看到如下页面:
这也就意味着对于服务器而言,不同的浏览器代表着不同的会话(Session 对象)。
Session 工作原理
那么服务器是如何识别不同的会话的呢?换而言之,服务器如何知道请求是由哪个浏览器发过来的呢?
我们先查看 firefox 浏览器的 Cookie 信息:
可以看到 Cookie 中存在名为 JSESSIONID 的信息,很明显这个信息并不是我们设置的,那么 JSESSIONID 只能是服务器生成的。而 JESSIONID 正是服务器可以识别不同会话的原因。
Session 的工作原理如下:
- 浏览器发送请求给服务器。
- 服务器发现请求携带的 Cookie 中没有 JSESSIONID,认为这是浏览器第一次发送请求。服务器生成一个 Session 对象,并生成名为 JSESSIONID 值为 Session 对象 id 的 Cookie,响应给浏览器。
- 浏览器再次请求服务器时,Cookie 中会携带 JSESSIONID。
- 服务器发现请求携带的 Cookie 中存在 JSESSIONID,在管理的 Session 对象中根据 JSESSIONID 找到对应的 Session 对象。
而服务器内部是通过 Map 管理 Session 对象:
看到这里你可能会有疑惑,之前演示 Cookie 的时候,怎么没有看到 JSESSIONID 。其实关键之处在于 getSession(boolean create) 方法,Session 对象的生成与获取均由该方法实现。
Session 失效
前面介绍 Cookie 的时候,提到 Cookie 有效期的概念,Session 也有类似的配置。
若使用 Tomcat 作为服务器,在 web.xm 文件中可以使用如下标签配置 Session 对象的超时时间(单位是分钟):
<session-config>
<session-timeout>30</session-timeout> <!-- Tomcat 默认超时时间是 30 分钟 -->
</session-config>
这里需要注意,超时时间的意思是距离该会话最后一次操作的时间间隔。也就是说,如果浏览器 29 分钟内没有向服务器发送请求,但是 29 分 59 秒访问了服务器,那么此次会话的超时时间又会被重置为 30 分钟。而一旦浏览器超过 30 分钟没有访问服务器,此次会话的 Session 对象就会失效,下一次浏览器发送的请求将会被服务器认为是第一次请求。
当然我们也可以手动让 Session 对象失效,只需要调用 Session 对象的 invalidate() 方法即可。
Cookie - Session 机制的问题
通过以上描述,我们可以知道 Session 是基于 Cookie 的,而 Cookie 是没有加密保存的,也就是说 Cookie 信息很容易被获取。如果 JSESSIONID 被轻而易举的获取到,那么用户信息将会有很大的风险。
例如,Edge 浏览器中并没有 JSESSIONID 信息,但是我们将 JSESSIONID 信息拼接在请求中发送:
其次,如果用户基础庞大,那么 Session 将会占用服务器大量的资源。
JWT
JWT(JSON Web Token)即 JSON 网络令牌,用于在各方之间安全的将信息作为 JSON 对象传输。
主要用途:
- 授权:用户登录成功之后,服务端就会给用户派发 JWT。用户后续的请求都将携带 JWT,服务端从 JWT 获知该令牌允许的路由、服务和资源。
- 信息交换:各方之间传输信息时,可以使用 JWT 进行签名。
认证流程
- 客户端使用用户名、密码请求登录(这里需要注意,由于 HTTP 协议使用明文发送请求,所以如果该请求被接截获,那么用户信息将是不安全的。因此这里最好使用 HTTPS 协议);
- 服务端收到请求,验证用户名、密码;
- 验证成功后,服务端签发一个 Token,并将该 Token 发送给客户端;
- 客户端收到 Token 以后将 Token 保存起来(这里可以考虑两个地方:Cookie 和 WebStorage,WebStorage 又分为 localStorage 和 sessionStorage,前者永久有效,后者在浏览器窗口中有效);
- 客户端每次向服务器发送请求时都需要带着服务端签发的 Token;
- 服务端收到请求,验证客户端请求中携带的 Token。若验证成功,则表示发送请求的是用户本人,向客户端返回请求的数据。
JWT 结构
JWT 是一个字符串,由 Header、Payload、Singnature 三个部分组成。
- Header:由两部分组成 — — 令牌类型(默认是 JWT)、所使用的签名算法(HMAC、SHA256、RSA),这两个信息会使用 Base64 进行编码(Base64 是一种编码,可以被翻译回原来的样子,并不是一种加密过程),组成 JWT 结构的第一部分。
- Payload:有效负载,其中包含声明。声明是有关实体(一般是用户)和其他数据的声明。同样的,使用 Base64 编码。
- Singnature:将编码后的 Header 和 Payload,再加上一个我们提供的密钥(最关键的东西),使用 Header 中指定的签名算法进行签名。
服务端解析 JWT 的过程
当服务器收到请求,会从请求中获取请求携带的 JWT。使用 Base64 解码 Header 和 Payload 中的信息,然后使用存放在服务器上的密钥和 Header 中指定的算法,对 Header 和 Payload 进行签名,接着比对签名结果和 JWT 的 Singnature 部分是否一致。
若签名结果和 Singnature 部分一致,即表示此次请求是一个受信请求,正常返回处理结果;反之,返回错误信息。