JavaWeb 学习笔记 6:会话跟踪

2023-09-22 10:38:39

JavaWeb 学习笔记 6:会话跟踪

HTTP 协议本身是无状态的,所以不能跟踪会话状态。所以会有额外的技术用于跟踪会话:

  • Cookie,客户端技术
  • Session,服务端技术

1.Cookie

1.1.写入 Cookie

可以在服务端通过HttpServletResponse.addCookie向浏览器写入 Cookie:

@WebServlet("/a")
public class ControllerA extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 向浏览器添加 cookie
        Cookie cookie = new Cookie("username", "icexmoon");
        Cookie cookie1 = new Cookie("msg", "hello");
        resp.addCookie(cookie);
        resp.addCookie(cookie1);
    }
}

请求 http://localhost:8080/session-demo/a 能看到响应报文头:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: username=icexmoon
Set-Cookie: msg=hello
Content-Length: 0
Date: Mon, 11 Sep 2023 09:39:41 GMT

使用开发者工具可以看到浏览器端 Cookie 已添加:

image-20230911174548444

应当注意到,服务端添加的 Cookie 默认的存活时间(Expire / Max age)默认是会话,即会话结束(关闭浏览器)后 Cookie 就会被销毁。此时 Cookie 仅保存在内存中,并不会被持久化保存(保存到硬盘)。

使用Cookie.setMaxAge可以设置 Cookie 的生存时间(单位:秒):

// 向浏览器添加 cookie
Cookie cookie = new Cookie("username", "icexmoon");
Cookie cookie1 = new Cookie("msg", "hello");
// 设置有效时间为 1 天
cookie.setMaxAge(1 * 24 * 60 * 60);
resp.addCookie(cookie);
resp.addCookie(cookie1);

响应报文:

Set-Cookie: username=icexmoon; Expires=Tue, 12-Sep-2023 09:54:25 GMT
Set-Cookie: msg=hello

响应报文中的 Cookie 有效期是直接以截至时间的方式返回的:

Expires=Tue, 12-Sep-2023 09:54:25 GMT

这是格林尼治时间(GMT),换算成中国时间(东八区)要+8小时。

用开发者工具查看就能看到有效期已经改变:

image-20230911175940484

有效期可以设置为以下几种:

  • 正数,在X秒后过期
  • 0,立即过期(删除)
  • 负数,会话有效期,在会话结束(浏览器退出)后过期

1.2.读取 Cookie

使用HttpServletRequest.getCookies可以读取浏览器传递的 Cookie:

@WebServlet("/b")
public class ControllerB extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Cookie[] cookies = req.getCookies();
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("username")) {
                String username = cookie.getValue();
                System.out.println("username: " + username);
                break;
            }
        }
    }
}

请求 http://localhost:8080/session-demo/b 就能看到服务端输出的 Cookie 内容。

查看请求报文:

GET /session-demo/b HTTP/1.1
Cookie: JSESSIONID=20C2014C72F0D7ED4D34B821B9A0BC89; username=icexmoon; msg=hello; sentinel_dashboard_cookie=69C1AF3B99482E641CDD23041937F691; JSESSIONID=6EEEF7C596140410E7A21F9DAECF4525
...

当前域名下的所有 Cookie 都以Cookie: xxx=xxx; xxx=xxx 这样的请求头传递。

1.3.中文 Cookie

HTTP 协议规定,报文头内容只能是 ASCII 字符集的字符,所以如果尝试写入中文的 Cookie 信息(UTF-8 字符集)就会报错:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cookie cookie = new Cookie("username", "魔芋红茶");
    response.addCookie(cookie);
}

错误信息:

java.lang.IllegalArgumentException: Control character in cookie value or attribute.

所以要将 UTF-8 字符串转换为全部由 ASCII 字符组成的字符串才能作为 Cookie 内容传递。有多种编码可以实现这一点,最常用的有 URL 编码和 Base64 编码。

这里用 URL 编码举例说明:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String username = "魔芋红茶";
    username = URLEncoder.encode(username, StandardCharsets.UTF_8.name());
    Cookie cookie = new Cookie("username", username);
    response.addCookie(cookie);
}

响应报文中的信息:

Set-Cookie: username=%E9%AD%94%E8%8A%8B%E7%BA%A2%E8%8C%B6

自然的,在服务端接收到的 Cookie 也是 URL 编码过的,所以需要解码:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String username = ServletUtil.getCookie(request, "username");
    if (username!=null){
        username = URLDecoder.decode(username, StandardCharsets.UTF_8.name());
    }
    System.out.println(username);
}

2.Session

Session 同样可以用于跟踪会话,并保存会话的状态信息,与 Cookie 不同的是,Session 是服务端技术,保存在服务端。

2.1.写入 Session

@WebServlet("/e")
public class ControllerE extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        session.setAttribute("msg", "Hello World!");
    }
}

2.2.读取 Session

@WebServlet("/f")
public class ControllerF extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        String msg = (String) session.getAttribute("msg");
        System.out.println("Msg in session: " + msg);
    }
}

2.3.实现原理

Session 是基于 Cookie 实现的,浏览器端持有的是作为 Cookie 存储的 SessionID,服务端为每个 SessionID 保存对应的 Session 对象,并且可以用浏览器端用 Cookie 方式传递的 SessionID 获取到对应的 Session 对象。

整个过程可以表示为:

session原理.drawio

根据 Session 的实现原理,Session 的有效期也包含两部分:

  • 浏览器端 SessionID 的有效期
  • 服务器端的 Session 对象的有效期

两者任意一个失效 Session 就不可用了。

浏览器端的 SessionID 的有效期是会话,即关闭浏览器后就失效:

image-20230912123211545

服务器端的 Session 对象由 Web 服务器软件的设置决定,对于 Tomcat,默认的设置为 30 分钟后被清理。需要说明的是,每次有当前会话的请求产生,对应的 Session 对象的过期时间就会刷新,即 +30 分钟。也就是说只要一直有请求,Session 就不会过期,但是如果有超过 30 分钟没有请求,那 Session 对象就会过期被删除。

之所以为 Session 对象设置有效期,是因为 Session 需要占用服务端内存资源。因此尽量不要为 Session 设置过长的有效期。

Tomcat 的默认设置在 /conf/web.xml 中:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

可以通过修改 Web 应用的 web.xml 覆盖 Tomcat 的默认设置:

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <session-config>
    <session-timeout>1</session-timeout>
  </session-config>
</web-app>

这样就可以将 Session 对象的过期时间修改为 1 分钟,1 分钟后再请求就会发现对应的 Session 对象已经获取不到了。

也可以用 HttpSession.invalidate方法主动让某个 Session 对象过期。

2.4.Session 的持久化

Session 对象是保存在内存中的,这意味着服务器重启后之前的 Session 对象将不存在。对此,Tomcat 可以在正常退出时将内存中的 Session 序列化后保存在硬盘上,再次启动后从硬盘加载 Session 对象到内存。

非常正常退出,比如关闭线程或者服务器电源关闭等无法持久化保存 Session。

下面用一个简单测试进行验证。

使用命令行mvn tomcat7:run启动 Web 项目。

请求 xxx/e后再请求xxx/f,可以看到 session 已经生成,并且可以读取。

在命令行中按Ctrl+C结束 Tomcat。

注意把 Session 有效期改回 30 秒,并去除相关主动销毁 Session 的代码。

此时会在 Tomcat 下的 localhost/session-demo/org 目录下出现一个序列化文件SESSIONS.ser

image-20230912175034988

重新启动 Tomcat 后,如果需要使用 Session,Tomcat 会将之前的 Session 对象从序列化文件加载,并删除该序列化文件,因此可以访问之前的 Session。

2.5.Session 和 Cookie 的区别

  • 存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端
  • 安全性:Cookie不安全,Session安全
  • 数据大小:Cookie最大3KB,Session无大小限制
  • 存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
  • 服务器性能:Cookie不占服务器资源,Session占用服务器资源

3.案例:登录注册

登录和验证的实现都比较简单,这里只说明一下验证码的实现。

这里使用一个工具类 CheckCodeUtil 实现验证码的生成:

