HTTP 协议 ( 超文本传输协议 ) 是无状态的,不能保存客户端与服务器之间通讯 ( 交互 ) 的信息。
打个比方,拿最常见的登录来说,现在好多网站的操作都需要用户登录,假如在 a 操作时,用户成功登录系统,再进行 b 操作时,由于 HTTP 协议是无状态的,
用户之前登录的信息并没有被记录下来。那么,用户需要再次登录系统才能继续操作,以此类推,每个操作都需要登录一次系统,这是非常可怕的事情。
cookie 和 session 技术的诞生,都是为解决 HTTP 无状态协议所带来的系列问题。它们可以维护用户与服务器间的会话状态。
Cookie
cookie 是保存在客户端的文本文件。在这个文本文件中以键值对 ( key - value ) 的形式保存了一些与用户相关的信息,如常见的有账号信息等。
对Cookie操作的封装
package fan.commons.web;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Cookie 操作
* @Author fancy
* @Mail fancore@126.com
* @Date 2013-09-29
*/
public class Cookies {
private final HttpServletRequest request;
private final HttpServletResponse response;
public Cookies(final HttpServletRequest request, final HttpServletResponse response){
this.request = request;
this.response = response;
}
/**
* 添加Cookie
* @param cookie 实例
*/
public void add(Cookie cookie){
response.addCookie(cookie);
}
/**
* 添加Cookie
* @param name 名称
* @param value 值
*/
public void add(String name, String value){
add(name, value, -1);
}
/**
* 添加Cookie
* @param name 名称
* @param value 值
* @param secondTimeout 设定多少秒后过期。若为负数, 则浏览器关闭后Cookie失效。
*/
public void add(String name, String value, int secondTimeout){
Cookie cookie = new Cookie(name, value);
cookie.setMaxAge(secondTimeout);
cookie.setPath("/");
response.addCookie(cookie);
}
/**
* 根据名称获取Cookie对象。如果不存在则返回null。
* @param name 名称
* @return 返回Cookie对象
*/
public Cookie getCookie(String name){
Cookie[] cookies = request.getCookies();
if(cookies != null){
for(Cookie cookie : cookies){
if(cookie.getName().equals(name)){
return cookie;
}
}
}
return null;
}
/**
* 根据名称获取Cookie对象的值。如果不存在则返回null。
* @param name 名称
* @return 返回该Cookie对象的值
*/
public String getValue(String name){
Cookie cookie = getCookie(name);
return cookie == null ? null : cookie.getValue();
}
/**
* 删除Cookie
* @param name 名称
*/
public void delete(String name){
delete(new Cookie(name, null));
}
/**
* 删除Cookie
* @param cookie 需要删除的对象
*/
public void delete(Cookie cookie){
cookie.setMaxAge(0); //0表示立即删除
cookie.setPath("/");
response.addCookie(cookie);
}
/**
* 清空Cookie
*/
public void clear(){
Cookie[] cookies = request.getCookies();
if(cookies != null){
for(Cookie cookie : cookies){
delete(cookie);
}
}
}
}
public class CookieServlet extends HttpServlet {
protected void service(HttpServletRequest request, HttpServletResponse response) {
Cookies cookie = new Cookies(request, response);
if(cookie.getValue("username") == null){
cookie.add("username", "fancy");
}
//从客户端传递过来的 cookie 中获取信息
System.out.println(cookie.getValue("username"));
}
}
当客户端发起 http 请求时,相关的 cookie 信息将被放在 http 请求头 ( Request Headers ) 中,并发送至服务器端。
当服务器向客户端写回 ( 新建、更新、删除 ) cookie 时,这些 cookie 信息将被放到 http 响应头 ( Response Headers ) 中,并发回给客户端浏览器。
客户端浏览器接收并解析服务器发回的 cookie 信息,然后将它保存到文件或保存到内存当中。
Session
Session 是保存在服务器端,用于存放一些与用户相关的信息,如登录状态等。
Session 的创建时机
首先,Session 不是在客户机首次访问服务器时被创建的。
Session 的创建时机是当服务器端程序首次执行 HttpServletRequest.getSession() 或 HttpServletRequest.getSession(true) 时被创建的。
HttpServletRequest.getSession() 和 HttpServletRequest.getSession(true) 是等效的,
调这两个方法的时候,若 Session 已经存在,则直接返回使用。若不存在,则创建一个 Session 对象并返回使用。
HttpServletRequest.getSession(false) ,调用该方法的时候,若 Session 已经存在,则返回使用。若不存在,则返回 null。
下面借助 Session 监听器来监听 Session 的创建时机:
package fan.core.listener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class SessionListener implements HttpSessionListener {
public SessionListener(){
System.out.println("*******************************");
System.out.println("Startup SessionListener");
System.out.println("*******************************");
}
public void sessionCreated(HttpSessionEvent e) {
System.out.println("*******************************");
System.out.println("Create Session");
System.out.println("*******************************");
}
public void sessionDestroyed(HttpSessionEvent e) {
System.out.println("*******************************");
System.out.println("Destroy Session");
System.out.println("*******************************");
}
}
public class TestServlet extends HttpServlet {
protected void service(HttpServletRequest request, HttpServletResponse response) {
System.out.println("*******************************");
System.out.println("TestServlet");
System.out.println("*******************************");
}
}
<listener>
<listener-class>fan.core.listener.SessionListener</listener-class>
</listener>
<servlet>
......
</servlet>
访问 TestServlet,发现控制台并没有打印出 "Create Session"。
修改 TestServlet 代码如下:
public class TestServlet extends HttpServlet {
protected void service(HttpServletRequest request, HttpServletResponse response) {
request.getSession(true);
}
}
再次访问该 TestServlet,控制台打印出 "Create Session"。
重启服务器,在浏览器中首次访问服务器中的任一 JSP 资源,发现控制台也打印出 "Create Session"。这是怎么回事呢?
是这样的,JSP 引擎在将 JSP 文件编译成类 Servlet 类文件 ( 该文件可以在
%Tomcat%/work/%工程目录% 下找到 ) 时,还是打开 index_jsp.java 源码来对照着说吧:
可以看到源码中有一行 session = pageContext.getSession()。
这里的 pageContext 是 org.apache.jasper.runtime.PageContextImpl 的一个实例 ( 这个验证非常简单,有兴趣的可以动手试试 ),
打开 PageContextImpl 源码看一眼:
可以看到,JSP 默认的实现方式中去调了 HttpServletRequest.getSession() 方法,因此,当第一次访问某个 JSP 文件时,
如果服务器之前还没有为该用户创建过 Session,则会创建一个,而不管 Session 会不会被用到。
这里可以通过 <%@page session="false"%> 来关闭 JSP 中默认创建 Session 的行为。
当在 jsp 文件中设置 <%@page session="false"%>,访问这个 jsp 文件,如 index.jsp,那么再打开 index_jsp.java 文件,
发现 session = pageContext.getSession() 这一行不见了,控制台也没有打印出 "Create Session" 的字样。
JSESSIONID ( session cookie / sessionid )
同一时间里,服务器端存在许多 Session 对象。假如一个 Session 对象代表一个用户,那么,当某客户机请求服务器时,服务器如何知道哪个 Session 属于 哪个用户呢?
这是通过唯一标识 id 来识别的。服务器首先检查客户端请求里是否包含 session 标识,如果已经包含,说明服务器之前已经为该用户创建了 session,接着服务器根据这个 session 标识去检索对应的 session 对象。如果检索不到 (可能因为 session 过期已被销毁),则可能创建一个新的 session 对象 (HttpServletRequest.getSession(false) 的时候不创建)。如果客户端请求里不包含 session 标识,则创建一个新的 session 对象。服务器在创建 session 对象的时候会为每个 session 对象关联一个唯一的标识 sessionid,并将该标识的值以 cookie 的方式写回给客户端浏览器,保存在客户端浏览器内存中。当客户端再次向服务器发起请求时,根据 HTTP 协议,cookie 会被携带到服务器端,而 cookie 里面保存了一个能唯一标识用户自己身份的 sessionid,服务器端根据这个 id 就能找得到属于用户自己的 session 了。
从上面描述可以知道,session 是基于 cookie 机制来实现的,因此,上面所提到的 session 标识、sessionid 通常被称为 session cookie。我们平常时候所说的 cookie 被称为 persistent cookie。session cookie 是保存在客户端浏览器的内存当中,仅在本次会话中有效,当浏览器关闭之后,session cookie 也就失效并随之消失了。
cookie 是通过键值对 ( Key-Value ) 的形式来保存文本信息的,在 Java Web 运用中,session cookie 的键的名称为 JSESSIONID。看到 JSESSIONID,相信很多人都有一种亲切感,一般情况下,通常我们是看不到 JSESSIONID 的,因为它保存在 cookie 中,每次随请求默默的发送至服务器端。
修改 SessionListener 的 sessionCreated 方法:
public void sessionCreated(HttpSessionEvent e) {
System.out.println("*******************************");
System.out.println("Create Session, id = " + e.getSession().getId());
System.out.println("*******************************");
}
public class TestServlet extends HttpServlet {
protected void service(HttpServletRequest request, HttpServletResponse response) {
request.getSession(true);
}
}
访问 TestServlet,结果如图:
public class TestServlet extends HttpServlet {
protected void service(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession(true);
System.out.println("[ Session ] " + session.getId());
Cookies cookies = new Cookies(request, response);
System.out.println("[ Cookies ] " + cookies.getValue("JSESSIONID"));
}
}
据说,在客户端禁用 cookie 的环境下,服务器会通过 URL 重写的方式,将 JSESSIONID 作为参数传递到服务器端。
本人不才,用了好些个浏览器禁用 cookie 几经折腾,并没有发现目标 URL 被重写,也没有发现请求参数中含有 JSESSIONID。什么情况?
Session 销毁的时机
当 session 有效期已过或服务器端程序调用了 HttpSession.invalidate(),则 session 对象被销毁,session 的内存空间也随之被 GC 收回。
客户端关闭浏览器时,存储在服务器端的 session 并不会被立刻销毁,直到 session 有效期过了,服务器才来销毁 session 对象。
Session 与浏览器的关系
根据上面对 JSESSIONID 的阐述,客户端浏览器每次请求服务器时,都会将能唯一识别用户 session 的 JSESSIONID 携带至服务器,以作为用户识别的身份凭据。
另外也知道,JSESSIONID 是保存在客户端浏览器的 cookie 文件中,存储在浏览器内存当中,其生命周期与浏览器相同。当多个浏览器请求同一台服务器时,在首次请求中,服务器发现这些浏览器都没有携带能标识用户 Session 的 JSESSIONID,因此,服务器分别为这些浏览器创建一个 Session 对象,并通过 HTTP 的响应头消息将 Session 的 id 以 cookie 的方式写回给客户端浏览器。因此,不同的浏览器所使用的 session 是不一样的 ( 这里只针对一般的情况 )。