26. 会话技术-Session的使用

一、 Session

1.1 概述

session是服务器端的会话技术

# session的作用
在一次会话的多次请求之间共享数据,将数据保存到服务器端

# HttpSession是一个域对象
HttpSession是一个接口
域对象可以看成是map(存储多个键值对), cookie是一个entry(只能存一个键值对)
1. 域对象的方法
a. 存储数据
void setAttribute(String name,Object value)
b. 获取数据
Object getAttribute(String name)
c. 删除数据
void removeAttribute(String name)

2. 生命周期: 一次会话的多次请求之间
pageContext(JSP) < request < session < servletContext
从api上来说, 小域对象可以获取大域对象

1.2 工作原理

Session基于Cookie技术实现

下面来使用一个上医院看病的示例,来简单理解 Session


26. 会话技术-Session的使用_python

1591321065800

方法介绍

1. 获取session对象: 
HttpSession session = request.getSession()
1). 通过请求对象创建一个会话对象,如果当前用户会话不存在,创建会话。
2). 如果会话已经存在,这个方法返回已经存在的会话对象。
2. 获取session的id
String sessionId = session.getId();
3. 使当前session失效
ression.invalidate();

记得把项目中的index.jsp干掉(影响到 JSESSIONID的cookie)

编写示例代码

index.html:  编写一个页面,模拟两个请求

  • 第一次请求至服务端:第一次去医院,生成一个session,并且保存信息
  • 第二次请求至服务端:再次去医院,查看之前保存的session信息。查询session id 是否与 上一次保存的 session ID 一致

26. 会话技术-Session的使用_spring_02


image-20210217165529664

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="Session01Servlet">第一次访问, 创建Session</a> <br>
<a href="Session02Servlet">第二次访问, 查看Session信息</a>
</body>
</html>

Session01Servlet:创建Session

26. 会话技术-Session的使用_java_03


image-20210217165651306

package com.lijw;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
* @author Aron.li
* @date 2021/2/17 16:56
*/
@WebServlet("/Session01Servlet")
public class Session01Servlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. (挂号) 获取session对象
HttpSession session = request.getSession();
//2. (看病) 存数据
session.setAttribute("sick","有点虚");

//3. tomcat自动实现,将sessionId通过cookie返回给浏览器
// String id = session.getId();
// Cookie cookie = new Cookie("JSESSIONID", id);
// response.addCookie(cookie);
}

}

Session02Servlet:读取Session信息

26. 会话技术-Session的使用_web_04


image-20210217165806174

package com.lijw;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
* @author Aron.li
* @date 2021/2/17 16:57
*/
@WebServlet("/Session02Servlet")
public class Session02Servlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. (挂号) 获取session对象
HttpSession session = request.getSession();
Object sick = session.getAttribute("sick");
System.out.println("value:" + sick + ",id:" + session.getId());
}
}

测试

  • 在 index.html 执行第一次请求,创建 session

26. 会话技术-Session的使用_web_05


image-20210217165909891

26. 会话技术-Session的使用_python_06


image-20210217170010048

  • 在 index.html 执行第二次信息,查看 session 的 ID

26. 会话技术-Session的使用_java_07


image-20210217170111939

26. 会话技术-Session的使用_session_08


image-20210217170137124

可以通过获取的 session ID 信息,我们可以知道 session 是同一个的。

1.3 Session细节

