会话跟踪技术

序言

  HTTP 是无状态协议,它不对之前发生过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求管理,服务器不知道用户上一次做了什么,这严重阻碍了交互式 Web 应用程序的实现。
  在典型的网上购物场景中,用户浏览了几个页面买了件衣服。最后结帐时,由于 HTTP 的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么,
  不可否认,无状态协议有它的优点,由于不必保存状态,自然可以减少服务器的压力。
  但是,用户该如何在网站上购物呢?总不可能辛辛苦苦挑了件衣服给前台小姐姐(第一次请求),出去接个电话的功夫回来后准备结账(第二次请求),可前台小姐姐却说:小哥哥你谁啊?
  为此,引入了会话跟踪技术。

会话跟踪

  会话指的是一次会话中包含多次的 HTTP请求和响应,我们可以简单的将会话理解为浏览器打开和关闭期间里对一个网站执行的多次操作。
  而会话跟踪技术指的是:通过相关技术在一次会话的范围内共享数据
  常用的会话跟踪技术有CookieSession
  Cookie通过在客户端记录信息来记录用户信息,而Session则通过在服务器端记录信息来记录用户信息。

Cookie

什么是 Cookie?

  Cookie(复数形态Cookies),又称为“小甜饼”。类型为“小型文本文件[1],指某些网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)。

  简单来说,Cookie

  • 就是浏览器(客户端)储存在用户电脑上的一小段文本文件
  • 为纯文本格式,不包含任何可执行的代码
  • 由键值对构成,由分号和空格隔开,如:PHPSESSID=web1~7un1oeibjsqlcj386m7tfgoab8;
  • 虽然是存储在客户端,但是通常由服务器端进行设置
  • 单个大小被限制在4kb左右
  • 同一域名下的总Cookie数量存在限制(< 20个)

属性

  Cookie 的属性众多,下图为通过抓包查看的结果:

Cookie 的属性

   它们的简单介绍见下表:

属性 含义
Name 键名
Value 键值
Domain 绑定的域名,默认当前域
Path 路径,默认 /
Expires / Max-Age 过期时间
Size 容量大小,单位 byte
HttpOnly 是否禁止 JS 访问 Cookie
Secure 安全相关
SameSite 用于防止 CSRF 攻击和用户追踪

分类

  Cookie总是保存在客户端中,按其在客户端中的存储位置,可分为:

  • 内存Cookie:由浏览器维护,保存在内存中,浏览器关闭后就消失了,其存在时间是短暂的。
  • 硬盘Cookie:保存在硬盘里,有一个过期时间,除非用户手工清理或到了过期时间才被删除。

应用场景

  Cookie就是用来绕开 HTTP 的无状态性的“额外手段”之一。服务器可以设置或读取Cookies中包含信息,借此维护用户跟服务器会话中的状态。

购物

  在购物场景中,当用户选购了第一项商品会请求服务器,服务器在向用户发送网页的同时还会发送一段Cookie用来记录被选商品的信息。当用户访问另一个页面,浏览器会把此Cookie发送给服务器,于是服务器知道他之前选购了什么。用户若继续选购衣服,服务器会在原Cookie上追加新的商品信息。用户结帐时,服务器读取最后发送来的Cookie就行了。

二次登录免密

  Cookie另一个典型的应用是当登录一个网站时,网站往往会请求用户输入用户名和密码,并且用户可以勾选“下次自动登录”。如果勾选了,那么下次访问同一网站时,用户会发现不用输入用户名和密码就已经登录了。
  这正是因为第一次登录时,服务器发送了包含登录凭据(用户名加密码的某种加密形式)的Cookie到用户的硬盘上。第二次登录时,如果该Cookie尚未到期,客户端会携带该Cookie到服务器,服务器会验证接受的登录凭据的Cookie,于是就实现了免验证登录。

缺点

  Cookie不可避免的有一些缺点,如:

  • Cookie会被附加在每次 HTTP 请求中,所以无形中增加了流量
  • 由于在 HTTP 请求中的Cookie是明文传递的(存在安全性成问题),因此许多网站会使用加密的 HTTPS 协议;
  • Cookie的大小限制在 4 KB 左右,这对于复杂的存储需求来说是不够用的。

  下面通过一个简单的 Demo 来认识Cookie

快速入门

①创建项目并添加依赖

  首先创建一个项目,在pom.xml文件加入Servlet依赖:

