Chapter 8. 会话技术

8.1 会话机制

HTTP 是无状态的协议(对于事务处理没有记忆能力),必须在 HTTP 请求中加入一些额外的用于跟踪客户状态的数据。

在 Web 开发中,会话机制,便是用于跟踪客户状态的普遍解决方案(使用 ASP、PHP 或 JSP 开发的 Web 应用都可以运用会话机制)。

会话,指在一段时间内,单个客户与 Web 应用的一连串相关的交互过程。在一个会话中,客户可能会多次请求访问 Web 应用的同一个网页,也有可能请求方法同一个 Web 应用中的多个网页。

8.2.1 Cookie基本运作机制

Cookie,是在客户端访问 Web 服务器时,服务器在客户端硬盘上存放的信息。而服务器可根据 Cookie 来跟踪客户状态,这对于需区别客户的场合十分有用。

当客户端首次请求访问服务器时,服务器现在客户端存放包含该客户的相关信息的Cookie,以后客户端每次请求访问服务器时,都会在 HTTP 请求数据中包含 Cookie,服务器解析 HTTP 请求中的 Cookie,便能获得关于客户的少量相关信息。(在不登录的情况下,完成服务器对客户的身份识别)

Cookie 作用,就类似于健身馆向会员发送的会员卡(存储了该会员的编号、姓名等信息),以后每次客户到健身馆需出示会员卡,健身馆依据会员卡信息判断是否允许客户健身。

服务器对客户端进行读写 Cookie 操作,会给客户端带来安全隐患,服务器可能会向客户端发送包含恶意代码的 Cookie 数据,此外,服务器可能会依据客户端的 Cookie 来窃取用户的保密信息。

Tomcat 作为 Web 服务器,对 Cookie 提供良好支持,Java Servlet API 为 Servlet 访问 Cookie 提供简单易用的接口。

Cookie 用 javax.servlet.http.Cookie 类来表示,每个 Cookie 对象包含一个 Cookie 名字(调用 getName() 方法)及 Cookie 值(调用 getValue() 方法)。

  • Cookie 类的构造方法中,第一个参数为 Cookie 名字,第二个为 Cookie 值:

    1
    Cookie theCookie = new Cookie("username", "Luffy");
  • 调用 HttpServletResponse 的方法,将 Cookie 添加到 HTTP 响应结果:

    1
    response.addCookie(theCookie);
  • 调用 HttpServletResquest 的方法,从 HTTP 请求中获取所有的 Cookie:

    1
    Cookie cookies[] = request.getCookies();

代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/CookieDemo1")
public class CookieDemo1 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 创建Cookie对象
Cookie c = new Cookie("msg", "secret"); //传入的字符串尽量不要含空白符
//2. 发送Cookie
response.addCookie(c);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebServlet("/CookieDemo2")
public class CookieDemo2 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//3. 获取Cookie
Cookie[] cookies = request.getCookies();
if(cookies != null){
for(Cookie ce : cookies){
System.out.println(ce.getName() + ":" + ce.getValue());
}
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}

共享问题

  1. 同一个Tomcat 服务器中,部署了多个 Web 应用,那么 默认情况下, Cookie 不能共享。

    • setPath(String path):设置 Cookie 的获取范围,默认情况下,设置当前的虚拟目录。若希望将该 Cookie 共享,则可将 path 设置为 "/"(Tomcat 服务器根路径)
    1
    2
    3
    4
    5
    6
    //1. 创建Cookie对象
    Cookie c = new Cookie("msg", "secret"); //传入的字符串尽量不要含空白符
    //2. 设置path,让当前服务器下部署的所有项目均能够共享Cookie
    c.setPath("/");
    //3. 发送Cookie
    response.addCookie(c);
  2. 对于 不同的 Tomcat 服务器

    • setDomain(String path):若设置一级域名相同,则可以实现多个特定服务器间的 Cookie 共享,如:(注意,参数 path 必须以 . 开头)
    1
    2
    cookie.setDomain(".baidu.com");
    //此时,tieba.baidu.com和news.baidu.com中可共享该Cookie