# 找不到当前会话中的session原因分析 (通俗版)
问题: 什么时候找不到班长原来的病历本?
1. 班长的原因
0). 只要班长把病例本编号弄丢了 (id弄丢了)
1). 用户清除cookie(清除浏览记录)
2). 用户关闭浏览器, 保存id的cookie默认会话级别,自动销毁了
-> 通过持久化cookie,达到session持久化 (下一个)
2. 医院的原因
0). 医院把病历本弄丢了
1). 手动销毁session
session.invalidate();
2). 自动销毁session
默认30分钟内不访问,自动销毁 (可以改)
a. tomcat/conf/web.xml 默认设置
(对发布在此tomcat上所有项目生效)
b. 在当前项目中修改 web.xml (会覆盖tomcat默认设置)
<session-config>
<session-timeout>30</session-timeout>
</session-config>
3). 服务器非正常关闭
突然断电, 数据来不及保存
正常关闭: session数据会会从内存保存硬盘上
-> session 钝化和活化
# 找不到当前会话中的session原因分析(专业版)
1. 浏览器方面的原因
0). 核心: 保存JSESSIONID的cookie被销毁
1). 因为cookie存活时间默认为会话,所以用户关闭浏览器就会销毁(用户无意识)
-> session持久化
2). 用户清除浏览记录(包含cookie)

2. 服务器方面的原因
0). 核心: session对象是存在服务器的内存,被销毁
1). session手动销毁:session.invalidate();
备注: session对象立即销毁

2). 过期销毁:session默认存活时间--30min
备注: (该用户连续30分钟不访问,服务器会自动销毁session)
文件配置: web.xml (tomcat中的默认设置)
1. tomcat/config/web.xml 中有session-config配置 (全局有效)
2. 我们可以在项目中web.xml覆盖其配置 (只对当前项目有效)
<session-config>
<session-timeout>30</session-timeout>
</session-config>

3). 非正常关闭tomcat(比如突然断电)
备注: 如果正常关闭tomcat,tomcat在停止之前会钝化session,下次启动时活化

1.4 session的持久化

#浏览器关闭后,session的持久化方案 
1. 问题: 从以上的分析我们得知, 浏览器关闭之后,就找不到原来的session了
2. 原因:
1. 浏览器关闭,服务器中的session是在的
2. 但是前端的JESSIONID这个cookie消失了
3. 浏览器提交请求没有这个id,服务器自然就找不到之前的session了

3. 解决: 浏览器关闭,session依然找到的
1. 在Servlet中手动创建name=JESSIONID的cookie;
2. 这个cookie存储session的id,设置持久化级别 setMaxAge()
3. 将JESSIONID的cookie响应给浏览器;
@WebServlet("/Session01Servlet")
public class Session01Servlet extends HttpServlet {

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

//1. (挂号) 获取session对象
HttpSession session = request.getSession();
//2. (看病) 存数据
session.setAttribute("sick","有点虚");

//3. tomcat自动实现,将sessionId通过cookie返回给浏览器
// String id = session.getId();
// Cookie cookie = new Cookie("JSESSIONID", id);
// response.addCookie(cookie);


//4. 我们手动实现(设置持久级别),覆盖tomcat的自动实现
String id = session.getId();
Cookie cookie = new Cookie("JSESSIONID", id);
cookie.setMaxAge(60*30); //30分钟
response.addCookie(cookie);
}

}

1.5 session的钝化和活化

# 之前提到, 当服务器正常关闭,重启后,还可以再获取session(跟之前的一样)
这是因为tomcat已实现以下二个功能
1. 钝化(序列化: ObjectOutputStream) 保存
当服务器正常关闭时,session中的数据,会序列化到硬盘 (持久化)
序列化的目的: 将内存中对象或数据结构 保存 到硬盘 (编码: 看得懂 -> 看不懂)
内存: 临时性存储设备, 断电了数据就消失
硬盘: 持久性存储设备, 断电了数据依然在

2. 活化(反序列化: ObjectInputStream) 读取
当服务器开启后,从磁盘文件中,将数据反序列化到内存中
反序列化的目的: 将硬盘上的数据读取到内存,形成对象或数据结构 (解码: 看不懂 -> 看得懂)

备注: 钝化和活化的本质是序列化技术, 所以保存的存储数据类型需要实现serializable接口

我们使用的idea工具有坑:

1. 我们正常关闭tomcat,tomcat确实将session钝化到磁盘(下图的位置中的sessions.ser)
2. : 但是在idea重启tomcat时,会默认删除之前保存的sessions.ser文件,造成tomcat没有活化数据
3. 解决: 设置idea重启时,不清除session会话(下图)

支持钝化

下面我们来演示一下 idea 工具的坑 到底是什么 坑!

1.5.1 首先查看 Idea 中操作 tomcat 的克隆空间(临时空间)

26. 会话技术-Session的使用_web_09


image-20210217221007671

根据这个路径,我们可以打开我们电脑的文件夹,如下:

26. 会话技术-Session的使用_web_10


image-20210217221059089

1.5.2 停止 tomcat ,查看钝化的效果,生成 session.ser 文件,将 session 信息保存在硬盘中

26. 会话技术-Session的使用_web_11


image-20210217221217785

停止 tomcat 后,那么将会将 session 保存如下:

26. 会话技术-Session的使用_web_12


image-20210217221540189

这个 SESSIONS.ser 就是 tomcat 钝化后的文件,提供后续启动 tomcat 的时候读取 session 数据,进行活化。

1.5.3 Idea默认启动的时候会删除之前钝化保存的文件,导致 tomcat 再次读取 session 数据 活化失败

26. 会话技术-Session的使用_java_13


image-20210217221824738

1.5.4 我们可以设置idea重启时,不清除session数据

26. 会话技术-Session的使用_spring_14


image-20210217222001702

1.6 URL重写(了解)

# URL重写是为了解决cookie禁用问题
1. 问题: 浏览器是默认启用cookie,但是用户也可以禁用浏览器的Cookie(浏览器自带功能: 不允许浏览器保存cookie), 由于Session基于Cookie技术实现,所以一旦禁用了之后,Session功能就会出现问题

2. 解决: url重写技术
response.encodeURL(path)
会在url,拼接JSESSIONID

3. 备注: 开发中,一般我们是不关注禁用cookie的用户,若用户禁用了cookie,会给很多功能的实现带来很大的麻烦

1.6.1 首先禁止浏览器使用 cookie

26. 会话技术-Session的使用_session_15


image-20210217232943160

26. 会话技术-Session的使用_spring_16


image-20210217233029543

此时点击 第二次访问,查看 Session 信息,由于没有 Session ID,导致没有 Session 信息,如下:

26. 会话技术-Session的使用_java_17


image-20210217233322018

1.6.2 修改第一次访问的请求 Servlet

index.html

26. 会话技术-Session的使用_session_18


image-20210217233653491

UrlOverrideServlet

26. 会话技术-Session的使用_spring_19


image-20210217234101372

package com.lijw;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
* @author Aron.li
* @date 2021/2/17 23:37
*/
@WebServlet("/UrlOverrideServlet")
public class UrlOverrideServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

/*
* 正常session 有个前提:
* 1. 浏览器需要携带 Jsessionid 过来
* 2. 默认情况下: Jsessionid是用cookie保存的
* 3. 用户在浏览器中设置: 禁用cookie -> 浏览器不再保存cookie
*
* 解决:
* jsessionid 作为参数放在url后面
*
* url = http://localhost:8081/Session02Servlet
*
* 重写url
* http://localhost:8081/Session02Servlet;jsessionid = ?
*
* 参数分割符 ? , 还有分号
* url?name=value -> value = request.getParameter(name)
* url;name=value 不能上面的api获取, tomcat可以获取就行了
* */
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 创建session
HttpSession session = request.getSession();
session.setAttribute("sick", "很虚");
String id = session.getId();
System.out.println(id);

//重写URL,携带sessionId参数
String url = "Session02Servlet";
String newUrl = response.encodeURL(url); //重写url,拼接jsessionid
System.out.println(newUrl); // Session02Servlet;jsessionid = ?

//将重写后的URL,作为超链接显示在浏览器上
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("<a href='"+newUrl+"'>第二次去医院</a>" );
}
}

1.6.3 再次测试禁用cookie的情况下,获取session数据

