Tomcat中实现Session小结

时间:2022-06-02 00:24:32

什么是session

对tomcat而言,session是一块在服务器开辟的内存空间,其存储结构为concurrenthashmap;

session的目的

http协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录;

session的主要目的就是为了弥补http的无状态特性。简单的说,就是服务器可以利用session存储客户端在同一个会话期间的一些操作记录;

实现机制

先看两个问题,如下:

1、服务器如何判断客户端发送过来的请求是属于同一个会话?

答:用session id区分,session id相同的即认为是同一个会话,在tomcat中session id用jsessionid表示;

2、服务器、客户端如何获取session id?session id在其之间是如何传输的呢?

答:服务器第一次接收到请求时,开辟了一块session空间(创建了session对象),同时生成一个session id,并通过响应头的set-cookie:“jsessionid=xxxxxxx”命令,向客户端发送要求设置cookie的响应;

客户端收到响应后,在本机客户端设置了一个jsessionid=xxxxxxx的cookie信息,该cookie的过期时间为浏览器会话结束;

接下来客户端每次向同一个网站发送请求时,请求头都会带上该cookie信息(包含session id);

然后,服务器通过读取请求头中的cookie信息,获取名称为jsessionid的值,得到此次请求的session id;

ps:服务器只会在客户端第一次请求响应的时候,在响应头上添加set-cookie:“jsessionid=xxxxxxx”信息,接下来在同一个会话的第二第三次响应头里,是不会添加set-cookie:“jsessionid=xxxxxxx”信息的;

而客户端是会在每次请求头的cookie中带上jsessionid信息;

举个例子:

以chrome浏览器为例,访问一个基于tomcat服务器的网站的时候,

浏览器第一次访问服务器,服务器会在响应头添加set-cookie:“jsessionid=xxxxxxx”信息,要求客户端设置cookie,如下图:

Tomcat中实现Session小结

同时我们也可以在浏览器中找到其存储的sessionid信息,如下图

Tomcat中实现Session小结

接下来,浏览器第二次、第三次...访问服务器,观察其请求头的cookie信息,可以看到jsessionid信息存储在cookie里,发送给服务器;且响应头里没有set-cookie信息,如下图:

Tomcat中实现Session小结

只要浏览器未关闭,在访问同一个站点的时候,其请求头cookie中的jsessionid都是同一个值,被服务器认为是同一个会话。

 再举个简单的例子加深印象,新建个web工程,并写一个servlet,在doget中添加如下代码,主要做如下工作

首先,从session中获取key为count的值,累加,存入session,并打印;

然后,每次从请求中获取打印cookie信息,从响应中获取打印header的set-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
/**
 * @see httpservlet#doget(httpservletrequest request, httpservletresponse response)
 */