1
2
3
4
5
6
<!--servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>

  创建一个使用了注解的SendCookieServlet,当客户端通过URLgetCookieFromServer访问这个Servlet后,服务器会发送一个设置好的Cookie给客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/getCookieFromServer")
public class SendCookieServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//创建Cookie
Cookie cookie = new Cookie("msg", "HelloWorld");
//设置Cookie过期时间
cookie.setMaxAge(60);
//响应发送Cookie
resp.addCookie(cookie);
}
}

  当我们启动服务器,客户端访问这个Servlet后,可以Http响应中查看到Cookie

1
Set-Cookie: msg=HelloWorld; Max-Age=60; Expires=Tue, 30-Jul-2019 05:13:40 GMT

③服务端设置Servlet获取客户发送的Cookie并测试

  再创建一个Servlet,它可以获取到客户端发送的Cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebServlet("/sendCookieToServer")
public class GetCookieServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie[] cookies = req.getCookies();
for (Cookie cookie : cookies) {
if ("msg".equals(cookie.getName())) {
String value = cookie.getValue();
System.out.println(value);
}
}
}
}

  客户端通过URLsendCookieToServer发送Cookie,之后服务器的控制台打印:

1
HelloWorld

总结

  当第一次访问一个网站时,Cookie由服务器设置,之后发送给客户端保存。对客户端之后发送的请求,都会将保存的Cookie放到Http请求中一并发送给服务器。
  这有点像人的出生一样,出生即被国家机构设置并赋予一份身份证,身份证会交给你保管,你每次坐飞机、高铁都要将身份证的信息发送给国家机构认证,认证成功才能开启旅程!

  在 Java 的 Web 程序中,Cookie类在javax.servlet.http包中,该类实现了序列化的Serializable接口,下面为其常用的API:

  • public Cookie(String name, String value):构造方法,name是键,value是值;
  • public String getName():获取当前Cookie对应的键;
  • public String getValue():获取当前Cookie对应的值;
  • public void setMaxAge(int expiry):设置Cookie的过期时间,单位秒,具体介绍:
    • 正数:将Cookie数据写进硬盘的文件中持久化存储指定时间;
    • 负数(-1):默认值,当浏览器关闭后,Cookie数据被销毁
    • 0: 删除Cookie信息
  • public void setPath(String uri):设置Cookie的获取范围,默认设置当前项目的目录。若要所有项目共享,将uri设置为/即可。
  • public void setDomain(String domain):设置Cookie的域名。如果设置的一级域名相同,那么多个服务器之间的Cookie可以共享

  除此之外,Web 程序中,HttpServletRequest可以通过其getCookies方法获取所有的Cookie,而HttpServletResponse可以通过addCookie(Cookie cookie)方法将指定的Cookie设置进Http响应消息中。

注意哦:不是所有 request中都会携带 Cookie
  若你添加 Cookie 时没有设置路径,那么 Cookie 默认访问这个 Contreoller的路径。
  比如你在 URL /user/login 中添加了 Cookie却没有设置路径,那么在后续访问中, 只有在访问 /user/*的时候 request 才会携带 Cookie

:服务器一次可以发送多个Cookie嘛?
:可以,服务器可以创建多个Cookie对象,再多次调用addCookie方法添加即可。
Cookie在浏览器(客户端)中保存多久?
:默认情况下,当浏览器关闭后Cookie数据即被销毁,但可以通过setMaxAge方法设置过期时间。
Cookie可以存储中文嘛?
:Tomcat 8 之前不能直接存储中文数据,需要将中文数据转码(使用URL编码),Tomcat 8 之后,支持中文数据,但还是不支持特殊字符,建议使用URL编码存储,URL解码解析。
Cookie可以共享码?若在一个Tomcat服务器中,部署了多个Web项目,它们中Cookie可以共享嘛?
:默认情况下不能共享,但可以通过setPath方法设置Cookie的获取范围。
:不同的Tomcat服务器的中的Cookie可以共享嘛?
:可以,setDomain方法设置即可,如setDomain(".google.com")则其子域名下的网站共享Cookie的帐号信息。

需求

  ①访问一个Servlet,如果是第一次访问,提示:您好,欢迎您首次访问
  ②如果不是第一次访问,则提示:欢迎回来,您上次访问的时间为:时间字符串

分析

  ①使用Cookie完成
  ②具体流程:在服务器的Servlet中判断是否有一个叫lastTimeCookie
  ③有则说明不是第一次访问:更新Cookie信息并响应消息
  ④无则说明是第一次访问,那么创建一个Cookie并响应消息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.设置编码
resp.setContentType("text/html;charset=utf-8");

//2.获取所有Cookie
Cookie[] cookies = req.getCookies();

//3.Cookie是否存在
boolean flag = false;

if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
//3.1获取指定cookie
if ("lastTime".equals(cookie.getName())) {
flag = true;
//3.2获取指定cookie对应的解码后的值
String value = URLDecoder.decode(cookie.getValue(), "utf-8");
//3.3设置新值
cookie.setValue(generateDateByURLEncoder());
//3.4将修改了新值的Cookie更新给客户端
resp.addCookie(cookie);
resp.getWriter().write("欢迎回来,您上次访问的时间为:" + value);
break;
}
}
}

//4.若Cookies不存在则创建一个新Cookie
if (cookies == null || cookies.length == 0 || !flag) {
Cookie cookie = new Cookie("lastTime", generateDateByURLEncoder());
cookie.setMaxAge(24 * 60 * 60 * 7);
resp.addCookie(cookie);
resp.getWriter().write("您好,欢迎您首次访问");
}
}

/**
* 生成URL编码的日期字符串
*
* @return
*/
public String generateDateByURLEncoder() throws UnsupportedEncodingException {
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String format = simpleDateFormat.format(date);
String encode = URLEncoder.encode(format, "utf-8");
return encode;
}
}