26. 会话技术-Session的使用_web_20


image-20210217234224356

26. 会话技术-Session的使用_web_21


image-20210217234305695

26. 会话技术-Session的使用_python_22


image-20210217234318230

26. 会话技术-Session的使用_java_23


image-20210217234336678

在后台成功读取到了 session 数据。

1.7 Session特点

# session是服务器端的会话技术
作用: 在一次会话的多次请求之间共享数据
从浏览器第一次向服务器发起请求建立会话, 直到其中一方断开为止会话结束
1. session存储数据在服务器
2. session存储任意类型的数据(Object)
3. session存储大小和数量没有限制(在服务器内存)
4. session存储相对安全

cookie和session的对比

26. 会话技术-Session的使用_web_24


1591287669163

cookie和session的选择

1. cookie将数据保存在浏览器端,数据相对不安全.
建议敏感的或大量的数据不要放在cookie中,而且数据大小是有限制的
成本低,对服务器要求不高

2. session将数据保存在服务器端内存,数据相对安全.
数据的大小要比cookie中数据灵活很多
成本较高,对服务器压力较大

二、 3大域对象总结

request < session < ServletContext

2.1 域对象方法

# 域对象方法都一致
1. 设置数据
void setAttribute(String name, Object o)
2. 获取数据
Object getAttribute(String name)
3. 删除数据
void removeAttribute(String name)

# 小域对象可以获取大域对象

# 不同域对象: 生命周期不一样

2.2 生命周期

2.2.1 ServletContext域对象

* 何时创建
服务器正常启动,项目加载时,创建
* 何时销毁
服务器关闭或项目卸载时,销毁
* 作用范围
整个web项目(共享数据)

2.2.2 HttpSession域对象

* 何时创建
用户第一次调用request.getSession()方法时,创建【不太标准..
用户访问携带的jsessionid与服务器里的session不匹配时,就会创建的
* 何时销毁
1. 服务器非正常关闭
2. 未活跃状态30分钟
3. 手动销毁
* 作用范围
一次会话中,多次请求间(共享数据)

# 会话的定义: 双方建立连接,连接期间的多次请求响应,直到一方断开连接为止
(B/S) 从浏览器第一次访问这个服务器,期间多次请求响应,直到浏览器关闭为止 -> 狭义的一次会话
cookie和session默认都是会话级别,都可以设置持久级别

2.2.3 HttpServletRequest域对象

* 何时创建
服务器接收到请求时,创建
* 何时销毁
服务器做出响应后,销毁
* 作用范围
一次请求中,多次请求转发间(共享数据)

2.3 小结

  • 能用小的不用大的:request(一次请求)<session(一次会话)<servletContext(应用全局)

因为生命周期长的域对象销毁时间比较晚,占用服务器内存时间太长


  • 常用的场景:
  • 用户登录状态
  • 验证码
  • 购物车
  • request:一次请求中(请求转发共享)
  • session:存放当前会话的私有数据
  • servletContext:若需要所有的servlet都能访问到,才使用这个域对象.

一般情况下,web阶段很少使用这个域对象,在框架spring的学习中会涉及到



三、 用户登录-验证码案例

3.1 用户登录(验证码)

需求

用户访问带有验证码的登录页面,输入用户名,密码以及验证码实现登录功能。

3.1.1 需求分析

26. 会话技术-Session的使用_spring_25


1591781837951

3.1.2 代码实现

1.编写生成验证码的Servlet

26. 会话技术-Session的使用_java_26


image-20210218232805983