protected void doget(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception {
  
  if(request.getsession().getattribute("count") == null){
    request.getsession().setattribute("count", 0);
    response.getwriter().write(0+"");
  }else{
    int a = integer.parseint(request.getsession().getattribute("count").tostring());
    request.getsession().setattribute("count", ++a);
    response.getwriter().write(a+"");
  }
 
  cookie[] cookies = request.getcookies();
  stringbuffer sb = new stringbuffer();
  if(cookies!=null){
    for(cookie cookie : cookies){
      sb.append(cookie.getname()+":"+cookie.getvalue()+",");
    }
    sb.deletecharat(sb.length()-1);
  }
 
  system.out.println("[第"+(++index)+"次访问]from client request, cookies:" + sb);
  system.out.println("[第"+(index)+"次访问]from server response, header-set-cookie:" + response.getheader("set-cookie"));;
}

部署到tomcat后,连续访问该servlet,观察控制台输出,如下,客户端第一次访问服务器的时候,在服务端的响应头里添加了jsessionid信息,且接下来客户端的每次访问都会带上该jsessionid:

Tomcat中实现Session小结

其实这里有一个问题,session劫持

只要用户知道jsessionid,该用户就可以获取到jsessionid对应的session内容,还是以上面这个例子为例,

我先用ie浏览器访问该站点,比如连续访问了5次,此时,session中的count值为:

Tomcat中实现Session小结

查看该会话的session id,为6a541281a79b24bc290ed3270cf15e32

Tomcat中实现Session小结

接下来打开chrome控制台,将ie浏览器获取过来的jsessionid信息(“6a541281a79b24bc290ed3270cf15e32”)写入到cookie中,如下

Tomcat中实现Session小结

接着删除其中的一个,只留下jsessionid为“6a541281a79b24bc290ed3270cf15e32”的cookie;

Tomcat中实现Session小结

刷新页面,发现我们从session获取的count值已经变成6了,说明此次chrome浏览器的请求劫持了ie浏览器会话中的session,

Tomcat中实现Session小结

tomcat中的session实现

tomcat中一个会话对应一个session,其实现类是standardsession,查看源码,可以找到一个attributes成员属性,即存储session的数据结构,为concurrenthashmap,支持高并发的hashmap实现;

?
1
2
3
4
/**
 * the collection of user data attributes associated with this session.
 */
protected map<string, object> attributes = new concurrenthashmap<string, object>();

那么,tomcat中多个会话对应的session是由谁来维护的呢?managerbase类,查看其代码,可以发现其有一个sessions成员属性,存储着各个会话的session信息:

?
1
2
3
4
5
/**
 * the set of currently active sessions for this manager, keyed by
 * session identifier.
 */
protected map<string, session> sessions = new concurrenthashmap<string, session>();

接下来,看一下几个重要的方法,

服务器查找session对象的方法

客户端每次的请求,tomcat都会在hashmap中查找对应的key为jsessionid的session对象是否存在,可以查看request的dogetsession方法源码,如下源码:

?
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
protected session dogetsession(boolean create) {
 
    // there cannot be a session if no context has been assigned yet
    context context = getcontext();
    if (context == null) {
      return (null);
    }
 
    // return the current session if it exists and is valid
    if ((session != null) && !session.isvalid()) {
      session = null;
    }
    if (session != null) {
      return (session);
    }
 
    // return the requested session if it exists and is valid
    manager manager = context.getmanager();
    if (manager == null) {
      return null;    // sessions are not supported
    }
    if (requestedsessionid != null) {
      try {
        session = manager.findsession(requestedsessionid);
      } catch (ioexception e) {
        session = null;
      }
      if ((session != null) && !session.isvalid()) {
        session = null;
      }
      if (session != null) {
        session.access();
        return (session);
      }
    }
 
    // create a new session if requested and the response is not committed
    if (!create) {
      return (null);
    }
    if ((context != null) && (response != null) &&
      context.getservletcontext().geteffectivesessiontrackingmodes().
          contains(sessiontrackingmode.cookie) &&
      response.getresponse().iscommitted()) {
      throw new illegalstateexception
       (sm.getstring("coyoterequest.sessioncreatecommitted"));
    }
 
    // re-use session ids provided by the client in very limited
    // circumstances.
    string sessionid = getrequestedsessionid();
    if (requestedsessionssl) {
      // if the session id has been obtained from the ssl handshake then
      // use it.
    } else if (("/".equals(context.getsessioncookiepath())
        && isrequestedsessionidfromcookie())) {
      /* this is the common(ish) use case: using the same session id with
       * multiple web applications on the same host. typically this is
       * used by portlet implementations. it only works if sessions are
       * tracked via cookies. the cookie must have a path of "/" else it
       * won't be provided to for requests to all web applications.
       *
       * any session id provided by the client should be for a session
       * that already exists somewhere on the host. check if the context
       * is configured for this to be confirmed.
       */
      if (context.getvalidateclientprovidednewsessionid()) {
        boolean found = false;
        for (container container : gethost().findchildren()) {
          manager m = ((context) container).getmanager();
          if (m != null) {
            try {
              if (m.findsession(sessionid) != null) {
                found = true;
                break;
              }
            } catch (ioexception e) {
              // ignore. problems with this manager will be
              // handled elsewhere.
            }
          }
        }
        if (!found) {
          sessionid = null;
        }
        sessionid = getrequestedsessionid();
      }
    } else {
      sessionid = null;
    }
    session = manager.createsession(sessionid);
 
    // creating a new session cookie based on that session
    if ((session != null) && (getcontext() != null)
        && getcontext().getservletcontext().
            geteffectivesessiontrackingmodes().contains(
                sessiontrackingmode.cookie)) {
      cookie cookie =
        applicationsessioncookieconfig.createsessioncookie(
            context, session.getidinternal(), issecure());
 
      response.addsessioncookieinternal(cookie);
    }
 
    if (session == null) {
      return null;
    }
 
    session.access();
    return session;
  }

先看dogetsession方法中的如下代码,这个一般是第一次访问的情况,即创建session对象,session的创建是调用了managerbase的createsession方法来实现的; 另外,注意response.addsessioncookieinternal方法,该方法的功能就是上面提到的往响应头写入“set-cookie”信息;最后,还要调用session.access方法记录下该session的最后访问时间,因为session是可以设置过期时间的;

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
session = manager.createsession(sessionid);
 
   // creating a new session cookie based on that session
   if ((session != null) && (getcontext() != null)
       && getcontext().getservletcontext().
           geteffectivesessiontrackingmodes().contains(
               sessiontrackingmode.cookie)) {
     cookie cookie =
       applicationsessioncookieconfig.createsessioncookie(
           context, session.getidinternal(), issecure());
 
     response.addsessioncookieinternal(cookie);
   }
 
   if (session == null) {
     return null;
   }
 
   session.access();
   return session;

再看dogetsession方法中的如下代码,这个一般是第二次以后访问的情况,通过managerbase的findsession方法查找session,其实就是利用map的key从concurrenthashmap中拿取对应的value,这里的key即requestedsessionid,也即jsessionid,同时还要调用session.access方法,记录下该session的最后访问时间;

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (requestedsessionid != null) {
  try {
    session = manager.findsession(requestedsessionid);
  } catch (ioexception e) {
    session = null;
  }
  if ((session != null) && !session.isvalid()) {
    session = null;
  }
  if (session != null) {
    session.access();
    return (session);
  }
}

在session对象中查找和设置key-value的方法

这个我们一般调用getattribute/setattribute方法:

getattribute方法很简单,就是根据key从map中获取value;

setattribute方法稍微复杂点,除了设置key-value外,如果添加了一些事件监听(httpsessionattributelistener)的话,还要通知执行,如beforesessionattributereplaced, aftersessionattributereplaced, beforesessionattributeadded、 aftersessionattributeadded。。。

session存在的问题

  • 安全性,session劫持,这个前面已经举过例子了;
  • 增加服务器压力,因为session是直接存储在服务器的内存中的;
  • 如果存在多台服务器的话,还存在session同步问题,当然如果只有一台tomcat服务器的话,也就没有session同步的事情了,然而现在一般的应用都会用到多台tomcat服务器,通过负载均衡,同一个会话有可能会被分配到不同的tomcat服务器,因此很可能出现session不一致问题;解决session同步问题,实际上主要是保证能够抽离出一块共享空间存放session信息,且这块空间不同的tomcat服务器都可以访问到;一般这块共享的空间可以是数据库,或者某台服务器的内存空间,甚至硬盘空间,或者客户端的cookie也是可以的;

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:http://www.cnblogs.com/chenpi/p/5434537.html