【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不一致,添加的购物信息也是该浏览器客户端独有的:

相关文章
|
6月前
|
监控 Cloud Native Java
Quarkus 云原生Java框架技术详解与实践指南
本文档全面介绍 Quarkus 框架的核心概念、架构特性和实践应用。作为新一代的云原生 Java 框架,Quarkus 旨在为 OpenJDK HotSpot 和 GraalVM 量身定制,显著提升 Java 在容器化环境中的运行效率。本文将深入探讨其响应式编程模型、原生编译能力、扩展机制以及与微服务架构的深度集成,帮助开发者构建高效、轻量的云原生应用。
731 44
|
6月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
646 1
|
7月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
237 4
|
7月前
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
1471 1
|
6月前
|
存储 前端开发 Java
【JAVA】Java 项目实战之 Java Web 在线商城项目开发实战指南
本文介绍基于Java Web的在线商城技术方案与实现,涵盖三层架构设计、MySQL数据库建模及核心功能开发。通过Spring MVC + MyBatis + Thymeleaf实现商品展示、购物车等模块,提供完整代码示例,助力掌握Java Web项目实战技能。(238字)
759 0
|
7月前
|
Java 测试技术 API
2025 年 Java 开发者必知的最新技术实操指南全览
本指南涵盖Java 21+核心实操,详解虚拟线程、Spring Boot 3.3+GraalVM、Jakarta EE 10+MicroProfile 6微服务开发,并提供现代Java开发最佳实践,助力开发者高效构建高性能应用。
1058 4
|
7月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
367 4
|
7月前
|
前端开发 Java 数据库
Java 项目实战从入门到精通 :Java Web 在线商城项目开发指南
本文介绍了一个基于Java Web的在线商城项目,涵盖技术方案与应用实例。项目采用Spring、Spring MVC和MyBatis框架,结合MySQL数据库,实现商品展示、购物车、用户注册登录等核心功能。通过Spring Boot快速搭建项目结构,使用JPA进行数据持久化,并通过Thymeleaf模板展示页面。项目结构清晰,适合Java Web初学者学习与拓展。
521 1
|
6月前
|
安全 Cloud Native Java
Java 模块化系统(JPMS)技术详解与实践指南
本文档全面介绍 Java 平台模块系统(JPMS)的核心概念、架构设计和实践应用。作为 Java 9 引入的最重要特性之一,JPMS 为 Java 应用程序提供了强大的模块化支持,解决了长期存在的 JAR 地狱问题,并改善了应用的安全性和可维护性。本文将深入探讨模块声明、模块路径、访问控制、服务绑定等核心机制,帮助开发者构建更加健壮和可维护的 Java 应用。
535 0
|
7月前
|
存储 人工智能 算法
Java 大视界 -- Java 大数据在智能医疗影像数据压缩与传输优化中的技术应用(227)
本文探讨 Java 大数据在智能医疗影像压缩与传输中的关键技术应用,分析其如何解决医疗影像数据存储、传输与压缩三大难题,并结合实际案例展示技术落地效果。