package com.lijw;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/**
* @author Aron.li
* @date 2021/2/18 23:24
*/
@WebServlet("/CheckcodeServlet")
public class CheckcodeServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 创建画布
int width = 120;
int height = 40;
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获得画笔
Graphics g = bufferedImage.getGraphics();
// 填充背景颜色
g.setColor(Color.white);
g.fillRect(0, 0, width, height);
// 绘制边框
g.setColor(Color.red);
g.drawRect(0, 0, width - 1, height - 1);
// 生成随机字符
// 准备数据
String data = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
// 准备随机对象
Random r = new Random();
// 声明一个变量 保存验证码
String code = "";
// 书写4个随机字符
for (int i = 0; i < 4; i++) {
// 设置字体
g.setFont(new Font("宋体", Font.BOLD, 28));
// 设置随机颜色
g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));

String str = data.charAt(r.nextInt(data.length())) + "";
g.drawString(str, 10 + i * 28, 30);

// 将新的字符 保存到验证码中
code = code + str;
}
// 绘制干扰线
for (int i = 0; i < 6; i++) {
// 设置随机颜色
g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));

g.drawLine(r.nextInt(width), r.nextInt(height), r.nextInt(width), r.nextInt(height));
}

// 将验证码 打印到控制台
System.out.println(code);

// TODO: 将验证码放到session中
request.getSession().setAttribute("code_session", code);

// 将画布显示在浏览器中
ImageIO.write(bufferedImage, "jpg", response.getOutputStream());
}
}

在网页测试请求,看看验证码的效果如下:

26. 会话技术-Session的使用_web_27


image-20210218232906641

2.编写用户登录的页面 login.html

26. 会话技术-Session的使用_spring_28


image-20210218233852836

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>登录页面</h2>
<hr>
<form action="LoginServlet" method="post">

<input type="text" name="username" placeholder="请输入用户名"> <br>
<input type="password" name="password" placeholder="请输入密码" style="margin-top: 5px;"> <br>

<div style="display: flex; margin: 5px 0;">
<input type="text" name="code" placeholder="请输入验证码" >
<img src="CheckcodeServlet" alt="" id="myimg" style="height: 25px; margin-left: 5px; ">
</div>

<input type="submit">

</form>

<script >
var img = document.getElementById("myimg");
img.onclick = function () {
/*
* 浏览器特点:
* 动态修改了网页中的元素属性,浏览器自动加载
*
* 原理:
* 通过在url中添加一个没有实际作用的参数来欺骗浏览器
* 修改网页属性,浏览器自动加载
* */
let time = new Date().getTime() // 获取当前系统时间毫秒值
img.src = "CheckcodeServlet?time=" + time
}
</script>
</body>
</html>

在浏览器访问页面效果如下:

26. 会话技术-Session的使用_web_29


image-20210218233938755

3.编写处理登录业务的 LoginServlet

26. 会话技术-Session的使用_session_30


image-20210218234843686

package com.lijw;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @author Aron.li
* @date 2021/2/18 23:40
*/
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//解决请求参数的中文乱码
request.setCharacterEncoding("UTF-8");
//解决响应中文乱码
response.setContentType("text/html;charset=utf-8");

//1. 接收验证码 code,判断验证码是否正确
//1.1 从session中获取图片的验证码
String code_session = (String)request.getSession().getAttribute("code_session");
//1.2 获取请求的验证码参数
String code = request.getParameter("code");
//1.3 判断验证码是否正确
if (!code.equalsIgnoreCase(code_session)) {
// 如果验证码不正确,则什么都不处理,直接返回
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("验证码错误,请重新输入");
return;
}

//1. 获取请求参数
String username = request.getParameter("username");
String password = request.getParameter("password");

//2. 业务处理
if("jack".equalsIgnoreCase(username) && "123".equalsIgnoreCase(password)){
//登录成功
response.getWriter().write("登录成功");
}else{
//登录失败
response.getWriter().write("登录失败");
}

}
}

4.测试用户登录

  • 登录成功的情况

26. 会话技术-Session的使用_spring_31


image-20210218234941346

26. 会话技术-Session的使用_java_32


image-20210218234955269

  • 登录失败的情况

26. 会话技术-Session的使用_python_33


image-20210218235031275

26. 会话技术-Session的使用_web_34


image-20210218235047690