注意哦:更新Cookie时仅设置新值并不会更新客户端的Cookie,必须重新在HttpServletResponse中使用addCookie添加该Cookie客户端才被更新,而且设置Cookie的值时最好编码,取出时也得解码,原因是特殊字符无法直接存储在Cookie中。

测试

  访问浏览器的测试结果:

1
2
3
4
//第一次访问
您好,欢迎您首次访问
//第二次访问
欢迎回来,您上次访问的时间为:2018-07-30 17:28:05

Session

什么是 Session?

  Session 是另一种记录客户状态的会话跟踪技术,Cookie 是保存在浏览器(客户端)中,而 Session 则保存在服务器上。当客户端访问服务器时,服务器会把客户端信息以某种方式保存在服务器上。之后客户端再次访问时只需要从该 Session 中查找客户的状态就行了。

  简单来说,Session

  • 存储在服务器的一段数据
  • 其实现依赖于Cookie
  • 也由键值对构成
  • 在用户第一次访问服务器(JSP、Servlet 等程序)的时候创建
  • 用户每访问一次服务器就被更新一次
  • 存储类型任意,没有大小限制

应用场景

  购物,验证码

缺点

  Session不可避免的有一些缺点:

  • SessionCookie为同类型。若用户将浏览器设置为不兼容任何Cookie,则Session无法使用
  • 若存储了较大的对象在Session中而超时时间又设置的过长,会比较消耗服务器资源
  • 可随意调用,不好维护

Java Web 中的 Session

  下面通过一个简单的 Demo 来认识Session

快速入门

①创建项目并添加依赖(和前面相同)

②服务器创建 Servlet 获取 Session 并测试

  在服务器创建一个Servlet,它可以获取Session,并向其中添加一个键值对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebServlet("/test1")
public class SessionDemo1 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req,resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取当前Session,没有则创建
HttpSession session = req.getSession();
session.setAttribute("msg","Hello World");
}
}

  当启动服务器后,通过浏览器(客户端)第一次访问这个Servlet会发生什么呢?
  因为客户端第一次访问服务器时是没有生成Session给它的,这时服务器会在内存中创建一个新的Session进行存储,之后发送给客户端,发送的是什么呢?
  发送的其实是Cookie,这个可以在响应头中看到:

1
Set-Cookie: JSESSIONID=1D902364DB3411C357BEE569F1DD74E9;

  这说明Session是基于Cookie实现的。客户端收到此Session后会将其存储在客户端内存中(但仅保留一次会话时间),当下一次去访问服务器时,比如下面的Servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebServlet("/test2")
public class SessionDemo2 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取当前Session,没有则创建
HttpSession session = req.getSession();
String msg = (String) session.getAttribute("msg");
System.out.println(msg);
}
}

  访问该Servlet时客户端会携带该Cookie到请求头中,如下:

1
Cookie: JSESSIONID=1D902364DB3411C357BEE569F1DD74E9

  之后将此信息与服务器中的做比对,具体怎么比对呢?
  客户端会将1D902364DB3411C357BEE569F1DD74E9与服务器中的存储的Session相比对,若有这个 ID ,则说明在一次会话中获取的Session是相同的,会话建立。

  因为Session的 ID 相同,此时服务器的控制台会打印Hello World

  关闭浏览器再重新打开则是一次新的会话,所以再次访问SessionDemo2这个Servlet时又会生成不同的Session,进行相关操作后的变化结果:

