为什么进行会话管理

Web应用程序基于HTTP协议

  • HTTP基于请求/响应模式
    • 所有请求都是相互独立的,无连续性的
  • HTTP是无连接的协议
    • 限制每次连接只处理一个请求
  • HTTP是无状态的协议
    • 协议对于事务处理没有记忆能力

对于简单的页面浏览或信息获取,HTTP协议即可胜任,但是对于需要客户端和服务器端多次交互的网络应用,则必须记住客户端状态

会话就是一个客户端连续不断地和服务器端进行请求/响应的一系列交互

何为会话

多次请求间建立关联的方式称为会话管理,或会话跟踪

会话状态,指服务器与浏览器在会话过程中产生的状态信息

会话管理是管理浏览器客户端和服务器端之间会话过程中产生的会话数据。

会话跟踪是Web程序中常用的技术,用来跟踪用户的整个会话,主要有四种会话跟踪方法,设置隐藏表单字段,URL重写,Cookie和Session。常用的会话跟踪技术是Cookie和Session。Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

浏览器客户端和服务器端的会话管理涉及的技术有Cookie技术与Session技术,两者的区别在于: - Cookie技术将会话数据保存在浏览器客户端 - Session技术将会话数据保存在服务器端

会话的实现过程

HTTP没有提供任何记住客户端的途径,服务器如何建立、维护与客户端的会话

image-20250415081247045
image-20250415081247045

Cookie技术

如何理解Cookie技术

Cookie实际上是存放在客户端浏览器的一小块文本。

客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来记录用户状态。

Cookie的工作原理主要的记录用户状态,客户端浏览器第一次请求服务器后,服务器会颁发一个Cookie(name = ErgouTree,该Cookie就相当于那唯一的卡号)响应给客户端浏览器,以后客户端的每次请求都会带上这个Cookie,这样服务器就可以根据这唯一的Cookie识别不同的用户。

特点

  • Cookie技术将会话数据保存在浏览器客户端。
  • Cookie数据只能是非中文的字符串类型的数据
  • 浏览器可以保存多个Cookie,但是浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie
  • 每个Cookie的大小限制为4KB

Cookie核心API

1
2
3
4
5
6
Cookie(String name, String value)   // 用户创建Cookie对象的构造函数
void setPath(String uri) // 设置cookie的有效访问路径,这个可以在浏览器内看,设置->内容设置->Cookie->查看所有Cookie和网站
void setMaxAge(int expiry) // 设置cokie的存活时间
void setValue(String newValue) // 设置cookie的值
Cookie[] request.getCookies() // 接收cookie
void response.addCookie(Cookie cookie) // 发送cookie到浏览器端保存

Cookie的创建

1
2
Cookie cookie = new Cookie("username", "kindleheart");
response.addCookie(cookie); // Cookie从servlet里发送到浏览器端

创建Cookie对象,利用Cookie的构造函数来创建Cookie,Cookie的构造函数有两个参数,name和value,必须要有这两个参数,value固定为String类型的。

Cookie 是存放在浏览器端的,所以还需要把 Cookie 从 servlet 里发送到浏览器端,利用response里的addCookie方法可以做到,原理是通过在响应中设置 set-Cookie 标头,以 Key/Value 键值对的形式发送到浏览器

你可以使用google浏览器查看响应头中的set-Cookie标头,你能看到你刚刚添加的Cookie和它的属性值

img
img

响应首部中黄色的部分就是从Servlet发送到浏览器的Cookie

请求首部中红色划线部分是浏览器发回到服务器的Cookie,这个不是你刚创建的Cookie,因为你创建的Cookie刚发送到浏览器,之后你再请求一次就可以在请求首部看到你刚创建的Cookie。

Cookie的获取

在Servlet中只能使用getCookies方法获取所有的Cookie,没有通过Cookie名来获取Cookie的方法。所以需要你自己来编写代码实现。

1
2
3
4
5
6
7
8
9
<%
// 如何获取Cookie
Cookie[] cookies = request.getCookies(); // 返回值类型为Cookie[]
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
System.out.println(cookies[i].getName() + ": " + cookies[i].getValue());
}
}
%>

Cookie的修改与删除 

Cookie并不提供修改,删除操作。