细节问题

  • 能够创建多个 Cookie 对象,多次调用 addCookie() 方法发送 Cookie 即可
  • Cookie 若要存储 中文数据、特殊字符,应使用 URL 编码存储。读取 Cookie 时,再解码解析信息

当 Servlet 向客户端写 Cookie 时,还可通过 Cookie 类的 setMaxAge(int expiry) 方法来设置 Cookie 的有效期。

传入参数 expiry为单位,它的值具有以下含义:

  • 正数:指示浏览器在客户端硬盘上保存 Cookie 的时间为 expiry
  • 零:指示浏览器删除当前 Cookie
  • 负数(默认值):指示浏览器不要把 Cookie 保存到客户端硬盘,Cookie 仅仅存在于 当前浏览器进程 中,当浏览器进程关闭,Cookie 也消失。
1
2
3
4
5
6
//1. 创建Cookie对象
Cookie c = new Cookie("msg", "secret"); //传入的字符串尽量不要含空白符
//2. 设置Cookie存活时间
c.setMaxAge(30); //将Cookie持久化存放到硬盘,30s后会自动删除Cookie文件
//3. 发送Cookie
response.addCookie(c);

8.2.4 案例演示:记忆上次访问时间

需求:

访问一个 Servlet ,若是首次访问,则提示:您好,欢迎您首次访问;若非首次访问,则提示:欢迎回来,您上次访问时间为:yyyy年MM月dd日 HH:mm:ss

此处只给出 Servlet 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@WebServlet("/HelloCookie")
public class HelloCookie extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置响应的消息体的数据格式以及编码
response.setContentType("text/html;charset=utf-8");
Cookie[] cookies = request.getCookies();
boolean flag = false;

if(cookies != null && cookies.length > 0){
for(Cookie ce : cookies){
if("lastTime".equals(ce.getName())){
flag = true;
String curDate = URLDecoder.decode(ce.getValue(), "utf-8");
response.getWriter().write("<h1>欢迎回来,您上次访问时间为:" + curDate + "</h1>");
break;
}
}
}
if(!flag) response.getWriter().write("<h1>您好,欢迎您首次访问</h1>");

Date date = new Date();//分配Date对象并自动设置时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss"); //日期格式化对象
String strDate = sdf.format(date); //将Date对象格式化为字符串
System.out.println("URL编码前:" + strDate);
strDate = URLEncoder.encode(strDate, "utf-8"); //URL编码
System.out.println("URL编码后:" + strDate);

Cookie lastTimeCk = new Cookie("lastTime", strDate);
lastTimeCk.setMaxAge(60 * 60 * 24 * 30); //设置Cookie存活时间:1 month
response.addCookie(lastTimeCk);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}

8.3 Session

8.3.1 会话的运作机制

Servlet 规范指定了基于 Java 的会话的具体运作机制,在 Servlet API 中定义了代表会话的 javax.servlet.http.HttpSession 接口,Servlet 容器必须实现这一接口。

会话的运作流程如下:

  1. 一个浏览器进程第一次请求访问某个 Web 应用中的任意一个支持会话的网页,Servlet 容器试图寻找 HTTP 请求中表示 Session ID 的 Cookie 。但由于还不存在该 Cookie,故认为一个新的会话开始,于是创建了一个 HttpSession 对象,为它分配唯一的Session ID ,并将 Session ID 作为 Cookie 添加到 HTTP 响应结果中。
  2. 当浏览器接收到 HTTP 响应结果后,会把其中表示 Session ID 的 Cookie 保存到客户端
  3. 浏览器进程继续请求访问 刚刚 Web 应用中的任意一个支持会话的网页,在本次 HTTP 请求中会包含表示 Session ID 的Cookie。Servlet 容器同时也成功寻找了该 Cookie,于是认为本次请求已经处于一个会话了,故不再创建新的 HttpServlet 对象,而是从 Cookie 中获取 Session ID,并根据其找到内存中对应的 HttpServlet 对象
  4. 浏览器进程重复步骤3,直到当前会话被销毁,HttpServlet 对象就会结束生命周期。

简而言之,Session 是基于 Cookie 实现的,Session 存储在 服务器端,而 Session ID 会被存储到客户端的 Cookie 中