1
Set-Cookie: JSESSIONID=1E068DBCCAA67547C8702A578DB37FB9;

  现在控制台会打印null,因为此时不是上一次的Session了,这个Session中并没有存储msg,也就无法获取了。

简化的 Session 会话流程

  将上面的Session执行过程简化:

  • 当浏览器第一次请求网站时, 服务端会生成 Session ID
  • 生成的 Session ID 会保存到服务端专门的数据结构中
  • 服务器把生成的 Session ID 通过 set-cookie返回给浏览器
  • 浏览器在下一次请求服务器时会携带该 Session ID
  • 服务端收到浏览器发来的 Session ID,会从 Session 存储的数据结构中找到用户状态数据,建立相同会话。
  • 此后的请求都会交换这个 Session ID,进行有状态的会话

Session 相关 API

  Session类位于javax.servlet.http包下,相关API如下:

  • public HttpSession getSession();:通过HttpServletRequest对象获取(或创建);
  • public void setAttribute(String name, Object value);:设置Session的键值;
  • public Object getAttribute(String name);:通过键获取值;
  • public void removeAttribute(String name);:移除键;
  • public void invalidate();:销毁该Session

Session 的问与答

:当客户端关闭后,服务器不关闭,两次获取的Session相同嘛?
:默认情况下不相同。如果需要相同,可以创建Cookie,设置键为JSESSIONID并设置存活时间让Cookie持久化保存发送给客户端。
:客户端不关闭,服务器关闭后,两次获取的Session相同嘛?
:不相同,但为了确保数据不丢失,Tomcat 自动完成如下工作:

  • Session的钝化:在服务器正常关闭之前,将Session对象序列化到硬盘上
  • Session的活化:在服务器启动后,将序列化的Session文件加载到内存中

Session什么时候被销毁?
:分 3 种情况:

  • 服务器关闭时;
  • Session对象调用其invalidate方法时;
  • Session过期时

Session的默认失效时间为多久?可以修改吗?
:Tomcat 中默认为 30 分钟,可以在其web.xml文件中修改全局配置,也可在项目的web.xml中修改仅该项目中Session的失效时间:

1
2
3
4
5
6
7
8
<!--Tomcat的conf目录的web.xml-->
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<!--在项目中的web.xml修改,一般无此属性默认使用Tomcat的-->
<session-config>
<session-timeout>20</session-timeout>
</session-config>

:如果几次请求之间有一个Servlet未调用getSession,或者干脆请求一个静态页面,会不会使得会话中断呢?
:不会,因为客户端只会将合法的Cookie值传送给服务端,至于服务端拿Cookie做什么事它是不会关心的,当然也无法关心。Session建立之后,客户端会一直将Session的ID发送到服务器,无论请求的页面是动态的还是静态的。
:在Servlet/Jsp中,容器是用何种数据结构来存储Session相关的变量的呢?
:我们猜测一下,首先它必须被同步操作,因为在多线程环境下Session是线程间共享的,而 Web 服务器一般情况下都是多线程的(为了提高性能还会用到池技术);其次,这个数据结构必须容易操作,最好是传统的键值对的存取方式。我们先具体到单个Session对象,它除了存储自身的相关信息(如 ID),TomcatSession还提供给程序员一个用以存储其他信息的接口,在类org.apache.catalina.session. StandardSession中:

1
public void setAttribute(String name, Object value, boolean notify)

  在这里可以追踪到它到底使用了何种数据结构:

1
protected Map attributes = new ConcurrentHashMap();

  原来 Tomcat 使用了一个ConcurrentHashMap对象存储数据,这是Java的concurrent包中的类。它刚好满足了我们所猜测的两点需求:同步与易操作性。

  那么 Tomcat 又是用什么数据结构来存储所有的Session对象呢?果然还是ConcurrentHashMap(在管理Sessionorg.apache.catalina.session. ManagerBase类中):

1
protected Map<String, Session> sessions = new ConcurrentHashMap<String, Session>();

Session 案例:

  验证码登录,略

Cookie VS Session

  • Cookie 存储在客户端,Session 存储在服务器
  • Cookie 存在大小限制,Session 则没有
  • Cookie 保存数据没有 Session 安全

总结

参考

0%