如果要修改某个Cookie,需要新建一个同名的Cookie,并添加到response中就会覆盖原来的Cookie。

下面的两行代码就可把username的value值 “kindleheart”改为”Hush”。

1
2
3
4
5
6
7
8
9
10
11
12
// 修改Cookie的示例方法
protected void modifyCookie(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

// 假设我们要将用户名从旧值改为"Hush"
Cookie cookie = new Cookie("username", "Hush");
cookie.setPath("/Demo"); // 设置与原始Cookie相同的路径
cookie.setMaxAge(7 * 24 * 60 * 60); // 保持相同的有效期
response.addCookie(cookie);

response.getWriter().println("Cookie已修改");
}

删除Cookie的实现

如果要删除某个Cookie,只需要新建一个同名的Cookie,并将maxAge设置为0,并添加到response中覆盖原来的Cookie。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 删除Cookie的标准方法
protected void deleteCookie(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

// 1. 创建一个同名Cookie
Cookie cookie = new Cookie("username", ""); // 设置值为空

// 2. 设置path与原始Cookie一致(重要!)
cookie.setPath("/Demo");

// 3. 设置生存期为0(立即失效)
cookie.setMaxAge(0);

// 4. 添加到响应中
response.addCookie(cookie);

response.getWriter().println("Cookie已删除");
}

修改和删除Cookie时,新建的Cookie除value,maxAge之外的所有属性,例如name,path,domain等,都要与原Cookie完全一样。否则,浏览器会把这两个Cookie视为不同的Cookie,不允许覆盖,导致修改删除失败。

服务的完整示例

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
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
// 获取表单数据
String username = request.getParameter("username");
String password = request.getParameter("pwd");
String rememberMe = request.getParameter("rememberMe");

// 简单的验证(实际项目中应该连接数据库验证)
if("admin".equals(username) && "123456".equals(password)) {
// 登录成功
// 如果用户选择了"记住我"
if("true".equals(rememberMe)) {
// 创建Cookie保存用户名
Cookie usernameCookie = new Cookie("username", username);
// 设置Cookie有效期为7天
usernameCookie.setMaxAge(7 * 24 * 60 * 60);
response.addCookie(usernameCookie);
}else{
// 用户没有选择记住我,删除可能存在的Cookie
Cookie usernameCookie = new Cookie("username", "");
usernameCookie.setMaxAge(0); // 立即过期
response.addCookie(usernameCookie);
}
// 创建会话
HttpSession session = request.getSession();
session.setAttribute("username", username);

// 重定向到欢迎页面
response.sendRedirect("");
}else{
// 登录失败
response.sendRedirect("index.jsp?error=1");
}
}
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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// 初始化变量
String name = "";
boolean isRemembered = false;

// 获取Cookie
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
// 查找用户名Cookie
if(cookie.getName().equals("username")){
name = cookie.getValue();
isRemembered = true;
}
}
}
%>
<html>
<head>
<title>用户登录</title>
</head>
<body>
<h2>用户登录</h2>
<form action="login" method="post">
账号:<input type="text" name="username" id="1" value="<%= name %>"/><br>
密码:<input type="password" name="pwd"/><br>
记住我:<input type="checkbox" name="rememberMe" value="true" <%= isRemembered ? "checked" : "" %>/><br>
登录:<input type="submit" value="登录"/>
</form>
</body>
</html>

Cookie的属性

除了name与value之外,Cookie还有其它的一些可选属性,比如注释、路径和域限定符、最大生存时间和版本号。

每个属性对应着一个get方法和一个set方法。

img
img

有效期

Cookie的maxAge决定着Cookie的有效期,单位为秒,默认值为-1。

Cookie中通过getMaxAge()方法与setMaxAge()方法来读写maxAge属性。

  • maxAge > 0:表示Cookie会在 maxAge 秒之后自动失效。浏览器会将 maxAge 为正数的Cookie持久化,即写到对应的Cookie文件中。无论客户关闭了浏览器还是电脑,只要还在 maxAge 秒之前,登录网站时该Cookie仍然有效。
  • maxAge < 0,则表示该Cookie仅仅在关闭窗口前有效。maxAge 为负数的Cookie,为临时Cookie,不会被持久化,不会被写到Cookie文件中,而是保存在浏览器内存中,因此关闭浏览器该Cookie就消失了。
  • maxAge = 0,有效时间为0,就表示为删除Cookie。Cookie机制没有提供删除Cookie的方法,因此通过设置Cookie即时失效实现删除Cookie的效果。失效的Cookie会被浏览器从Cookie文件或者内存中删除。