8.3.2 HttpSession 相关方法

  • 获取 HttpSession 对象:
1
HttpSession session = request.getSession();

HttpSession 接口的以下方法用于向会话范围内存取共享数据:

  1. 返回会话范围内与参数 name 匹配的共享数据:Object getAttribute(String name)
  2. 向会话范围内存放共享数据:void setAttribute(String name, Object value)
  3. 删除会话范围内的一个共享数据:void removeAttribute(String name)

8.3.4 HttpSession 的生命周期

以下情况,会开始一个新的会话,即 Servlet 容器会创建一个新的 HttpSession 对象:

  • 一个浏览器进程第一次访问 Web 应用中的支持会话的任意一个网页;
  • 当浏览器进程与 Web 应用的一次会话已经被销毁后,浏览器进程再次访问 Web 应用中的支持会话的任意一个网页。

以下情况,会话被销毁,即 Servlet 容器使得 HttpSession 对象结束生命周期,并且存放在会话范围内的共享数据也都被销毁:

  • 浏览器进程被终止;

    当会话开始后,若浏览器进程突然关闭,Servlet 容器端无法立即执照浏览器进程已经被关闭,故 Servlet 容器端的 HttpSession 对象不会立即结束生命周期,该对话进入不活动状态,等到超过了 setMaxInactiveInterval(int interval) 设置的时间,会话就会过期而被 Servlet 容器销毁。

  • 服务器端执行 HttpSession 对象的 invalidate() 方法;

    当 Tomcat 中的 Web 应用被终止时,它的会话不会被销毁,而是被 Tomcat 持久化到永久性存储设备中,当 Web 应用重启时,Tomcat 会重新加载这些会话。

  • 会话过期:当会话开始后,若一段时间内,客户一直没有和 Web 应用交互,即一直没有请求访问 Web 应用中的支持会话的任意一个网页,那么 Servlet 容器会自动销毁这个会话。

    HttpSession 类的 setMaxInactiveInterval(int interval) 方法,用于设置允许会话保持不活动状态的时间(以秒为单位)

CookieSession
安全性存储在客户端存储在服务器端,
更加安全
存值类型只支持字符串数据,
其他类型数据需转换为字符串
存任意数据类型
有效期长时间保持
(如默认登陆)
短时间
存储大小单个 Cookie 不超过 4K比 Cookie 高

8.3.6 案例演示:验证码与页面跳转

需求:

  1. 带有验证码的登陆页面 login.jsp
  2. 用户输入用户名、密码及验证码后:
    • 若用户名和密码输入有误,跳转到登陆页面,并提示:用户名或密码错误
    • 验证码输入有误,跳转登陆页面,提示:验证码错误
    • 若全部输入正确,跳转到主页 success.jsp,提示:用户名,欢迎您

此处只给出了 LoginServlet 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1.设置request编码
request.setCharacterEncoding("utf-8");
//2.获取参数
String username = request.getParameter("username");
String password = request.getParameter("password");
User LoginUser = new User(username, password);
String checkCode = request.getParameter("checkCode");
//3.先获取生成的验证码
HttpSession session = request.getSession();
String checkCode_session = (String) session.getAttribute("checkCode_session");
//删除session中存储的验证码(保证登陆失败重定向后,验证码图片不是原来的)
session.removeAttribute("checkCode_session");
//3.先判断验证码(忽略大小写)是否正确
if(checkCode_session!= null && checkCode_session.equalsIgnoreCase(checkCode)){
User resUser = new UserDao().login(loginUser); //判断用户名和密码是否一致
if(resUser != null){
//登录成功,则将用户信息存到 session,并通过响应重定向到success.jsp
session.setAttribute("user",username);
response.sendRedirect(request.getContextPath()+"/success.jsp");
}else{ //登录失败,将提示信息存到【请求】,并将请求转发到登陆页面
session.setAttribute("login_error", "用户名或密码错误");
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
}else{ //验证码不一致,将提示信息存到【请求】,并将请求转发到登陆页面
session.setAttribute("cc_error", "验证码错误");
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}