【Java Web编程 九】深入理解会话追踪技术Session和Cookie(上)

简介: 【Java Web编程 九】深入理解会话追踪技术Session和Cookie(上)

什么是会话,会话就是从打开浏览器进行一系列操作再到关闭浏览器的全过程。什么是会话追踪技术,就是在一次会话中记录用户的状态,我们知道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

它的工作机制如下:

  1. 首先用户登录,server 会为用户生成一个 session,为其分配唯一的 sessionId,这个 sessionId 是与某个用户绑定的。也就是说根据此 sessionid(假设为 abc) 可以查询到它到底是哪个用户,然后将此 sessionid 通过 cookie 传给浏览器。
  2. 之后浏览器的每次添加购物车请求中只要在 cookie 里带上 sessionId=abc 这一个键值对即可,server 根据 sessionId 找到它对应的用户后,把传过来的商品 id 保存到 server 中对应用户的购物车即可。
  3. 用户登出注销该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不一致,添加的购物信息也是该浏览器客户端独有的:

相关文章
|
2天前
|
JSON 前端开发 JavaScript
java-ajax技术详解!!!
本文介绍了Ajax技术及其工作原理,包括其核心XMLHttpRequest对象的属性和方法。Ajax通过异步通信技术,实现在不重新加载整个页面的情况下更新部分网页内容。文章还详细描述了使用原生JavaScript实现Ajax的基本步骤,以及利用jQuery简化Ajax操作的方法。最后,介绍了JSON作为轻量级数据交换格式在Ajax应用中的使用,包括Java中JSON与对象的相互转换。
8 1
|
4天前
|
SQL 负载均衡 安全
安全至上:Web应用防火墙技术深度剖析与实战
【10月更文挑战第29天】在数字化时代,Web应用防火墙(WAF)成为保护Web应用免受攻击的关键技术。本文深入解析WAF的工作原理和核心组件,如Envoy和Coraza,并提供实战指南,涵盖动态加载规则、集成威胁情报、高可用性配置等内容,帮助开发者和安全专家构建更安全的Web环境。
15 1
|
7天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
20 3
|
7天前
|
SQL 监控 Java
Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面
本文探讨了Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,以实现高效稳定的数据库访问。示例代码展示了如何使用HikariCP连接池。
7 2
|
7天前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
11 1
|
7天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
22 1
|
8天前
|
负载均衡 监控 算法
论负载均衡技术在Web系统中的应用
【11月更文挑战第4天】在当今高并发的互联网环境中,负载均衡技术已经成为提升Web系统性能不可或缺的一环。通过有效地将请求分发到多个服务器上,负载均衡不仅能够提高系统的响应速度和处理能力,还能增强系统的可扩展性和稳定性。本文将结合我参与的一个实际软件项目,从项目概述、负载均衡算法原理以及实际应用三个方面,深入探讨负载均衡技术在Web系统中的应用。
36 2
|
8天前
|
监控 前端开发 JavaScript
前端技术探索:构建高效、可维护的Web应用
【10月更文挑战第23天】前端技术探索:构建高效、可维护的Web应用
26 0
|
2天前
|
存储 安全 搜索推荐
理解Session和Cookie:Java Web开发中的用户状态管理
理解Session和Cookie:Java Web开发中的用户状态管理
10 4
|
5天前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密