public class CheckCodeUtil {
    /**
     * 输出随机验证码图片流,并返回验证码值(一般传入输出流,响应response页面端,Web项目用的较多)
     *
     * @param w 宽
     * @param h 高
     * @param os 输出流
     * @param verifySize 验证码位数
     * @return 生成的验证码(字符串)
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, os, verifyCode);
        return verifyCode;
    }
    // ...
}

利用这个工具类生成验证码,并将生成的验证码图片写入响应报文的输出流:

@WebServlet("/user/check_code")
public class CheckCodeController extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        OutputStream os = response.getOutputStream();
        String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
        request.getSession().setAttribute("checkCode", checkCode);
    }
    // ...
}

用于验证的字符串形式的验证码要保存到 Session,以便在收到注册请求时进行验证。

注册页面用于显示验证码的图片设置src

<tr>
    <td>验证码</td>
    <td class="inputs">
        <input name="checkCode" type="text" id="checkCode">
        <img id="checkCodeImg" src="/login-demo/user/check_code">
        <a href="#" id="changeImg" onclick="refreshCheckCode()">看不清?</a>
    </td>
</tr>

现在页面加载时就能显示验证码。为了能点击 看不清 链接时能刷新,需要实现一个替换图片 src 的 js 方法:

// 刷新验证码
function refreshCheckCode(){
    $("img#checkCodeImg").attr("src","/login-demo/user/check_code");
}

要注意的是,此时只有在开发者工具选择禁用缓存的情况下才能正常刷新验证码,缓存生效时是不会有效果的,因为验证码图片会被缓存起来,浏览器会直接使用缓存,不会再次请求。

这就需要让生成验证码图片的 Servlet 返回的响应报文中禁用缓存:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 浏览器不能缓存验证码 Cache-Control: no-cache
    response.setHeader("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate");
    // ...
}

现在就没有类似的问题了。

在客户端发起注册请求时检查验证码:

// 检查验证码是否正确
String checkCode = (String) request.getSession().getAttribute("checkCode");
String inputCheckCode = request.getParameter("checkCode");
if (checkCode == null || inputCheckCode == null){
    throw new RuntimeException("请先输入验证码");
}
if (!checkCode.equalsIgnoreCase(inputCheckCode)){
    System.out.println(checkCode);
    System.out.println(inputCheckCode);
    throw new RuntimeException("验证码不正确");
}

The End,谢谢阅读。

本文的完整示例可以从这里获取。

4.参考资料

更多推荐

invoke与begininvoke区别

`Invoke`和`BeginInvoke`是用于在多线程应用程序中执行委托的两种不同方法,它们之间的主要区别在于同步和异步执行:1.`Invoke`:-`Invoke`是一个同步方法,它会在当前线程中执行委托。-调用`Invoke`方法会阻塞当前线程,直到委托的执行完成,然后才继续执行后续代码。-这意味着如果在主线程

计算机视觉与深度学习-经典网络解析-VGG-[北邮鲁鹏]

目录标题VGG参考VGG网络贡献使用尺寸更小的$3\times3$卷积串联来获得更大的感受野放弃使用$11\times11$和$5\times5$这样的大尺寸卷积核深度更深、非线性更强,网络的参数也更少;去掉了AlexNet中的局部响应归一化层(LRN)层。网络结构主要改进输入去均值小卷积核串联代替大卷积核无重叠池化卷

TikTok如何打造爆款视频?超店有数让你的视频上热门!

作为TikTok视频博主,你肯定面临着以下难题:播放量卡1000,粉丝数原地踏步。视频创意枯竭,不知道拍什么?不知道拍什么会火?流行趋势慢人一步,热点捉摸不透?一直在模仿,从未有超越。拍摄费时费力,视频制作效率低下...然而!别人家却是这样:粉丝量低的博主也能随随便便播放量破10W+,一条视频带爆粉丝数的翻几番。点赞、

C语言中的sizeof运算符的作用是什么?

在C语言中,sizeof运算符是一个非常重要的运算符,它用于计算数据类型或表达式的大小(以字节为单位)。这个运算符在C语言中的作用非常广泛,它可以帮助程序员确定内存的分配和数据类型的大小,从而更好地管理内存和优化程序性能。在本文中,我们将详细探讨sizeof运算符的作用、用法以及一些示例,以帮助C语言初学者更好地理解它

【计组】计算机系统体系结构

【计组】计算机系统体系结构文章目录【计组】计算机系统体系结构1、体系的发展与思维变化1.1计算机发展1.2冯诺依曼体系2、计算机系统2.1CPU2.2存储层次2.2.1寄存器2.2.2高速缓存(Cache)2.2.3动态随机访问存储器(DRAM)2.2.4硬盘2.3总线2.3.1总线层次2.3.2总线属性1、体系的发展

想要精通算法和SQL的成长之路 - 环形子数组的最大和

想要精通算法和SQL的成长之路-环形子数组的最大和前言一.环形子数组的最大和1.1空间优化前言想要精通算法和SQL的成长之路-系列导航一.环形子数组的最大和原题链接在写这道题目之前,可以先看下这个题:最大子数组和。本题是它的进阶版本,在原本的基础上,有一个环状的数组。那么我们如果将其平铺开来,就是一个两段数组拼接而成。

从源码全面解析 Java SPI 的来龙去脉

👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦🍂博主正在努力完成20

Spring高手之路12——BeanDefinitionRegistry与BeanDefinition合并解析

文章目录1.什么是BeanDefinitionRegistry?2.为什么需要BeanDefinitionRegistry?3.BeanDefinitionRegistry的使用3.1BeanDefinitionRegistry简单例子3.2有关ImportBeanDefinitionRegistrar的实现类的例子4

sed的不同执行方式

1.命令行执行多条sed命令1.1命令行通过多条-e选项sed-e'command1'-e'command2'-e'command3'匹配root或nobody,或mail:sed-n-e'/^root/p'-e'/^nobody/p'-e'/^mail/p'/etc/passwd1.2用\换行Shell的换行符依然有

音频领域的50个关键词

音频领域的50个关键词前言50个关键词label:音频领域,关键词,领域黑话持续更新中,评论点赞收藏能加快更新的速度……前言本文小结音频领域中高频出现的关键词,便于初入此道的同学有个初略概念。有了这个黑话词典或者研究地图,也能帮助新同学更好地和音频相关领域人员进行交流沟通。单个关键词深入的细节都可以在互联网上搜索到,感

献给阿尔吉侬的花束( 入门级bfs查找 + 模版解读 + 错误示范)

献给阿尔吉侬的花束问题文章目录献给阿尔吉侬的花束问题前言题目描述题目分析方法判定bfs算法模版介绍两个数组【记录地图,记录移动距离】一个队列【依次遍历所有接触到的点】一次遍历模版代码如下;题解代码错误示范总结前言许多小伙伴刚刚接触到bfs算法时可能会觉得步骤比较繁琐,所以这里找了一道入门级的bfs算法题为大家介绍模版,

热文推荐