HttpServletResponse.addCookie(Cookie cookie)
Cookie[] HttpServletRequest.getCookies()
setMaxInactiveInterval(int interval)
ServletContext getServletContext()
void setAttribute(String name, Object value)
Object getAttribute(String name)
void removeAttribute(String name)
Enumeration getAttributeNames()
HttpServletRequest接口中的Session方法
会话与状态管理
HTTP协议是一种无状态的协议。
浏览器需要对其发出的每个请求消息都进行标识,属于同一个会话中的请求消息都附带同样的标识号,而属于不同会话的请求消息总是附带不同的标识号,这个标识号就称之为会话ID(SessionID)。
会话ID可以通过一种称之为Cookie的技术在请求消息中进行传递,也可以作为请求URL的附加参数进行传递,会话ID是wb服务器为每个客户端浏览器分配的一个惟一代号,它通常是在服务器接收到某个浏览器的第一次访问时产生的,并且随同响应消息一道发送给浏览器,浏览器则在以后发出的访问请求消息中都顺带一条信息:“我的代号(即SessionID)是xxx”。这样,Web服务器程序就能识别出这次访问请求是来自哪个客户端浏览器了。
cookie
Cookie不是HTTP1.1的内容,它是Netscape的一个扩展,为浏览器和服务器之间提供了一种有效的状态信息交换方式。
Cookie是一种在客户端保持HTTP状态信息的技术。它是浏览器访问服务器时,由服务器在HTTP响应消息头中附带传送给浏览器的一片数据,不同的浏览器有不同的数据。浏览器可以决定是否保存这片数据,一旦Web浏览器保存了这片数据,那么它在以后访问该服务器时,都应该在HTTP请求头中将这片数据回传给服务器。
服务器在响应头中增加 Set-Cookie 响应头字段将 Cookie 信息发送给浏览器,浏览器则在请求消息中增加Cookie请求头字段将Cookie回传给服务器。
不保存在硬盘中的Cookie信息是否可以被同一台计算机上启动的多个浏览器进程共享,不同的浏览器有不同的处理方式。IE不能共享,这就会导致出现同一台机上的每个浏览器进程都会与服务器形成各自独立的会话;而Firefox浏览器来说,所有的进程和标签而都共享Cookie信息。另外,在IE中接Ctrl+N打开的窗口或是用javaScript的window.open语句打开的容器,都会共享原窗口的Cookie信息,因为它们属于同一个浏览器进程内部的多个窗口。
为确保cookie不能恶意使用,浏览器还对cookie的使用进行了一些限制:
? 每个域最多只能在一台用户的机器上存储20个cookie。
? 每个cookie的总大小不能超过4096字节。
? 一台用户的机器上的cookie的总数不能超过300个。
组成
l 名称——每个cookie由一个唯一的名称表示。名称不区分大小写,但最好是区分,因为有些服务器端软件是这样的。
l 值——保存在cookie中的字符串值。这个值在存储之前必须用encodeURIComponent()对其进行编码,以免丢失数据或占用了cookie。注:名称和值一起的字节数不能超4KB。
l 域——出于安全考虑,网站不能访问由其他域创建的cookie。创建cookie后,域的信息会作为cookie的一部分存储起来。不过,虽然这不常见,还是可以覆盖这个设置以允许另一个网站访问这个cookie的。
l 路径——另一个cookie的安全特征,路径限制了对WEB服务器上的特定目录的访问。例如:可指定cookie只能从http://www.wrox.com/books中访问,这样就不能访问http://www.wrox.com/上的网页了,尽管都在同一个域中。
l 失效日期——cookie何时应该被删除。默认情况下,关闭浏览器时,即将cookie删除;不过,也可以自己设置删除时间。这个值是个GMT格式的日期(可以使用Date对象的toGMTString()方法),用于指定应该删除cookie的准确时间。因此,cookie可在浏览器关闭后依然保存在用户的机上。如果你设置的失效日期是以前时间,则cookie被立即删除。
l 安全标志——一个true/false值,用于表示cookie是否只能从安全网站(使用SSL和https协议的网站)中访问。可以将这个值设置为true以提供加强的保护,进而确保cookie不被其他网站访问。
Set-Cookie2响应头
Set-Cookie2响应头用于指定服务器向客户端传送的Cookie内容。按照Netscape规范实现Cookie功能的服务器,使用的是Set-Cookie头,而不是Set-Cookie头。在目前的实际使用中,Set-Cookie比Set-Cookie2更为常见,但两者语法和作用类似,现以Set-Cookie2来讲解。
Set-Cookie2: user=jzj; version=1; path=/
上面开头是若干键值对,除了键值对必须位于最前面外,其他可选属性的先后顺序可以任意。
下面是Set-Cookie2头中各个属性的介绍。
Comment=value
该属性说明这个Cookie的作用和对用户的私人信息可能造成影响等,浏览器向用户提示这部分信息,让用户决定是否接收这个Cookie并继续同服务器会话。value部分字符必须使用UTF-8编码。
Discard
该属性没有值部分,用于通知浏览器关闭时无条件地删除该Cookie信息。如果没有设置这个属性,浏览器删除Cookie行为由Max-Age属性决定。
Domain=value
用于指定Cookie在哪个域中有效,浏览器访问这个域中的所有主机时,都将回传这个Cookie信息。如下面的Cookie信息对 xxx.com域中的所有主机有效:
Set-Cookie2: user=jzj; version=1; path=/; Domain=.xxx.com
Domain属性默认值为当前主机名,浏览器以后只有在访问与当前主机名完全相同的服务器时,才会将这个Cookie回送给该服务器。
Max-Age=value
Max-Age属性用于指定Cookie在客户端保持有效的时间,其中的value部分是以秒为单位的十进制整数,当value部分被设置为0时,浏览器将立即删除除前面保存的这个Cookie。如果value部分被设置成一个大于0的整数,浏览器将在文件系统中保存这个Cookie信息息,直到没有超过指定的秒数之前,这个Cooke都保持有效,并且同一台计算机上运行的所有浏览器进程都可以使用这个Cookie信息。如果没有设置Max-Age属性,浏览器将这个Cookie保存在自身进程的内存中,Cookie信息随这个浏览器进程的关闭而消失,并且这个Cookie信息对同一台计算机上的其他浏览器进程无效(注,Firefox是可以共享内存中的Cookie信息的)。
Path=value
指定Cookie对服务器上的哪个URL目录和其子目录有效。这个属性的默认值是产生Set-Cookie2头字段时的那个请求URL地址所在的目录,这样,如果没有设置Path属性时,浏览器以后在访问当前请求URL地址所在的目录及其子目录下的任何资源时都将回传该Cookie信息,而访问服务器上的其他目录下的资源进都不回传该Cookie信息。
Port=portlist
指定浏览器通过哪些端口访问Web服务器时,Cookie才有效,端口号列表中的每个端口号之间使用逗号分隔,并且列表使用双引号引起来。默认是任意端口。
Secure
没有值。它用于通知浏览器在回传这个Cookie信息时,就使用安全的方式访问服务器,以保护Cookie中的机密和认证方面的信息,否则,浏览器不能访问这个被存储的Cookie信息。
Version=value
用于指定Cookie内容所遵循的版本格式,为一个十进制整数,目前只有Version=1可用。
Web服务器使用Set-Cookie2头字段发送一个Cookie以后,它在以后的响应消息中可以不再发送针对该Cookie的Set-Cookie2头字段,也可以发送与以前完全相同的Set-Cookie2头字段,还可以发送对以前信息进行了修改的Set-Cookie2头字段。如果浏览器接收到的Set-Cookie2头字段中设置的Cookie与以前存储的Cookie的名称相同,并且Domain属性值(不区分大小写)和Path属性值〔区分大小写)都相等,那么就可以认为这两个Cookie代表的是同一个信息,新的Cookie将替换以前的Cookie。如果服务器想让浏览器端保存的某个Cookie失效,可以重新发送包含该Cookie的Set-Cookie2头字段,并在设置值部分增加Max-Age=M属性
一个响应头中可以包含多个Set-Cookie2头字段来设置多个Cookie,也可以用一个Set-Cookie2头字段来设置多个Cookie。在响应头中用一个Set-Cookie2头字段设置多个Cookie是一种非正规的格式,其中的每个Cookie之间用逗号分隔,Cookie内部的每个属性之间用分号和空格分隔。
Cookie请求头
多个Cookie信息可以且只能通过一个Cookie请求头字段回送给Web服务器,即对于服务器使用多个Set-Cookie2响应头字段发送来的多个Cookie信息,浏览器应将它们集中到一个Cookie请求送字段中再回传服务器。
浏览器向服务器发送访问请求时,根据下面规则来决定是否发送Cookie请求头,以及在Cookie请求头字段中附带哪些Cookie信息:
1、 请求的主机名是否与某个存储的Cookie的Domain属性匹配;
2、 请求的端口号是否在该Cookie的Port属性列表中;
3、 请求的资源路径是否在该Cookie的 Path属性指定的目录及子目录中;
4、 该Cookie是否过期;
如果有多个存储的Cookie满足上面规则,浏览器将把这些Cookie信息都添加到一个Cookie请求头中,每个Cookie之间用逗号或分号分隔。为了兼容以前的情况,目前出现在Cookie请求头字段中的每个Cookie之间最好都用分号,但服务器端为了兼容未来的情况,应该也能接受Cookie之间的逗号作为分隔符的情况。在Version、Path、Domain、Port等属性名前,都要增加一个“$”字符作为前缀。Version属性只能出现一次,且要位于Cookie请求头设置值的最前面。对于使用Netscape规范的浏览器,就将Version属性设置为0,并且Path、Domain、Port等属性必须位于该Cookie信息的“名=值”设置之后 ,Path属性指向的子目录的Cookie要排在在Path属性指向父目录的Cookie之前:
Cookie: $Version=1; Course=java; $Path=/xxx/lesson; Course=vc; $Path=/xxx
由于“/xxx/lesson”是“/xxx”的子目录,所以Path属性等于“/xxx/lesson”的Cookie信息排在了Path属性等于“/xxx”的Cookie信息的前面。
在Servlet中使用Cookie
Servlet API中提供了一个javax.servlet.http.Cookie类来封装Cookie信息。在HttpServletResponse中定义了一个addCookie方法来向浏览器发送Cookie信息,在HttpServletRquest中定义了一个getCookies方法来读取浏览器回传的Cookie信息。
Cookie类
构造方法
Cookie(java.lang.String name, java.lang.String value)
参数分别表示Cookie的名和值,名中不能包含任何空白字符、逗号、分号,并不能以$字符开头。
getName
String getName()
返回cookie的名,一旦创建名称不能被修改。
setValue、getValue
setValue(java.lang.String newValue)
String getValue()
设置与返回Cookie的值。
setMaxAge、getMaxAge
int getMaxAge()
setMaxAge(int expiry)
分别用于设置和返回Cookie在浏览器客户机上保持有效的秒数。如果设置值为0,则通知浏览器立即删除这个Cookie项。如果设置值为负数,则表示在浏览器关闭时删除这个Cookie项,这与没有调用Cookie对象的setMaxAge方法的效果一样,Cookie项保留在接收它的浏览器进程的内存中,并且只对该浏览器进程可见,对同一台计算机上的其他浏览器进程不可见,对于这种情况,getMaxAge方法返回-1。当设置值为正数时,浏览器将该Cookie项保留在客户机的硬盘上,这个Cookie对该客户机上的所有浏览器进程都有效,即使关闭浏览鉴和计算机以后,只要设置的时间还没到期,该Cookie就一直有效。
setPath、getPath
setPath与getPath方法分别用于设置和返回该Cookie项的有效目录路径。如果创建某个Cookie对象时没有设置它的Path属性,那么该Cookie只对当前访问路径所属的目录及其子目录有效,如果想让某个Cookie项对站点的所有目录下的访问路径都有效:应调用Cookie对象的setPath方法将其Path属性设置为“/”。
setDomain、getDomain
设置和返回Cookie项的有效域
setVersion、getVersion
Cookie的协议版本
setComment、getComment
Cookie的注解部分
setSecure、getSecure
是否只能使用安全协议传送Cookie。
HttpServletResponse.addCookie(Cookie cookie)
用于在发送给浏览器的响应消息中增加一个Set-Cookie响应头,这个头的设置值由传递给该方法的一个Cookie对象生成,在Servlet程序中可以多次调用addCookie方法来设置多个Set-Cookie头字段。尽量不要使用HttpServletResponse.setHeader方法来直接设置Set-Cookie头。
Cookie[] HttpServletRequest.getCookies()
用于从请求消息的Cookie请求头中读取所有的Cookie项,将每个Cookie项封装成各自的Cookie对象后,存在一个数组中返回。不要使用HttpServletRequest.getHeader方法来直接读取Cookie请求头。
记住登录名应用
=======================登录页面实现=======================
<%
//读取请求消息中的所有cookie信息,用来是否让“记住公司ID”与“记住用户名”选中,以及公司ID与用户名框中的内容回填
Cookie[] allcookie = request.getCookies();
String custID = "";//公司ID
String userID = "";//用户名
if (allcookie != null) {//如果浏览器请求的消息头中含有cookie信息
for (int i = 0; i < allcookie.length; i++) {
String idx = allcookie[i].getName();//cookie的名字
String value = allcookie[i].getValue();//cookie的值
if(allcookie[i].getName() != null
&& allcookie[i].getName().equals("cookie_custID")
&& allcookie[i].getMaxAge() != 0){//如果以前 记住过公司ID
custID = allcookie[i].getValue();
}else if(allcookie[i].getName() != null //如果以前 记住过用户名
&& allcookie[i].getName().equals("cookie_userID")
&& allcookie[i].getMaxAge() != 0){
userID = allcookie[i].getValue();
}
}
}
%>
<tr>
<td width="100" align="right" height="17" class="bnt_txt">公司ID:</td>
<td width=""><input type="text"
value="<%=request.getParameter("corporationID") == null
? (custID == null ? "" : custID)
: request.getParameter("corporationID")%>"
name="corporationID" size="21" /> <input
name="memorizeCustID" type="checkbox" value="true"
<%if (!custID.equals("")) {%> checked <%}%> />记住公司ID</td>
</tr>
<tr>
<td align="right" class="bnt_txt" style="height: 23px">用户名:</td>
<td style="height: 23px"><input type="text"
value="<%=request.getParameter("userID") == null
? (userID == null ? "" : userID)
: request.getParameter("userID")%>"
name="userID" size="21" /> <input name="memorizeUserID"
type="checkbox" value="true" <%if (!userID.equals("")) {%> checked
<%}%> />记住用户名</td>
</tr>
<tr>
<td align="right" class="bnt_txt">密 码:</td>
<td><input type="password" name="pwd" size="21" value="" />
</td>
</tr>
<tr valign="middle" height="30">
<td align="right" class="bnt_txt" valign="middle" valign="middle">认证码:</td>
<td valign="middle"><input style=" 60px" type="text"
name="validateCode" maxlength="5" value="" /> <img
id="validCode" src="<%=request.getContextPath()%>/validatecode"
border="0" onClick="rnd()" style="cursor: pointer;" /></td>
</tr>
<tr>
<td></td>
<td><input class="button" type="submit" value=" 登 录 "
name="button1" /> <input class="button "
onClick="window.opener=null;window.open('','_self','');window.close();"
type="button" value=" 取 消 " name="button2" /> <a
href="<%=request.getContextPath()%>/custapp/reg.jsp">注册使用系统</a></td>
</tr>
=======================登录实现=======================
// --记住公司ID与用户名
Cookie[] allcookie = request.getCookies();
Cookie cookie = null;
//如果选中了记住公司ID时
if (request.getParameter("memorizeCustID") != null
&& request.getParameter("memorizeCustID").equals("true")) {
cookie = new Cookie("cookie_custID", corporationID);
// 有效期一个星期
cookie.setMaxAge(604800);
response.addCookie(cookie);
}//如果没有选中
else if (request.getParameter("memorizeCustID") == null) {
if (allcookie != null)//浏览器是否发送了cookie
{
for (int i = 0; i < allcookie.length; i++) { //如果浏览器上传的cookie中含 记住公司ID 复选框信息
if (allcookie[i].getName() != null
&& allcookie[i].getName().equals("cookie_custID")) {
allcookie[i].setMaxAge(0);//删除以前 记住公司ID cookie
response.addCookie(allcookie[i]);
break;
}
}
}
}
//如果选中了 记住用户名 复选框
if (request.getParameter("memorizeUserID") != null
&& request.getParameter("memorizeUserID").equals("true")) {
cookie = new Cookie("cookie_userID", userID);
// 有效期一个星期
cookie.setMaxAge(604800);
response.addCookie(cookie);
}//如果没有选中,且存在 记住用户名 cookie时,删除它
else if (request.getParameter("memorizeUserID") == null) {
if (allcookie != null) {
for (int i = 0; i < allcookie.length; i++) {
if (allcookie[i].getName() != null
&& allcookie[i].getName().equals("cookie_userID")) {
allcookie[i].setMaxAge(0);//为0就是立即删除
response.addCookie(allcookie[i]);
break;
}
}
}
}
Cookie测试
public class CookieTest extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
//System.out.println(headerName +" = " + request.getHeader(headerName));
Enumeration values = request.getHeaders(headerName);
while (values.hasMoreElements()) {
System.out.println(headerName + " = " + values.nextElement());
}
}
System.out.println("---------------------------------");
Cookie[] cks = request.getCookies();
for (int i = 0; cks != null && i < cks.length; i++) {
System.out.println(cks[i].getName() + " = " + cks[i].getValue());
}
response.setContentType("text/html;charset=gb2312");
PrintWriter out = response.getWriter();
String name = request.getParameter("name");
String nickname = request.getParameter("nickname");
Cookie ckName = new Cookie("name", name);//
Cookie ckNickname = new Cookie("nickname", nickname);//
ckNickname.setMaxAge(365 * 24 * 3600);//有效期为一年
Cookie ckEmail = new Cookie("email", "xxx@126.com");
Cookie ckPhone = new Cookie("phone", "13711111111");
response.addCookie(ckName);
response.addCookie(ckNickname);
response.addCookie(ckEmail);
response.addCookie(ckPhone);
}
}
第一次访问,在地址栏中输入 http://localhost:8080/myapp/CookieTest?name=jzj&nickname=javafan
在浏览器端生成的Cookie文件中只保存了名为 nickname 的Cookie信息,而没有保存其他名称的Cookie信息,这是因为在CookieTest程序中只设置了 nickname 这个Cookie保持有效的时间,而没有设置其他Cookie保持有效的时间,除了 nickname 以外的其他Cookie都将在浏览器关闭时消失。
第一次命令窗口中没有输出任何Cookie信息,因为第一次访问前还没有向浏览器添加Cookie 。如果第二次刷新页面,则命令窗显示如下:
在第二次刷新页面后,现开启第三个浏览器,命令窗口显示(如果在Firefox上则可以共享内存中的Cookie信息):
可见不同浏览器只能共享已存储在cookie文件中的信息,而不能共享浏览器未存储到Cookie文件中的Cookie信息,比如内存中的Cookie信息。再在第二个浏览器上刷新页面,命令窗口显示如下:
现将上面的Servlet映射成另外一个Servlet,如下访问:
http://localhost:8080/myapp/servlet/CookieTest?name=jzjleo&nickname=jspfan
命令窗口显示如下:
发现在临时文件夹中生成了另外一个Cookie文件,因为路径不一样,所以是不同的cookie,另外,子目录的Cookie信息会排在父目所对应的Cookie信息前:
另外从命令窗口可以看出当访问子目录(localhost/myapp/servlet/)的资源时,父目录(localhost/myapp)的cookie信息都能访问,但父目录不能访问子目录的Cookie信息,如现在在同一浏览器中使用父目录请求URL:http://localhost:8080/myapp/CookieTest?name=jzj&nickname=javafan进行访问时,发现命令窗口没有显示子目录的Cookie信息:
如果禁止掉浏览器对Cookie支持,则浏览器不会回传Cookie信息,但服务器还是会设置 Cookie头信息,因为服务器并不知道浏览已禁止掉Cookie。
另外使用telnet访问服务,可以看到生成的Cookie头信息:
GET /myapp/CookieTest?name=jzj&nickname=javafan HTTP/1.1
Host:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: name=jzj
Set-Cookie: nickname=javafan; Expires=Thu, 21-Jul-2011 17:11:49 GMT
Set-Cookie: email="xxx@126.com"; Version=1
Set-Cookie: phone=13711111111
Content-Type: text/html;charset=gb2312
Content-Length: 0
Date: Wed, 21 Jul 2010 17:11:49 GMT
注,上面的过期属性为Expires,而不是Max-Age,这可以通过查阅有关文档规范去了解。
JavaScript中的cookie
document对象有个cookie属性,是包含所有符合当前页面可访问的cookie的字符串,但不包括Cookie属性。cookie属性很特别,你对它的修改只是对加载到内存中的Cookie进行操作,并不会真真影响到硬盘上的Cookie文件本身,浏览器最后以内存中的Cookie信息提交到服务器。
要创建一个cookie,必须按照下面的格式创建字符串:
cookie_name=cookie_value; expires=expiration_time; path=domain_path; domain=domain_name;secure
只有第一部分对设置cookie是必需的,其他部分都可以省。然后将这个字符串复制给document.cookie属性,即可创建cookie。如:
document.cookie='name=Nicholas';
document.cookie='book=' + encodeURIComponent('Professional JavaScript');
读取document.cookie的值即可以访问这些cookie,以及所有其他可以从给定页面访问的cookie。如果在运行上面两行代码后显示 document.cookie 的值,则出现'name=Nicholas;book=Professional%20JavaScript'。注,即使创建了其他的cookie属性,如失效时间,document.cookie也只返回每个cookie的名称和值,并用分号来分隔这此cookie。因为创建和读取cookie均需记住它的格式,大部分开发人员用函数来处理这些细节。cookie操作函数:
//创建cookie
function setCookie(sName, sValue, oExpires, sPath, sDomain, bSecure) {
var sCookie = sName + "=" + encodeURIComponent(sValue);
// 除sName, sValue外,其他参考可选,所以使用前要判断是否传入
if (oExpires) {
// 时间要是GMT格式
sCookie += "; expires=" + oExpires.toGMTString();
}
if (sPath) {
sCookie += "; path=" + sPath;
}
if (sDomain) {
sCookie += "; domain=" + sDomain;
}
if (bSecure) {
sCookie += "; secure";
}
document.cookie = sCookie;
}
// 通过传入的名字获取cookie的值
function getCookie(sName) {
var sRE = "(?:; )?" + sName + "=([^;]*);?";
var oRE = new RegExp(sRE);
if (oRE.test(document.cookie)) {
return decodeURIComponent(RegExp["$1"]);
} else {
return null;
}
}
// 删除cookie,cookie名、域、路径可以确定一个Cookie
function deleteCookie(sName, sPath, sDomain) {
// 删除cookie必须给出与创建它时一样的路径和域信息。
var sCookie = sName + "=; expires=" + (new Date(0)).toGMTString();
if (sPath) {
sCookie += "; path=" + sPath;
}
if (sDomain) {
sCookie += "; domain=" + sDomain;
}
document.cookie = sCookie;
}
alert("Setting cookies...");
setCookie("name", "Nicholas");
setCookie("book", "Professional JavaScript");
alert("The value of cookie 'name' is " + getCookie("name"));
alert("The value of cookie 'book' is " + getCookie("book"));
alert("Deleting cookies...");
deleteCookie("name");
deleteCookie("book");
alert("The value of cookie 'name' is " + getCookie("name"));
alert("The value of cookie 'book' is " + getCookie("book"));
JavaScript适用于操作静态页面的Cookie,如果是Jsp页面,最好用服务器端的Cookie类会方便操作。
Session
Session跟踪机制
由于创建session对象会消耗内存资源,Web服务器并不会在客户端开始访问它时就创建只有客户端访问某个特殊的Servlet程序,并且这个Servlet程序决定与客户端开启一个会话时,Web应用程序才会创建一个与该客户端对应的session对象。并为这个Session对象分配一个独一无二的会话标识号(SessionID),然后在响应消息中将这个会话标识号传递给客户端。客户端需要记住会话标识号,并在后续的每次访问请求中都把这个会话标识号传送给务器, 服务器程序依据回传的会话标识号就知道这次请求是哪个客户端发出的,从而选择与之对应的session对象。
Session超时管理
在HTTP协议中,服务器无法判断当前的客户端浏览器是否还会继续访问,也无法检测客户端浏览器是否关闭,所以,即使客户已经离开或关闭了浏览器,Web服务器也还要保留与之对应的session对象。显然,“只要关闭了浏览器,Session也就消失了”的这种常见说法是错误的。
confweb.xml:
<session-config>
<session-timeout>30</session-timeout>
</session-config>
HttpSession接口方法
getId
返回session对象的会话标识号。
long getCreationTime()
session对应的创建时间,毫秒数。
long getCreationTime()
最近一次访问当前session的毫秒数。
setMaxInactiveInterval(int interval)
设置超时时间,即客户端最后一次访问以来可以空闲的时间。如果为负数,则表示会话永不超时。单位秒。
int getMaxInactiveInterval()
返回超时时间,单位秒。
boolean isNew()
isNew方法返回当前HttpSession对象是否是新创建的,如果是则返回true,否则返回false。如果客户端请求消息中有一个与Servlet程序当前获得的HttpSession对象的会话标识号相同的会话标识号,则认为这个HttpSesaion对象不是新建的,isNew方法返回false。在下列情况下,HttpSeseton对象都需要新建,isNew方法返回true:
(1)请求消息中没有通过任何方式传递会话标识号,这种情况发生在某个客户端浏览器首次访问某个能开启会话功能的Servlet程序时。
(2)请求消息中通过某种方式返回了会话标识号但是传递的会话标识号与当前HtipSession对象中保存的会话标识号不匹配,这种情况发生在客户端浏览器超时后再次访问某个能开启会话功能的Servlet程序时。
如果服务器使用了基本Cookie的Session,客户端又禁止了Cookie,则这个session对于每次请求都是一个新的。
invalidate()
强制使当前Session失效,立即释放该Session对象,并解除绑定在它上面的对象,不用等到超时后才释放该Session。
Web页面上的注销操作,内部实现就是调用了该方法来结束会话。
ServletContext getServletContext()
返回当前Session对象所属于的Web应用程序对象,即代表当前Web应用程序的ServletContext对象。
void setAttribute(String name, Object value)
如果已存在,则替换,如果值为null,则相当于removeAttribute。
绑定时,容器会调用实现了HttpSessionBindingListener的valueBound方法。
如果已经绑定了,则容器会调用HttpSessionBindingListener.valueUnbound方法。
Object getAttribute(String name)
不存在时返回null
void removeAttribute(String name)
不存在时什么也不做。
容器会调用HttpSessionBindingListener.valueUnbound方法。
Enumeration getAttributeNames()
反回一个包含当前Session对象中的所有属性名的Enumeration对象。
HttpServletRequest接口中的Session方法
1、 getSession方法
getSession方法用于返回与当前请求相关的session对象。该方法有两种重载形式:
.HttpSession getSession(boolean create)
.HttpSession getSession()
当调用这两个方法时,如果客户端的请求消息中包含有SessionID,服务器检索并返回与这个Sessionm对应的HttpSession对象,如果请求消息中没有SessionID(通常发生在会话的第一次请求的情况)或服务器端不存在与SessionID对应的HttpSesston对象(通常发生在以前的HttpSession对象已超时的情况),第一个方法根据传递的参数来决定是否创建新的HttpSession对象,如果参数为true,则在相关的HttpSesstion对象不存在时创建并返回新的HttpSession对象,否则不创建新的HttpSession对象,而是返回null。第二个方法相当于第一个方法的参数为true时的惰况,在相关的HttpSession对象不存在时总是创建新的HttpSession对象。注意,由于getSession方方法可能会产生发送会话标识号的Cookie头字段,因此必须在发送任何响应内容之前调用getSession方法。
什么情况下该调用request.getSession(false)方法?
很多Web开发人五经常困惑这样的一个问题,即不知道在什么情况下该调用request.getSession(false)方法?有时候,为了节省Web服务器端的内存资源,不要创建一些无价值的HttpSession对象,系统需要限定一个会话只能从某个Servlet程序开始,而不能从其他Servlet程序开始,例如,一些Web应用程序要求只有用户成功登录后才真正开启与客户端的会话过程。为了简化系统的复杂性和易维护性,系统可以限定只有某一个Servlet程序可以调用request.getSession()方法创建session对象,而其他的Servlet则只能调用request.getSession(false)方法来获得Session对象,如果其他Servlet程序调用request.getSessioa(false)方法未获得Session对象则跳转到
登录页面。这样,可以确保其他Servlet不会创建HttpSession对象,只有用户成功登录后,那个限定的Servlet程序才会创建Httpsession对象。如果其他Servlet程序中调用的是request.getSession()方法,那么只要用户访问了其他Servlet程序,即使用户没有成功登录,但与该客户端对应的session对象却被创建了,这就造成了一些
不必要的内存开销。
2、 boolean isRequestedSessionIdValid()
检查请求消息中的 sessionID 是否还有效。如果客户端没有指定 sessionID,则返回fale。session超时后也会返回false。
3、 boolean isRequestedSessionIdFromCookie()
判断SessionID是否通过请求消息中的Cookie传递过来的。
4、 boolean isRequestedSessionIdFromURL()
判断SessionID是否通过请求消息中的URL参数传递过来的。
利用Cookie实现Session跟踪
如果服务器处理某个访问请求时创建了新的HttpSession对象,它将把会话标识号作为一个Cookie项加入到响应消息中,遇常情况下,浏览器在随后发出的访问请求中又将会话标识号以Cookie的形式回传给服务器。服务器端程序依据回传的会话标识号就知道以前己经为该客户端创建了HttpSession对象,不必再为该客户端创建新的HttpSession对象,而是直接使用与该会话标识号匹配的HttpSession对象,通过这种方式就实现了对同一个客户端的会话状态的跟踪。
public class SessionTest extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
System.out.println("getQueryString = " + request.getQueryString());
System.out.println("----------------------------------------");
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
//System.out.println(headerName +" = " + request.getHeader(headerName));
Enumeration values = request.getHeaders(headerName);
while (values.hasMoreElements()) {
System.out.println(headerName + " = " + values.nextElement());
}
}
System.out.println("----------------------------------------");
HttpSession session = request.getSession();
System.out.println("getId = " + session.getId());
System.out.println("isNew = " + session.isNew());
System.out.println("getCreationTime =" + session.getCreationTime());
System.out.println("getLastAccessedTime ="
+ session.getLastAccessedTime());
System.out.println("getMaxInactiveInterval ="
+ session.getMaxInactiveInterval());
System.out.println("isRequestedSessionIdFromCookie ="
+ request.isRequestedSessionIdFromCookie());
System.out.println("isRequestedSessionIdFromURL ="
+ request.isRequestedSessionIdFromURL());
System.out.println("isRequestedSessionIdValid ="
+ request.isRequestedSessionIdValid());
System.out.println();
}
}
Telnet:
GET /myapp/SessionTest HTTP/1.1
Host:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=89AD5BC804973DED41C06AC84893CE23; Path=/myapp
Content-Length: 0
Date: Thu, 22 Jul 2010 03:59:34 GMT
从响应头可以看出,服务器发送给客户端的会话标识号的Cookie名称为JSESSIONID。JSESSIONID这个Cookie项的path属性为当前Web应用程序的根目录,这说明浏览器在访问其他Web应用程序时将不回送该Cookie信息,因此,两个Web应用程序不能共享会话状态,它们与同一个客户端浏览器保持各自的独立的会话。
命令窗口输出:
getQueryString = null
----------------------------------------
host =
----------------------------------------
getId = 89AD5BC804973DED41C06AC84893CE23
isNew = true
getCreationTime =1279771538312
getLastAccessedTime =1279771538312
getMaxInactiveInterval =1800
isRequestedSessionIdFromCookie =false
isRequestedSessionIdFromURL =false
isRequestedSessionIdValid =false
每次使用telnet时,都会新创建一个Session,因为请求时SessionID没有传回到服务器。现将SessionID通过Cookie消息头传回到服务器:
Telnet:
GET /myapp/SessionTest HTTP/1.1
Host:
cookie:JSESSIONID=89AD5BC804973DED41C06AC84893CE23;
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Length: 0
Date: Thu, 22 Jul 2010 04:19:29 GMT
从上面的输出可以看出,服务器没有再在响应头中添加Set-Cookie头信息了。此时并没有创建新的Session,而是共享以前创建的Session。
命令窗口输出:
getQueryString = null
----------------------------------------
host =
cookie = JSESSIONID=89AD5BC804973DED41C06AC84893CE23;
----------------------------------------
getId = 89AD5BC804973DED41C06AC84893CE23
isNew = false
getCreationTime =1279771538312
getLastAccessedTime =1279771684640
getMaxInactiveInterval =1800
isRequestedSessionIdFromCookie =true
isRequestedSessionIdFromURL =false
isRequestedSessionIdValid =true
现如果禁止掉浏览器的Cookie,则每次都会新创建一个Session,因为浏览器不支持Cookie,导致无法将SessionID传递给浏览器。注,禁止掉Cookie后要通过真实的IP进行访问,否则将可能不会生效。
利用URL重写实现Session跟踪
将会话标识事情以参数形式附加在超链接的URL地址后面的技术称为URL重写。
当请求消息头中没有包括 Cookie 头时,服务器就认为客户端不支持Cookie。
Tomcat服务器发送给客户端的会话标识号的Cookie名称为JSESSIONID,因此,Tomcat服务器中的URL重写就是要将jsessionid关键字作为参数名(注意是小写,这与Cookie头中的不一样,Tomcat在传给浏览器的响应头的Set-Cookie头中的Cookie名为全大写的JSESSIONID,但在重写URL中的SessionID参数名jsessionid全小写)和将会话标识号作为参数值附加到URL后面。如果在浏览器不支持Cookie或者关闭了Cookie功能的情况下,Web服务器还要能够与浏览器实现现有状态会话,就必须对所有可能被客户端访问的请求路径里德URL重写。HttpServletResponse接口定义了两个用于完成URL重写方法:
l String encodeURL(String url):将SessionID添加到url中,如果没有必要重写,则原样返回。比如虽然浏览器支持Cookie,但是页面未启用,则原样返回。
l String encodeRedirectURL(String url):用于对要传递给HttpServletResponse.sendRedirect方法的URL进行重写。
下面是判断URL是否需要重写的代码片断:
final Request hreq = request;
final Session session = hreq.getSessionInternal(false);
if (session == null)//session为null代表没有启动Session,所以没有必要
return (false);
if (hreq.isRequestedSessionIdFromCookie())//如果请求头中有 Cookie 头,则没有必要
return (false);
实现机制大体如下:
1) 先判断当前的WEB组件是否启用session,如果没有启用session,例如在jsp中声明<%page session="false"%>或者已经执行了session.invalidate()方法,那么直接返回参数URL;或者当前根就没有Session对象。
2) 再判断客户浏览器请求头中是否包含Cookie头字段,且含有SessionID的Cookie名(在Tomcat里为JSESSIONID)就直接返回参数URL;如果没有Cookie头或者有但没有名为JSESSIONID的Cookie时,并且当前Session存在并有效时,就在参数URL中加入SessionID信息,然后返回修改后的URL。
encodeURL、encodeRedirectURL的区别
public String encodeRedirectURL(String url) {
if (isEncodeable(toAbsolute(url))) {
return (toEncoded(url, request.getSessionInternal().getIdInternal()));
} else {
return (url);
}
}
public String encodeURL(String url) {
String absolute = toAbsolute(url);
if (isEncodeable(absolute)) {
// W3c spec clearly said
if (url.equalsIgnoreCase("")){
url = absolute;
}
return (toEncoded(url, request.getSessionInternal().getIdInternal()));
} else {
return (url);
}
}
encodeURL、encodeRedirectURL没有很大的区别,只在在处理空的URL时,不太一样罢了:encodeURL在传递的url为空时,会修改能决对路径absolute,但encodeRedirectURL不会,它返回的还是空字符串。
继续使用上一节的例子,使用URL重写来利用以前存在的session。
Telnet:
GET /myapp/SessionTest;jsessionid=89AD5BC804973DED41C06AC84893CE23 HTTP/1.1
Host:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Length: 0
Date: Thu, 22 Jul 2010 06:42:47 GMT
命令窗口发现使用的是已存在的session,而不会重新创建。
在程序里应该这样:
PrintWriter out = response.getWriter();
String urlEncode = response.encodeURL("/myapp/SessionTest?parm=xxx");
out.print("<a href="" + urlEncode + "">" + urlEncode + "</a>");
最后在页面上显示如下:
/myapp/SessionTest;jsessionid=A68EA07F0CA7072E762A5B3AB3BF7A0F?parm=xxx
由此可见,encodeURL重写URL时会有一定的规范,不会将jsessionid直接作为参数传递,即不会放在真正参数问题的后面,而是在它的前面,并在请求URL的后面,且jsessionid前还要有一个分号分隔。
利用Session真正防止表单重复提交
利用JavaScript是不能真正做到防止用户恶意的提交表单的,因为用户可以绕过浏览器。所以只能在服务器端来实现。
通过服务器端程序防止表单重复提交的基本原理如下:
1) 为每次产生的页面中的FORM表单都分配一个惟一的随机标识号,并在FORM表单的一个隐藏字段中设里这个标识号,同时在当前用户的Session域中保存这个标识号;
2) 当用户提交FORM表单时负责接收这一请求的服务器程序比较FORM表单隐藏字段中的标识号与存储在当前用户的Session域中的标识号是否相同,如果相同则处理表单数据,处理完后清除当前用户的Session域中存储的标识号。经过这样的处理后,当用户重复提交原来得到的FORM表单时,当前用户的Sesstion域中己不存在相应的表单标识号。在下列情况下,服务器程序将忽略提交的表单请求:
l 当前用户的Session中不存在表单标识号;
l 用户提交的表单数据中没有标识号字段;
l 存储在当前用户的Session域中的表单标识号与表单数据中的标识不同。
3) 浏览器只有重新向Web服务器语法包含Form表单的页面时,服务器程序才会产生另外一个随机标识号,并将这个标识号保存在Session域中和作为新返回的FORM表单中的隐藏字段值。
可见,经过这样的设计后,只有客户端重新访问包含有FORM表单的页面后,服务器端的FORM表单程序才可以接受FORM表单提交的数据,否则,服务器端的FORM表单处理程序将不接受用户使用同一个表单重复提交的数据。
表单的ID一定要唯一,这里可以采用MD5来生成,因为MD5可以根据很多不同的数据源生成摘要:
public static void main(String[] args) throws NoSuchAlgorithmException {
String sessionId = "89AD5BC804973DED41C06AC84893CE23";
String currentTime = String.valueOf(System.currentTimeMillis());
//获取MD5摘要算法实例
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(sessionId.getBytes());//设置源
md.update(currentTime.getBytes());
//生成表单ID
String formId = toHex(md.digest());
System.out.println(formId);;
}
//将字节数据转换成十六进制字符串
private static String toHex(byte buffer[]) {
StringBuffer sb = new StringBuffer(buffer.length * 2);
for (int i = 0; i < buffer.length; i++) {
//取一个字节中的高4位,并转换成十六进制
sb.append(Character.forDigit((buffer[i] & 0xF0) >> 4, 16));
//取一个字节中的低4位,并转换成十六进制
sb.append(Character.forDigit((buffer[i] & 0x0F), 16));
}
return sb.toString().toUpperCase();
}
注意,服务器在处理表单数据时可能要花很长一旦时间,我们应该在处理表单数据前就将表单ID清除掉,这样可以进一步防止在提交的过程中用户可以重复提交。当然,如果在服务器处理的过程中用户重复提交后会看不到正确的处理的结果,因为重复提交时浏览器只能显示最后一次请求的结果,所以这里最好在浏览器端结合一下JavaScript,当提交后将提交钮disable掉,这样就即完善,又真正做到了安全。
利用Session实现一次性验证码
public class CheckCodeServlet extends HttpServlet {
private int WIDTH = 60;
private int HEIGHT = 20;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
response.setContentType("image/jpeg");
//设置浏览器不要缓存此图片
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
//创建内存图像并获得其图形上下文
BufferedImage image = new BufferedImage(WIDTH, HEIGHT,
BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
//产生随机验证码
char[] rands = generateChechCode();
//产生图像
drawBackground(g);
drawRands(g, rands);
//完成
g.dispose();
//将图像输出到客户端
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(image, "JPEG", bos);
byte[] buf = bos.toByteArray();
response.setContentLength(buf.length);
//输出图像
response.getOutputStream().write(buf);
bos.close();
response.getOutputStream().close();
//将当前验证码存入到Session中
session.setAttribute("check_code", new String(rands));
}
private char[] generateChechCode() {
String chars = "0123456789abcdefghijklmnopqrstuvwxyz";
char[] rands = new char[4];
for (int i = 0; i < 4; i++) {
int rand = (int) (Math.random() * 36);
rands[i] = chars.charAt(rand);
}
return rands;
}
private void drawRands(Graphics g, char[] rands) {
g.setColor(Color.BLACK);
g.setFont(new Font(null, Font.ITALIC | Font.BOLD, 18));
//在不同的调度上输出验证码的每个字符
g.drawString("" + rands[0], 1, 17);
g.drawString("" + rands[1], 16, 17);
g.drawString("" + rands[2], 31, 18);
g.drawString("" + rands[3], 46, 16);
}
private void drawBackground(Graphics g) {
//画背景
g.setColor(new Color(0xDCDCDC));
g.fillRect(0, 0, WIDTH, HEIGHT);
//随机产生 120个干扰点
for (int i = 0; i < 120; i++) {
int x = (int) (Math.random() * WIDTH);
int y = (int) (Math.random() * HEIGHT);
int red = (int) (Math.random() * 255);
int green = (int) (Math.random() * 255);
int blue = (int) (Math.random() * 255);
g.setColor(new Color(red, green, blue));
g.drawOval(x, y, 1, 0);
}
}
}
<img src="/myapp/CheckCodeServlet">
Session持久管理
存储在HttpSession对象中的每个属性对应必须是可序列化的,即必须是实现了Serializable接口的对象。
Session持久化可以实现服务的集群。
重启服务器后,客户端的会话还存在。
Tomcat中的Session持久化管理
org.apache.catalina.session.StandardManager
org.apache.catalina.session.PersistentManager
StandardManager在Web应用程序关闭时,对内存中的所有HttpSession对象进行持久化,返它们保存到文件系统中,默认存储文件为:
workcatalina主机名应用程序名SESSIONS.ser
PersistentManager提供了比StandardManager更为灵活的Session管理功能,只要某个设备提供了实现org.apache.catalina.Store接口的驱动类,PersistentManager就可以将HttpSession对象保存到该设备。Tomcat提提供了将HttpSession对象保存到文件系统和数据库的Store接口实现类。
Tomcat默认使用StandardManager,如果要使用其他的Sesion Manger,则要在server.xml配置文件中的Context元素下增加一个名为Manager的了元素,具体可以参数相关文档。