Cookie的域名

Cookie是不可跨域名的,同一个一级域名下的两个二级域名如 www.kindleheart.com 和 images.kindleheart.com 也不能互相使用Cookie,因为二者的域名并不严格相同。如果想所有kindleheart.com名下的二级域名都可以使用该Cookie,可以设置domain参数。

1
2
3
Cookie cookie = new Cookie("username", "kindleheart");
cookie.setDomain(".kindleheart.com");
response.addCookie(cookie);

Cookie的路径

domain属性决定访问Cookie的域名,而path属性决定允许访问Cookie的路径(ContextPath)。

如果只允许Demo工程下的程序使用Cookie,可以这么写:

1
2
3
Cookie cookie = new Cookie("username", "kindleheart");
cookie.setPath("/Demo");
response.addCookie(cookie);

Seesion

核心API

1
2
3
4
5
6
7
8
HttpSession getSession()    ——    得到session对象
HttpSession getSession(boolean create) —— 当create参数为true时,如果获取不到对应session对象就为浏览器创建一个session对象;如果create参数为false时,如果获取不到对应session对象就返回null
void setAttribute(String name, Object value) —— 保存会话数据到session对象
Object getAttribute(String name) —— 从session对象中获取会话数据
void removeAttribute(String name) —— 清除session对象中对应的会话数据
void setMaxInactiveInterval(int interval) —— 设置session的有效时间,默认情况是30分钟
void invalidate() —— 销毁session对象
String getId() —— 得到session编号

理解Session

Cookie可以让服务器跟踪每个客户端的访问,Cookie存放在客户端浏览器,但是每次客户端的访问都必须传回这些Cookie,如果Cookie很多,那么无形地会增加了客户端与服务器端的数据传输量,而Session正是解决这个问题的。

Session存放在服务器端,同一个客户每次和服务器端进行交互时,不需要每次传回所有的Cookie值,而是只要传回一个ID,这个ID是客户端第一次访问时生成的,而且每个客户端都是唯一的,这样每个客户端都有了一个唯一的ID,客户端只需要传回这个ID就行了,这个ID通常是name为JSESSIONID的一个Cookie。

实际上有以下三种方式使得Session正常工作。

  • 基于Cookie,如果没有修改Context容器的Cookies标识,则默认也是支持的。客户端每次请求的时候,Cookie会被返回到服务器,利用请求头中的Cookie标头。  

  • 基于URL Path Parameter(URL重写),默认支持。

    浏览器不支持Cookie或者用户把浏览器的Cookie功能关闭了,浏览器就会把该用户的Session的ID信息(JSESSIONID)重写到用户请求的URL参数中,服务器再从URL参数中解析出Session的ID。、

    首先我们把浏览器的Cookie关闭,再使用HttpServletRequest类提供的encodeURL(String url)实现地址重写,下面是一个计录浏览次数的小例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.setContentType("text/html;charset=utf-8");
    HttpSession session = request.getSession();
    int count = 0;
    if(session.getAttribute("count") != null) {
    int c = (int) session.getAttribute("count");
    count = c + 1;
    }
    session.setAttribute("count", count);
    PrintWriter out = response.getWriter();
    out.println("<html>");
    out.println("<body>");
    out.println("<h1>登入" + count + "次</h1>");
    //URL重写把JSESSIONID发送到服务器
    out.println("<a href='" + response.encodeURL("IndexServlet") + "'>click me</a>");
    out.println("</body>");
    out.println("</html>");
    out.close();
    }

    在浏览器的地址栏URL里文件名后面URL参数前面可以看到 jsessionid = XXX。 

    注意:如果浏览器支持Cookie,那么Tomcat仍然会解析Cookie里的中的Session ID,并会覆盖URL中的Session ID,也就是你在URL就中看不到 jsessionid = XXX了。

  • 基于SSL,默认不支持,只有connector.getAttribute(“SSLEnabled”)为TRUE时才支持。

Session的生命周期

  1. Session的创建
  • Session在用户第一次请求服务器的时候自动创建,只有访问Servlet,JSP等动态资源才会创建,访问HTML,IMAGE等静态资源并不会创建Session,如果没有创建成功,也可以使用request.getSession(true)强制创建Session。
  1. Session的获取
  • 通过HttpServletRequest对象的getSession方法获取一个HttpSession实例。

    1
    2
    //获取此会话
    HttpSession session = request.getSession();
  1. Session的撤销

    Session的撤销有三种可能的情况:

    • Session超时,Session的默认有效期为30分钟,你如果30分钟内没有请求服务器,Session就会撤销,你30分钟内请求了服务器,服务器就认为你active了一次,重新计算有效期。

      可以在web.xml文件里或者使用setMaxInactiveInterval(20 * 60)方法设置Session的有效期,注意的是web.xml文件里参数以分为单位,15分钟,setMaxInactiveInterval(20 * 60)中的参数以秒为单位,这里是20分钟。

      1
      2
      3
      4
      5
      6
      <web-app>
      <display-name>Archetype Created Web Application</display-name>
      <session-config>
      <session-timeout>15</session-timeout>
      </session-config>
      </web-app>
      1
      2
      //设置Session的有效期为20分钟
      session.setMaxInactiveInterval(20 * 60);
    • 通过会话对象使用invalidate方法使Session无效。

      1
      2
      //使此会话无效
      session.invalidate();
    • 程序结束

Session的属性操作

  1. Session增加属性

    Session中的属性也是以键值对的形式存储的,用setAttibute(name, value)方法添加属性,value是Object对象的,所以value不限于String类型,可以是任何数据类型。

    1
    2
    3
    //添加一个name为count的属性,值为250
    int count = 250;
    session.setAttribute("count", count);
  2. Session获取属性值

    1
    2
    //获取name为count的value值,需要强转
    int count = (int) session.getAttribute("count");
  3. Session修改属性

    Session修改属性,直接使用setAttibute(name, value)方法覆盖相同name的Session即可。

    1
    2
    3
    //把name为count的属性值由250修改到520
    session.setAttribute("count", 250);
    session.setAttribute("count", 520);
  4. Session删除属性

    Session删除属性,使用removeAttribute(name)方法,删除对应name的属性。

    1
    2
    //删除name为count的属性
    session.removeAttribute("count");

应用实例

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
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("pwd");
String rememberMe = request.getParameter("rememberMe");

// 简单的登录验证
if(isValidUser(username, password)) {
// 创建或获取session
HttpSession session = request.getSession();

// 存储登录状态
session.setAttribute("isLoggedIn", true);
session.setAttribute("username", username);

// 如果用户选择了"记住我"
if("true".equals(rememberMe)) {
session.setAttribute("rememberMe", true);
// 设置session超时时间(7天)
session.setMaxInactiveInterval(7 * 24 * 60 * 60);
}else {
// 不记住则移除属性
session.removeAttribute("rememberMe");
// 使用默认的超时时间
}
// 重定向到欢迎页面
response.sendRedirect("welcome.jsp");
} else {
// 登录失败
response.sendRedirect("index.jsp?error=1");
}
}
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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// 检查session中是否有记住的用户名
String name = "";
boolean isRemembered = false;
HttpSession existingSession = request.getSession(false);
if (existingSession != null && existingSession.getAttribute("rememberMe") != null) {
// 取出session中存储的用户名,一般只是取出,不修改
name = (String) existingSession.getAttribute("rememberMe");
isRemembered = true;
}
%>
<html>
<head>
<title>用户登录(Session版)</title>
</head>
<body>
<h2>用户登录(Session版)</h2>
<% if ("1".equals(request.getParameter("error"))) { %>
<p style="color:red;">用户名或密码错误!</p>
<% } %>
<form action="login" method="post">
账号:<input type="text" name="username" value="<%= name %>"/><br>
密码:<input type="password" name="pwd"/><br>
记住我:<input type="checkbox" name="rememberMe" value="true" <%= isRemembered ? "checked" : "" %>/><br>
登录:<input type="submit" value="登录"/>
</form>
</body>
</html>

比较ServletContext,HttpServletRequest和HttpSession

ServletContext

  • 范围最大,应用程序级别的,整个应用程序都能访问;
  • 是Servlet上下文对象,在服务器启动阶段解析web.xml文件创建ServletContext对象,在同一个web app中所有的Servlet对象共享同一个ServletContext对象。该对象一旦创建不会被销毁,除非将服务器停掉。
  • 一般存储在该对象中的数据首先是所有用户共享的,不会被修改的,少量数据
  • ServletContext对象传递数据可以跨Servlet、跨请求、跨用户(跨会话)传递数据。

HttpSession

  • 次之,会话级别的,在当前的浏览器中都能訪问,不论是在同一浏览器开多少窗体,都能够访问,可是换个浏览器就不行了,就必须又一次创建session;
  • 每一个用户都有一个这样的对象,是一个用户级别的对象,存储在该对象中的数据一般都是该用户专属的数据
  • HttpSession对象传递数据可以跨Servlet、跨请求(这些请求必须属于同一个会话)、但是不能跨用户传递数据。

HttpServletRequest

  • 范围最小,请求级别,请求结束,变量的作用域也结束(也就是仅仅是一次访问,访问结束,这个也结束)。
  • 是请求对象,一次请求一个对象,每一次请求都会新建一个请求对象,是一个请求级别的对象,存储该对象中的数据一般都是请求级别的数据,一次请求之后这个数据就不再使用的数据可以存储在该对象中
  • HttpServletRequest对象传递数据可以跨Servlet,但是不能跨请求,更不能跨用户传递数据。

尽量从小范围向大范围使用。(考虑原则:request< session < application)

ServletContext、HttpSession、HttpServletRequest接口的对比:

以上都是范围对象:

  • ServletContext application; 是应用范围
  • HttpSession session; 是会话范围
  • HttpServletRequest request; 是请求范围

三个范围的排序:application > session > request

  • application完成跨会话共享数据
  • session完成跨请求共享数据,但是这些请求必须在同一个会话当中
  • request完成跨Servlet共享数据,但是这些Servlet必须在同一个请求当中【转发】

使用原则:有小到大尝试,优先使用小范围。例如:

  • 登录成功之后,已经登录的状态需要保存起来,可以将登录成功的这个状态保存到session对象中。
  • 登录成功状态不能保存到request范围中,因为一次请求对应一个新的request对象。
  • 登录成功状态也不能保存到application范围中,因为登录成功状态是属于会话级别的,不能所有用户共享。

Cookie与Session的比较

Cookie与Session都是为了保持用户访问的连续状态,之所以为了要保持这种状态,一方面是为了实现业务方便,另一方面就是简化服务器端的程序设计,提高访问性能。但是两者的实现原理不太一样,各自都有优点和缺陷,下面通过比较说明这两者的特点和适用场合。

  1. 存取方式上

    Cookie中只能保持ASCLL字符串,如果存取Unicode字符或者二进制数据,需要进行UTF-8,GBK,或者BASE64等方式的编码,而Session中可以存取任何类型的数据。

  2. 隐私安全上

    Cookie存放在客户端浏览器,对客户端是可见的,客户端的一些程序可能会窥探复制甚至修改Cookie中的内容。而Session存放在服务器端,对用户是透明的,不存在敏感信息泄露的危险。

  3. 有效期上

    Cookie可以设置长期有效,浏览器关闭也有效,虽然Session可以设置很长的有效期,但是Session依赖名为JSESSIONID的Cookie,该Cookie的maxAge默认为-1,浏览器关闭Cookie就失效,所以该Session也就失效了。

  4. 服务器的负担上

    Session存放在服务器端,每个用户都会产生一个Session。如果并发非常大的网站,会产生大量的Session,消耗大量内存,因此像Baidu,Google这样并发量极高的网站是不会使用Session来追踪会话的,而Cookie保存在客户端,不占用服务器资源,对于并发量极高的网站Cookie是更好的选择。

  5. 从浏览器支持上

    Cookie是需要浏览器支持的,如果浏览器不支持Cookie,就需要使用Session以及URL地址重写。

  6. 从跨域名上

    Cookie支持跨域名访问,只要设置domain属性即可,但Session不能够跨域名访问,Session仅在他的域名下有效。