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

相关文章
|
8天前
|
安全 Java 数据库连接
2025 年最新 Java 学习路线图含实操指南助你高效入门 Java 编程掌握核心技能
2025年最新Java学习路线图,涵盖基础环境搭建、核心特性(如密封类、虚拟线程)、模块化开发、响应式编程、主流框架(Spring Boot 3、Spring Security 6)、数据库操作(JPA + Hibernate 6)及微服务实战,助你掌握企业级开发技能。
95 3
|
30天前
|
JavaScript Java 微服务
现代化 Java Web 在线商城项目技术方案与实战开发流程及核心功能实现详解
本项目基于Spring Boot 3与Vue 3构建现代化在线商城系统,采用微服务架构,整合Spring Cloud、Redis、MySQL等技术,涵盖用户认证、商品管理、购物车功能,并支持Docker容器化部署与Kubernetes编排。提供完整CI/CD流程,助力高效开发与扩展。
223 63
|
19天前
|
Java
Java编程:理解while循环的使用
总结而言, 使用 while 迴圈可以有效解决需要多次重复操作直至特定條件被触发才停止執行任务场景下问题; 它简单、灵活、易于实现各种逻辑控制需求但同时也要注意防止因邏各错误导致無限迁璇発生及及時處理可能発生异常以确保程序稳定运作。
113 0
|
28天前
|
安全 Cloud Native Java
Java:历久弥新的企业级编程基石
Java:历久弥新的企业级编程基石
|
28天前
|
移动开发 Cloud Native Java
Java:历久弥新的企业级编程基石
Java:历久弥新的企业级编程基石
|
2月前
|
缓存 NoSQL Java
Java Web 从入门到精通之苍穹外卖项目实战技巧
本项目为JavaWeb综合实战案例——苍穹外卖系统,涵盖Spring Boot 3、Spring Cloud Alibaba、Vue 3等主流技术栈,涉及用户认证、订单处理、Redis缓存、分布式事务、系统监控及Docker部署等核心功能,助你掌握企业级项目开发全流程。
216 0
|
2月前
|
设计模式 Java 数据库连接
Java编程的知识体系 | Java编程精要
Java是一种广泛使用的通用编程语言,具备面向对象、跨平台、安全简单等优势,适用于桌面、企业、Web、移动及大数据等多个领域。它功能强大且易于学习,是程序设计入门和面向对象思想学习的优选语言。本书系统讲解Java编程知识,涵盖技术核心与应用拓展两大模块,内容包括基础语法、面向对象设计、GUI、数据库、多线程、网络编程及Web开发等,帮助读者全面掌握Java开发技能。
59 0
|
2月前
|
安全 JavaScript Java
java Web 项目完整案例实操指南包含从搭建到部署的详细步骤及热门长尾关键词解析的实操指南
本项目为一个完整的JavaWeb应用案例,采用Spring Boot 3、Vue 3、MySQL、Redis等最新技术栈,涵盖前后端分离架构设计、RESTful API开发、JWT安全认证、Docker容器化部署等内容,适合掌握企业级Web项目全流程开发与部署。
104 0
|
11月前
|
存储 安全 数据安全/隐私保护
Cookie 和 Session 的区别及使用 Session 进行身份验证的方法
【10月更文挑战第12天】总之,Cookie 和 Session 各有特点,在不同的场景中发挥着不同的作用。使用 Session 进行身份验证是常见的做法,通过合理的设计和管理,可以确保用户身份的安全和可靠验证。
370 57
|
9月前
|
存储 前端开发 Java
【SpringMVC】——Cookie和Session机制
获取URL中参数@PathVarible,上传文件@RequestPart,HttpServerlet(getCookies()方法,getAttribute方法,setAttribute方法,)HttpSession(getAttribute方法),@SessionAttribute
203 11

热门文章

最新文章