什么是会话,会话就是从打开浏览器进行一系列操作再到关闭浏览器的全过程。什么是会话追踪技术,就是在一次会话中记录用户的状态,我们知道HTTP协议是无状态的,服务器端接收客户端的请求,生成HTTP响应回发,服务器端关闭连接,当发生一次请求和响应结束后服务器并不能记录信息。
在Web1.0问题这样并没有什么问题,因为大家只是浏览网页而已,但是到了Web2.0时代,也就是交互互联网时代到来后,无状态的HTTP就行不通了,于是就需要会话追踪技术来追踪请求,比如随着网上购物的兴起,需要记录用户的购物车记录(购物车的数据存储),就需要有一个机制记录每个连接的关系,这样我们就知道加入购物车的商品到底属于谁?
有需求才会产生新技术,首先来了解下会话追踪技术的发展历史,这样我们才知道我们的技术是怎样一步一步被需求驱动到现在的(需要特别说明的是,Session,Cookie以及Token我们都默认是浏览器客户端对单个网站应用请求中的行为,而一个站点在一个浏览器中通常只允许一个用户登录,否则后登录的总会覆盖前面先登录的,所以我们可以认为下边的阐述范围是:一个用户对一个网站的请求行为):
Cookie机制
Cookie 是在 HTTP 协议下,维护客户工作站上信息的一种方式。Cookie 是由 Web 服务器保存在用户浏览器上的小文本数据文件,它可以包含有关用户的信息。cookie是不可跨域的,每个cookie都会绑定一个单一的域名,并只能在指定的域名下使用,cookie的作用方式如下:
Cookie解决购物车问题
以当前的购物车问题为例,每次浏览器请求后 server 都会将本次商品 id 存储在 Cookie 中返回给客户端,客户端会将 Cookie 保存在本地,下一次再将上次保存在本地的 Cookie 传给 server 就行了。这样每个 Cookie 都保存着用户的商品 id,购买记录也就不会丢失了
我们在第一次请求的时候除了默认添加的session,还没有任何其它cookie信息:
接下来我们模拟了一个购物行为,在如下Servlet中设置了cookie:
package com.example.MyFirstJavaWeb; import java.io.*; import javax.servlet.http.*; import javax.servlet.annotation.*; @WebServlet(name = "helloServlet", value = "/hello-servlet") public class HelloServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { Cookie cookie = new Cookie("cart", "cat,dog"); //获取cookie的名字和值,都是string类型 cookie.setMaxAge(10000); //设置最大存活时间(在客户端存活),单位是秒,正值表示存活时长(浏览器关闭也没有影响),负值表示浏览器不关就一直存活 response.addCookie(cookie); //通过响应把cookie返回给客户端 Cookie[] cook = request.getCookies(); //通过请求把客户端的cookie返回到服务器 if (null == cook) { return; } PrintWriter out =response.getWriter(); for (Cookie c : cook) { out.println(c.getName()); out.println(c.getValue()); } } public void destroy() { } }
这样当我们首次请求该购物页面的时候,请求头中还没有cookie:
但是实际的应用程序中我们确看到cookie已经被设置上了,并作为响应cookie返回了:
此时我们再次访问该站点或该网站的其它任意站点都会打印出cookie信息,而且可以在请求头中看到该cookie信息:
Cookie基本概念
HTTP 协议中的 Cookie 包括 Web Cookie 和浏览器 Cookie,它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。Cookie 主要用于下面三个目的
- 会话管理,登陆、购物车、游戏得分或者服务器应该记住的其他内容
- 个性化,用户偏好、主题或者其他设置
- 追踪,记录和分析用户行为
Cookie 曾经用于一般的客户端存储,也是在客户端上存储数据的唯一方法,Cookie 随每个请求一起发送,因此它们可能会降低性能(尤其是对于移动数据连接而言),查看Cookie很简单,以Edge浏览器为例:
Cookie的分类
有两种类型的 Cookies,一种是 Session Cookies,一种是 Persistent Cookies,如果 Cookie 不包含到期日期,则将其视为会话 Cookie。会话 Cookie 存储在内存中,永远不会写入磁盘,当浏览器关闭时,此后 Cookie 将永久丢失。如果 Cookie 包含有效期 ,则将其视为持久性 Cookie。在到期指定的日期,Cookie 将从磁盘中删除。
- 会话 Cookies,会话 Cookie 有个特征,客户端关闭时 Cookie 会删除,因为它没有指定Expires 或 Max-Age指令。但是,Web 浏览器可能会使用会话还原,这会使大多数会话 Cookie 保持永久状态,就像从未关闭过浏览器一样。
- 永久性 Cookies,永久性 Cookie 不会在客户端关闭时过期,而是在特定日期(Expires)或特定时间长度(Max-Age)外过期。例如
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2050 07:28:00 GMT;
Cookie的常用方法
Cookie的源码如下,包含一些Cookie的常用方法,例如设置过期时间,域名,安全策略等:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package javax.servlet.http; import java.io.Serializable; import java.text.MessageFormat; import java.util.Locale; import java.util.ResourceBundle; public class Cookie implements Cloneable, Serializable { private static final long serialVersionUID = -6454587001725327448L; private static final String TSPECIALS; private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings"; private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings"); private String name; private String value; private String comment; private String domain; private int maxAge = -1; private String path; private boolean secure; private int version = 0; private boolean isHttpOnly = false; public Cookie(String name, String value) { if (name != null && name.length() != 0) { if (this.isToken(name) && !name.equalsIgnoreCase("Comment") && !name.equalsIgnoreCase("Discard") && !name.equalsIgnoreCase("Domain") && !name.equalsIgnoreCase("Expires") && !name.equalsIgnoreCase("Max-Age") && !name.equalsIgnoreCase("Path") && !name.equalsIgnoreCase("Secure") && !name.equalsIgnoreCase("Version") && !name.startsWith("$")) { this.name = name; this.value = value; } else { String errMsg = lStrings.getString("err.cookie_name_is_token"); Object[] errArgs = new Object[]{name}; errMsg = MessageFormat.format(errMsg, errArgs); throw new IllegalArgumentException(errMsg); } } else { throw new IllegalArgumentException(lStrings.getString("err.cookie_name_blank")); } } public void setComment(String purpose) { this.comment = purpose; } public String getComment() { return this.comment; } public void setDomain(String domain) { this.domain = domain.toLowerCase(Locale.ENGLISH); } public String getDomain() { return this.domain; } public void setMaxAge(int expiry) { this.maxAge = expiry; } public int getMaxAge() { return this.maxAge; } public void setPath(String uri) { this.path = uri; } public String getPath() { return this.path; } public void setSecure(boolean flag) { this.secure = flag; } public boolean getSecure() { return this.secure; } public String getName() { return this.name; } public void setValue(String newValue) { this.value = newValue; } public String getValue() { return this.value; } public int getVersion() { return this.version; } public void setVersion(int v) { this.version = v; } private boolean isToken(String value) { int len = value.length(); for(int i = 0; i < len; ++i) { char c = value.charAt(i); if (c < ' ' || c >= 127 || TSPECIALS.indexOf(c) != -1) { return false; } } return true; } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException var2) { throw new RuntimeException(var2.getMessage()); } } public void setHttpOnly(boolean isHttpOnly) { this.isHttpOnly = isHttpOnly; } public boolean isHttpOnly() { return this.isHttpOnly; } static { if (Boolean.valueOf(System.getProperty("org.glassfish.web.rfc2109_cookie_names_enforced", "true"))) { TSPECIALS = "/()<>@,;:\\\"[]?={} \t"; } else { TSPECIALS = ",; "; } } }
Cookie的缺点
Cookie有如下的问题,导致一般我们不用Cookie的这种解决方案去追踪重要信息:
- 每个 cookie的容量有限,为了保证COOKIE不占用太多的磁盘空间,每个COOKIE大小一般不超过4KB
- 因为cookie由浏览器存储在本地目录,所以不方便记录敏感信息,如密码等
- cookie不支持跨域访问
- cookie不支持手机端方案
所以我们要想追踪用户的会话,还需要进行方案改进。
Session机制
客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个对象便是 Session 对象,存储结构为 ConcurrentHashMap
。Session 弥补了 HTTP 无状态特性,服务器可以利用 Session 存储客户端在同一个会话期间的一些操作记录。
Session解决购物车问题
回到上边的那个例子,随着购物车内的商品越来越多,每次请求的 cookie 也越来越大,这对每个请求来说是一个很大的负担。对于浏览器来说每次请求只是想将其中一个商品加入购物车,但是cookie却将历史记录也保留了,这是个很大的问题。仔细考虑下,由于用户的购物车信息都会保存在 Server 中,所以在 Cookie 里只要保存能识别用户身份的信息,知道是谁发起了加入购物车操作即可。这样每次请求后只要在 Cookie 里带上用户的身份信息,请求体里也只要带上本次加入购物车的商品 id,大大减少了 cookie 的体积大小。
我们把这种能识别哪个请求由哪个用户发起的机制称为 Session(会话机制),生成的能识别用户身份信息的字符串称为 sessionId
它的工作机制如下:
- 首先用户登录,server 会为用户生成一个 session,为其分配唯一的 sessionId,这个 sessionId 是与某个用户绑定的。也就是说根据此 sessionid(假设为 abc) 可以查询到它到底是哪个用户,然后将此 sessionid 通过 cookie 传给浏览器。
- 之后浏览器的每次添加购物车请求中只要在 cookie 里带上 sessionId=abc 这一个键值对即可,server 根据 sessionId 找到它对应的用户后,把传过来的商品 id 保存到 server 中对应用户的购物车即可。
- 用户登出注销该session,当别的用户登录时又给新用户生成新的session和sessionid。
可以看到通过这种方式再也不需要在 cookie 里传所有的购物车的商品 id 了,大大减轻了请求的负担,另外通过上文不难观察出 cookie 是存储在 client 的,而 session 保存在 server,sessionId 需要借助 cookie 的传递才有意义。首先第一次请求该站点时生成Jsessionid:
然后我们在代码里跟踪该客户端的请求:
package com.example.MyFirstJavaWeb; import java.io.*; import javax.servlet.http.*; import javax.servlet.annotation.*; @WebServlet(name = "helloServlet", value = "/hello-servlet") public class HelloServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { HttpSession session =request.getSession(); response.getWriter().println(session.getId()); String currentCartItem=request.getParameter("cartItem"); session.setAttribute("cart", session.getAttribute("cart")+","+currentCartItem); response.getWriter().println(session.getAttribute("cart")); } public void destroy() { } }
请求该站点并向购物车中添加商品时,只需要每次请求时带着当前商品id并且cookie中包含session id即可:
这样我们不必每次请求时带着客户端的信息和全部购物车信息了,只需要带着sessionid即可,当前站点应用程序会通过sessionid自动匹配当前会话对应的session,然后进行数据的存取。这里我们默认一个浏览器客户端对应一个本站点的用户,如果想具体识别某个用户,也可以在登录后把该用户存储到session中即可,登出时销毁session即可。同时我们可以看到从另一个浏览器客户端进入请求时由于sessionid不一致,添加的购物信息也是该浏览器客户端独有的: