JavaWeb 之 Cookie 和 Session

JavaWeb 之 Cookie 和 Session

由于 Http 是一种无状态的协议,服务器单从网络连接上无从知道客户身份。

会话跟踪是 Web 程序中常用的技术,用来跟踪用户的整个会话。常用会话跟踪技术是 Cookie 与 Session。

Cookie 实际上是存储在客户端上的文本信息,并保留了各种跟踪的信息。

Cookie 工作步骤:

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

注:Cookie 功能需要浏览器的支持,如果浏览器不支持 Cookie 或者 Cookie 禁用了,Cookie 功能就会失效。

Java 中把 Cookie 封装成了javax.servlet.http.Cookie类。

Cookies 通常设置在 HTTP 头信息中(虽然 JavaScript 也可以直接在浏览器上设置一个 Cookie)。

设置 Cookie 的 Servlet 会发送如下的头信息:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Date: Fri, 04 Feb 2000 21:03:38 GMT
Server: Apache/1.3.9 (UNIX) PHP/4.0b3
Set-Cookie: name=xyz; expires=Friday, 04-Feb-07 22:03:38 GMT;
path=/; domain=w3cschool.cc
Connection: close
Content-Type: text/html

正如您所看到的,Set-Cookie 头包含了一个名称值对、一个 GMT 日期、一个路径和一个域。名称和值会被 URL 编码。expires 字段是一个指令,告诉浏览器在给定的时间和日期之后”忘记”该 Cookie。

如果浏览器被配置为存储 Cookies,它将会保留此信息直到到期日期。如果用户的浏览器指向任何匹配该 Cookie 的路径和域的页面,它会重新发送 Cookie 到服务器。浏览器的头信息可能如下所示:

1
2
3
4
5
6
7
8
9
GET / HTTP/1.0
Connection: Keep-Alive
User-Agent: Mozilla/4.6 (X11; I; Linux 2.2.6-15apmac ppc)
Host: zink.demon.co.uk:1126
Accept: image/gif, */*
Accept-Encoding: gzip
Accept-Language: en
Accept-Charset: iso-8859-1,*,utf-8
Cookie: name=xyz
方法 功能
public void setDomain(String pattern) 该方法设置 cookie 适用的域。
public String getDomain() 该方法获取 cookie 适用的域。
public void setMaxAge(int expiry) 该方法设置 cookie 过期的时间(以秒为单位)。如果不这样设置,cookie 只会在当前 session 会话中持续有效。
public int getMaxAge() 该方法返回 cookie 的最大生存周期(以秒为单位),默认情况下,-1 表示 cookie 将持续下去,直到浏览器关闭。
public String getName() 该方法返回 cookie 的名称。名称在创建后不能改变。
public void setValue(String newValue) 该方法设置与 cookie 关联的值。
public String getValue() 该方法获取与 cookie 关联的值。
public void setPath(String uri) 该方法设置 cookie 适用的路径。如果您不指定路径,与当前页面相同目录下的(包括子目录下的)所有 URL 都会返回 cookie。
public String getPath() 该方法获取 cookie 适用的路径。
public void setSecure(boolean flag) 该方法设置布尔值,向浏览器指示,只会在 HTTPS 和 SSL 等安全协议中传输此类 Cookie。
public void setComment(String purpose) 该方法规定了描述 cookie 目的的注释。该注释在浏览器向用户呈现 cookie 时非常有用。
public String getComment() 该方法返回了描述 cookie 目的的注释,如果 cookie 没有注释则返回 null。

CookiemaxAge决定着 Cookie 的有效期,单位为秒。

如果 maxAge 为 0,则表示删除该 Cookie;

如果为负数,表示该 Cookie 仅在本浏览器中以及本窗口打开的子窗口内有效,关闭窗口后该 Cookie 即失效。

Cookie 中提供getMaxAge()setMaxAge(int expiry)方法来读写maxAge属性。

Cookie 是不可以跨域名的。域名 www.google.com 颁发的 Cookie 不会被提交到域名 www.baidu.com 去。这是由 Cookie 的隐私安全机制决定的。隐私安全机制能够禁止网站非法获取其他网站的 Cookie。

正常情况下,同一个一级域名的两个二级域名之间也不能互相使用 Cookie。如果想让某域名下的子域名也可以使用该 Cookie,需要设置 Cookie 的 domain 参数。

Java 中使用setDomain(Stringdomain)getDomain()方法来设置、获取 domain。

Path 属性决定允许访问 Cookie 的路径。

Java 中使用setPath(Stringuri)getPath()方法来设置、获取 path。

HTTP 协议不仅是无状态的,而且是不安全的。

使用 HTTP 协议的数据不经过任何加密就直接在网络上传播,有被截获的可能。如果不希望 Cookie 在 HTTP 等非安全协议中传输,可以设置 Cookie 的 secure 属性为 true。浏览器只会在 HTTPS 和 SSL 等安全协议中传输此类 Cookie。

Java 中使用setSecure(booleanflag)getSecure ()方法来设置、获取 Secure。

通过 Servlet 添加 Cookies 包括三个步骤:

  1. 创建一个 Cookie 对象:您可以调用带有 cookie 名称和 cookie 值的 Cookie 构造函数,cookie 名称和 cookie 值都是字符串。
  2. 设置最大生存周期:您可以使用 setMaxAge 方法来指定 cookie 能够保持有效的时间(以秒为单位)。
  3. 发送 Cookie 到 HTTP 响应头:您可以使用 response.addCookie 来添加 HTTP 响应头中的 Cookies。

AddCookies.java

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/servlet/AddCookies")
public class AddCookies extends HttpServlet {
private static final long serialVersionUID = 1L;

/**
* @see HttpServlet#HttpServlet()
*/
public AddCookies() {
super();
}

/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 为名字和姓氏创建 Cookie
Cookie name = new Cookie("name", URLEncoder.encode(request.getParameter("name"), "UTF-8")); // 中文转码
Cookie url = new Cookie("url", request.getParameter("url"));

// 为两个 Cookie 设置过期日期为 24 小时后
name.setMaxAge(60 * 60 * 24);
url.setMaxAge(60 * 60 * 24);

// 在响应头中添加两个 Cookie
response.addCookie(name);
response.addCookie(url);

// 设置响应内容类型
response.setContentType("text/html;charset=UTF-8");

PrintWriter out = response.getWriter();
String title = "设置 Cookie 实例";
String docType = "<!DOCTYPE html>\n";
out.println(docType + "<html>\n" + "<head><title>" + title + "</title></head>\n"
+ "<body bgcolor=\"#f0f0f0\">\n" + "<h1 align=\"center\">" + title
+ "</h1>\n" + "<ul>\n" + " <li><b>站点名:</b>:" + request.getParameter("name")
+ "\n</li>" + " <li><b>站点 URL:</b>:" + request.getParameter("url")
+ "\n</li>" + "</ul>\n" + "</body></html>");
}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}

}

addCookies.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ page language="java" pageEncoding="UTF-8" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta charset="utf-8">
<title>添加Cookie</title>
</head>
<body>
<form action=/servlet/AddCookies method="GET">
站点名 :<input type="text" name="name">
<br/>
站点 URL:<input type="text" name="url"/><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>

要读取 Cookies,您需要通过调用 HttpServletRequestgetCookies() 方法创建一个 javax.servlet.http.Cookie 对象的数组。然后循环遍历数组,并使用 getName()getValue() 方法来访问每个 cookie 和关联的值。

ReadCookies.java

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/servlet/ReadCookies")
public class ReadCookies extends HttpServlet {
private static final long serialVersionUID = 1L;

/**
* @see HttpServlet#HttpServlet()
*/
public ReadCookies() {
super();
}

/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Cookie cookie = null;
Cookie[] cookies = null;
// 获取与该域相关的 Cookie 的数组
cookies = request.getCookies();

// 设置响应内容类型
response.setContentType("text/html;charset=UTF-8");

PrintWriter out = response.getWriter();
String title = "Delete Cookie Example";
String docType = "<!DOCTYPE html>\n";
out.println(docType + "<html>\n" + "<head><title>" + title + "</title></head>\n"
+ "<body bgcolor=\"#f0f0f0\">\n");
if (cookies != null) {
out.println("<h2>Cookie 名称和值</h2>");
for (int i = 0; i < cookies.length; i++) {
cookie = cookies[i];
if ((cookie.getName()).compareTo("name") == 0) {
cookie.setMaxAge(0);
response.addCookie(cookie);
out.print("已删除的 cookie:" + cookie.getName() + "<br/>");
}
out.print("名称:" + cookie.getName() + ",");
out.print("值:" + URLDecoder.decode(cookie.getValue(), "utf-8") + " <br/>");
}
} else {
out.println("<h2 class=\"tutheader\">No Cookie founds</h2>");
}
out.println("</body>");
out.println("</html>");
}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}

}

Java 中并没有提供直接删除 Cookie 的方法,如果想要删除一个 Cookie,直接将这个 Cookie 的有效期设为 0 就可以了。步骤如下:

  1. 读取一个现有的 cookie,并把它存储在 Cookie 对象中。
  2. 使用 setMaxAge() 方法设置 cookie 的年龄为零,来删除现有的 cookie。
  3. 把这个 cookie 添加到响应头。

DeleteCookies.java

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/servlet/DeleteCookies")
public class DeleteCookies extends HttpServlet {
private static final long serialVersionUID = 1L;

/**
* @see HttpServlet#HttpServlet()
*/
public DeleteCookies() {
super();
}

/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Cookie cookie = null;
Cookie[] cookies = null;
// 获取与该域相关的 Cookie 的数组
cookies = request.getCookies();

// 设置响应内容类型
response.setContentType("text/html;charset=UTF-8");

PrintWriter out = response.getWriter();
String title = "删除 Cookie 实例";
String docType = "<!DOCTYPE html>\n";
out.println(docType + "<html>\n" + "<head><title>" + title + "</title></head>\n"
+ "<body bgcolor=\"#f0f0f0\">\n");
if (cookies != null) {
out.println("<h2>Cookie 名称和值</h2>");
for (int i = 0; i < cookies.length; i++) {
cookie = cookies[i];
if ((cookie.getName()).compareTo("url") == 0) {
cookie.setMaxAge(0);
response.addCookie(cookie);
out.print("已删除的 cookie:" + cookie.getName() + "<br/>");
}
out.print("名称:" + cookie.getName() + ",");
out.print("值:" + cookie.getValue() + " <br/>");
}
} else {
out.println("<h2 class=\"tutheader\">No Cookie founds</h2>");
}
out.println("</body>");
out.println("</html>");
}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}

}

Session

Session 是什么

不同于 Cookie 保存在客户端浏览器中,Session 保存在服务器上。

如果说 Cookie 机制是通过检查客户身上的“通行证”来确定客户身份的话,那么 Session 机制就是通过检查服务器上的“客户明细表”来确认客户身份。

Session 对应的类为 javax.servlet.http.HttpSession 类。Session 对象是在客户第一次请求服务器时创建的。

Session 类中的方法

javax.servlet.http.HttpSession 类中的方法:

方法 功能
public Object getAttribute(String name) 该方法返回在该 session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null。
public Enumeration getAttributeNames() 该方法返回 String 对象的枚举,String 对象包含所有绑定到该 session 会话的对象的名称。
public long getCreationTime() 该方法返回该 session 会话被创建的时间,自格林尼治标准时间 1970 年 1 月 1 日午夜算起,以毫秒为单位。
public String getId() 该方法返回一个包含分配给该 session 会话的唯一标识符的字符串。
public long getLastAccessedTime() 该方法返回客户端最后一次发送与该 session 会话相关的请求的时间自格林尼治标准时间 1970 年 1 月 1 日午夜算起,以毫秒为单位。
public int getMaxInactiveInterval() 该方法返回 Servlet 容器在客户端访问时保持 session 会话打开的最大时间间隔,以秒为单位。
public void invalidate() 该方法指示该 session 会话无效,并解除绑定到它上面的任何对象。
public boolean isNew() 如果客户端还不知道该 session 会话,或者如果客户选择不参入该 session 会话,则该方法返回 true。
public void removeAttribute(String name) 该方法将从该 session 会话移除指定名称的对象。
public void setAttribute(String name, Object value) 该方法使用指定的名称绑定一个对象到该 session 会话。
public void setMaxInactiveInterval(int interval) 该方法在 Servlet 容器指示该 session 会话无效之前,指定客户端请求之间的时间,以秒为单位。

Session 的有效期

由于会有越来越多的用户访问服务器,因此 Session 也会越来越多。为防止内存溢出,服务器会把长时间没有活跃的 Session 从内存中删除。

Session 的超时时间为maxInactiveInterval属性,可以通过getMaxInactiveInterval()setMaxInactiveInterval(longinterval)来读写这个属性。

Tomcat 中 Session 的默认超时时间为 20 分钟。可以修改 web.xml 改变 Session 的默认超时时间。

例:

1
2
3
<session-config>
<session-timeout>60</session-timeout>
</session-config>

Session 对浏览器的要求

HTTP 协议是无状态的,Session 不能依据 HTTP 连接来判断是否为同一客户。因此服务器向客户端浏览器发送一个名为 JESSIONID 的 Cookie,他的值为该 Session 的 id(也就是 HttpSession.getId()的返回值)。Session 依据该 Cookie 来识别是否为同一用户。

该 Cookie 为服务器自动生成的,它的maxAge属性一般为-1,表示仅当前浏览器内有效,并且各浏览器窗口间不共享,关闭浏览器就会失效。

URL 地址重写

URL 地址重写的原理是将该用户 Session 的 id 信息重写到 URL 地址中。服务器能够解析重写后的 URL 获取 Session 的 id。这样即使客户端不支持 Cookie,也可以使用 Session 来记录用户状态。

HttpServletResponse类提供了encodeURL(Stringurl)实现 URL 地址重写。

META-INF/context.xml中编辑如下:

1
2
<Context path="/SessionNotes" cookies="true">
</Context>

部署后,TOMCAT 便不会自动生成名 JESSIONID 的 Cookie,Session 也不会以 Cookie 为识别标志,而仅仅以重写后的 URL 地址为识别标志了。

Session 实例

Session 跟踪

SessionTrackServlet.java

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;

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;

@WebServlet("/servlet/SessionTrackServlet")
public class SessionTrackServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 如果不存在 session 会话,则创建一个 session 对象
HttpSession session = request.getSession(true);
// 获取 session 创建时间
Date createTime = new Date(session.getCreationTime());
// 获取该网页的最后一次访问时间
Date lastAccessTime = new Date(session.getLastAccessedTime());

// 设置日期输出的格式
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

String title = "Servlet Session 实例";
Integer visitCount = new Integer(0);
String visitCountKey = new String("visitCount");
String userIDKey = new String("userID");
String userID = new String("admin");

// 检查网页上是否有新的访问者
if (session.isNew()) {
session.setAttribute(userIDKey, userID);
} else {
visitCount = (Integer) session.getAttribute(visitCountKey);
visitCount = visitCount + 1;
userID = (String) session.getAttribute(userIDKey);
}
session.setAttribute(visitCountKey, visitCount);

// 设置响应内容类型
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();

String docType = "<!DOCTYPE html>\n";
out.println(docType + "<html>\n" + "<head><title>" + title + "</title></head>\n"
+ "<body bgcolor=\"#f0f0f0\">\n" + "<h1 align=\"center\">" + title
+ "</h1>\n" + "<h2 align=\"center\">Session 信息</h2>\n"
+ "<table border=\"1\" align=\"center\">\n" + "<tr bgcolor=\"#949494\">\n"
+ " <th>Session 信息</th><th>值</th></tr>\n" + "<tr>\n" + " <td>id</td>\n"
+ " <td>" + session.getId() + "</td></tr>\n" + "<tr>\n"
+ " <td>创建时间</td>\n" + " <td>" + df.format(createTime) + " </td></tr>\n"
+ "<tr>\n" + " <td>最后访问时间</td>\n" + " <td>" + df.format(lastAccessTime)
+ " </td></tr>\n" + "<tr>\n" + " <td>用户 ID</td>\n" + " <td>" + userID
+ " </td></tr>\n" + "<tr>\n" + " <td>访问统计:</td>\n" + " <td>" + visitCount
+ "</td></tr>\n" + "</table>\n" + "</body></html>");
}
}

web.xml

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>SessionTrackServlet</servlet-name>
<servlet-class>SessionTrackServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SessionTrackServlet</servlet-name>
<url-pattern>/servlet/SessionTrackServlet</url-pattern>
</servlet-mapping>

删除 Session 会话数据

当您完成了一个用户的 session 会话数据,您有以下几种选择:

移除一个特定的属性:您可以调用 removeAttribute(String name) 方法来删除与特定的键相关联的值。

删除整个 session 会话:您可以调用 invalidate() 方法来丢弃整个 session 会话。

设置 session 会话过期时间:您可以调用 setMaxInactiveInterval(int interval) 方法来单独设置 session 会话超时。

注销用户:如果使用的是支持 servlet 2.4 的服务器,您可以调用 logout 来注销 Web 服务器的客户端,并把属于所有用户的所有 session 会话设置为无效。

web.xml 配置:如果您使用的是 Tomcat,除了上述方法,您还可以在 web.xml 文件中配置 session 会话超时,如下所示:

1
2
3
<session-config>
<session-timeout>15</session-timeout>
</session-config>

上面实例中的超时时间是以分钟为单位,将覆盖 Tomcat 中默认的 30 分钟超时时间。

在一个 Servlet 中的 getMaxInactiveInterval() 方法会返回 session 会话的超时时间,以秒为单位。所以,如果在 web.xml 中配置 session 会话超时时间为 15 分钟,那么getMaxInactiveInterval() 会返回 900。

存取方式

Cookie 只能保存ASCII字符串,如果需要存取 Unicode 字符或二进制数据,需要进行UTF-8GBKBASE64等方式的编码。

Session 可以存取任何类型的数据,甚至是任何 Java 类。可以将 Session 看成是一个 Java 容器类。

隐私安全

Cookie 存于客户端浏览器,一些客户端的程序可能会窥探、复制或修改 Cookie 内容。

Session 存于服务器,对客户端是透明的,不存在敏感信息泄露的危险。

有效期

使用 Cookie 可以保证长时间登录有效,只要设置 Cookie 的maxAge属性为一个很大的数字。

而 Session 虽然理论上也可以通过设置很大的数值来保持长时间登录有效,但是,由于 Session 依赖于名为JESSIONID的 Cookie,而 Cookie JESSIONIDmaxAge默认为-1,只要关闭了浏览器该 Session 就会失效,因此,Session 不能实现信息永久有效的效果。使用 URL 地址重写也不能实现。

服务器的开销

由于 Session 是保存在服务器的,每个用户都会产生一个 Session,如果并发访问的用户非常多,会产生很多的 Session,消耗大量的内存。

而 Cookie 由于保存在客户端浏览器上,所以不占用服务器资源。

浏览器的支持

Cookie 需要浏览器支持才能使用。

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

需要注意的事所有的用到 Session 程序的 URL 都要使用response.encodeURL(StringURL)response.encodeRediretURL(String URL)进行 URL 地址重写,否则导致 Session 会话跟踪失效。

跨域名

  • Cookie 支持跨域名。
  • Session 不支持跨域名。