会话Cookie及session的关系(Cookie & Session)

会话Cookie及session的关系(Cookie & Session)

在通常的使用中,我们只知道session信息是存放在服务器端,而cookie是存放在客户端。但服务器如何使用session和客户端之间进行通信,以及jsessionId是怎么回事,这并没有一个完整和正确的认识,因此这里将这类信息汇总。

session中的jsessionId是在session创建好之后,发送给客户端。然后在每一次请求中,客户端即会将这个信息传递给服务器端,服务器端使用这个信息来维护和客户端之间的会话通信,在浏览器关闭之后,这个session就消失了。
而对于普通的cookie来说,它是有着一定的时间存放在客户端的机器中的。当下次打开浏览器时,这个cookie就会被读取,同时在请求时发放到服务器端。在关闭浏览器,这个cookie仍然存在的,并没有消失。
那么,这两者之间有没有联系呢,这就要看官方对于Cookie的不同分类,其实就对应着session Cookie和psersistent Cookie的描述了。如下所示:

Session Cookie
A user's session cookie[15] (also known as an in-memory cookie or transient cookie) for a website exists in temporary memory only while the user is reading and navigating the website. When an expiry date or validity interval is not set at cookie creation time, a session cookie is created. Web browsers normally delete session cookies when the user closes the browser.

Persistent cookie
A persistent cookie[15] will outlast user sessions. If a persistent cookie has its Max-Age set to 1 year (for example), then, during that year, the initial value set in that cookie would be sent back to the server every time the user visited the server. This could be used to record a vital piece of information such as how the user initially came to this website. For this reason, persistent cookies are also called tracking cookies.

从上面的描述中看,所谓的session Cookie,就是我们所说的session,其实就是一个没有设置过期时间的cookie信息。而普通的cookie,即我们通常所说的cookie,就是设置了过期时间(有效时间,通常大于一个特定的值,如一周)的cookie。

那么,我们可以这样认为。session就是服务器端,通过使用session Cookie来维系客户端和服务器的关系,而所谓的存储就是在服务器端针对这个session值(如jsessionId值)作了一个内部的增强,可以围绕着这个session Cookie创建一个单独的数据存储器(如map),然后来实现我们所谓的session数据存储呢。既然这样,在一些特定的场景(如分布式环境),是否可以按照这种设计思路设计另外的session存储呢,这也是可以的。

 

接下来,我们可测试一下这种思路,是否正确。有两个很简单的servlet,一个为写cookie,另一个为读cookie。如下所示:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
//writeServlet
//add memory cookie
        Cookie cookie = new Cookie("memoryCookie", "12345678");
        cookie.setMaxAge(-1);
        resp.addCookie(cookie);
 
        //add normal cookie
        cookie = new Cookie("persistentCookie", "abcdefg");
        cookie.setMaxAge(300);//one minute
        resp.addCookie(cookie);
 
//readServlet
Cookie[] cookies = req.getCookies();
        for(Cookie cookie : cookies) {
            System.out.println(
                    cookie.getPath() + "->" + cookie.getName() + "->" + cookie.getValue() + "->" + cookie.getMaxAge());
        }

1    未访问write,直接访问read时

开启F12,查看浏览器端的请求信息,如下所示:

只上传了一个jsession信息(这里是因为我修改了sessionName的原因)。

而在后台,打印结果如下所示:

1
null->tomcat7utf8_session->F5B543F5D5FD4A9B78CA18D96D4CE995->-1

即只有jsessionId信息。

2    访问write,再访问read

开启F12,我们查看一下访问write之后 的cookie情况,如下所示:

以上图为响应头信息。

可以看出,在响应返回时,服务器返回了2个cookie,而key为memoryCookie的信息并没有过期信息.

那我们再访问一下write,相应信息如下所示.
这是请求头信息:

而相应的cookie信息也同样如下所示:

在后台,打印的cookie信息如下所示:

1
2
3
null->tomcat7utf8_session->F5B543F5D5FD4A9B78CA18D96D4CE995->-1
null->memoryCookie->12345678->-1
null->persistentCookie->abcdefg->-1

这里显示的maxAge是-1,这是因为客户端本身就没传递相应的maxAge,所以未获取到。并且这个信息从获取来说,并没有实际性的意义。

3 关闭浏览器,再访问read

我们关闭一次浏览器,再看一下相应的数据信息,如下所示:

后台打印信息如下:
null->persistentCookie->abcdefg->-1
null->tomcat7utf8_session->6B6E27A4F6B71853CC2D2C58BEE83238->-1

即可以看出,在重新打开浏览器(并且之前写的persistentCookie并没有过期)时,在请求后台读取时,只会发送有效的信息。而之前的内存cookie已随着浏览器关闭消失了,所以这里才只会有2个信息。
需要注意的,这里的关闭浏览器,是把所有的标签界面均关闭。在IE中,标签而中是共享Cookie的,所以只关闭一个标签是没有作用的。

4 如何创建的Session

这里,我们来查看一下在后台是如何创建session,并同时产生相应的cookie信息的。我们可以这样认为,当我们请求后台session时,如果没有相应的cookie信息,后台会产生一个相应的cookie,并发到客户端(即调用response.addCookie方法)。来看一下后台的实现是否如我们所说呢(这里以tomcat实现为参考),相应的代码在类Request.doGetSession中,如下所示:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    protected Session doGetSession(boolean create) {
。。。。。。
 
//这里尝试获取session或者创建一个新的session
        // Attempt to reuse session id if one was submitted in a cookie
        // Do not reuse the session id if it is from a URL, to prevent possible
        // phishing attacks
        if (connector.getEmptySessionPath()
                && isRequestedSessionIdFromCookie()) {
//这里如果客户端显示传递了一个sessionId,这里直接从session池中获取,如果获取失败则直接返回null,被认为是客户端数据有问题或者是其它攻击
            session = manager.createSession(getRequestedSessionId());
        } else {
            session = manager.createSession(null);
        }
 
        //这里就是创建一个新的cookie,并放置在response头中
        // Creating a new session cookie based on that session
        if ((session != null) && (getContext() != null)
               && getContext().getCookies()) {
            String scName = context.getSessionCookieName();
            if (scName == null) {
                scName = Globals.SESSION_COOKIE_NAME;
            }
//name即jsessionId或配置的sessionName value即我们所说的sessionId
            Cookie cookie = new Cookie(scName, session.getIdInternal());
            configureSessionCookie(cookie);
            response.addSessionCookieInternal(cookie, context.getUseHttpOnly());
        }
。。。。。。
    }

可以看出,这和我们的大致思路是一样的。

5 Cookie中的HttpOnly

The HttpOnly attribute is supported by most modern browsers.[19][20] On a supported browser, an HttpOnly session cookie will be used only when transmitting HTTP (or HTTPS) requests, thus restricting access from other, non-HTTP APIs (such as JavaScript). This restriction mitigates but does not eliminate the threat of session cookie theft via cross-site scripting (XSS).[21] This feature applies only to session-management cookies, and not other browser cookies.

现在的浏览器已经支持在cookie中增加一个新的httpOnly选项,这个选项就表示这个cookie只用于在浏览器请求中使用,而不能被其它方式读取和设置。比如,httpOnly式的cookie就不能被js读取到(而常规的cookie是可以读取的)。

在最新的J2EE6.0即Servlet3.0中,已经支持直接在Cookie上设置httpOnly选项了。而在之间的版本,如tomcat,则可以在context.xml中设置是否支持httpOnly选项。针对像session Cookie这种会话式cookie,建议是使用httpOnly的,并且默认也是直接使用此标记的。

分布式cookie-session的实现(spring-session)

本文使用的spring-session版本为 1.0.0,地址为: https://github.com/spring-projects/spring-session

1     session存储策略

存储,即在后台使用session的setAttribute,getAttribute等方法时,这些内部存放的数据最终存储至什么位置。比如在默认的tomcat实现中,相应的数据即存储在内存中,并在停止之后会序列化至磁盘中。
可以使用内存存储和第三方存储,如redis。对于集群式的session存储,那么肯定会使用第三方存储的实现。

在spring-session中,对session存储单独作了一个定义,但定义上基本保证与http session一致,主要的目的在于它可以支持在非http的环境中模拟使用。因此不直接使用http session接口。
先看定义:

1
2
3
4
5
6
7
8
public interface Session {
     /** 获取惟一的id,可以理解为即将每个session当作一个实体对象 */
    String getId();
    T getAttribute(String attributeName);
    Set getAttributeNames();
    void setAttribute(String attributeName, Object attributeValue);
    void removeAttribute(String attributeName);
}

从这个定义来看,如果要使用httpSession,如果实现了这个session接口,那么实现上就可以全部实现httpSession对于存储的要求。对于非httpSession场景,使用这个也可以达到session存储的目的。
当然,为了保证在会话场景中会使用到失效,最后访问时间,最大不活跃时间的目的,spring-session也有一个继承于session接口的expiringSession接口。

1.0    id主键的生成

对于id,即理解为session实体对象惟一键,可以采用任意的一种惟一key生成策略。比如,使用uuid来生成惟一键。同时,也可以将这个id认为就是在http中sessionCookie的值

 

1.1     map存储

如果是map来存储,那么 set,get,remove就可以直接使用map的相应实现即可。在spring-session实现中MapSession,即采用内部的一个hashmap来进行存储。

1.2     redis存储

在sprng-session中,将属性的存储和整体的存储分开,使用专门的仓库来处理session实体的处理。对于从领域模型的概念来说,set,get,removeAttribute只是对属性的处理,处理的是一个内部状态的变化,对于整体的实体来说,并没有整体上的处理。具体到实现,在redis的存储实现中,spring-session内部使用了一个 mapSession来存储相应的属性信息。
在一个实体被创建之后,相应的属性信息被全部设置至mapSession中,然后接下来的属性访问均通过mapSession来处理。为了最终存储相应的数据值,redis实现使用了一个额外的deltaMap来存放变化了的数据,并在重新保存时,一次性地将所有属性put至redis中。
从这个实现来看,spring-session的实现并不是实时地与redis进行交互,这种实现方式在解决同一session设置冲突属性时可能存在问题。因此存在这种场景的,需要重新实现相应的逻辑。

2     session仓库策略

之所以将存储策略和仓库策略分开,也是spring-session的一个设计思想。spring将session设计为一个实体,因此将实体对象的操作和属性的操作进行拆分。在前面的存储策略已经描述了属性的操作,因此这里的仓库策略主要处理实体的创建,获取,更新,删除等,即CRUD。如下的定义所示:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public interface SessionRepositoryextends Session> {
 
     /** 新建 */
    S createSession();
 
     /** 保存或更新 */
    void save(S session);
 
     /** 获取 */
    S getSession(String id);
 
     /** 删除 */
    void delete(String id);
}

2.1     本地仓库
如果是本地仓库,那么可以使用一个本地全局的globalMap,然后使用sessionId作为key,mapSession作为value来实现即可。

2.2     redis仓库

如果是resi仓库,那么在新建时,只是在本地创建一个redisSession对象,然后在redis存储中使用一个 hash结构来进行存储。即>的结构。其中I即表示sessionId,表示具体的redisSession对象。其中getSession,delete都是针对I的操作,而save则在在指定I的情况下,直接保持即可。

为了在一定程度上进行优化,以及解决在一定程序上的覆盖问题,在redis实现时。spring-session只对修改过的属性作存储,即当使用setAttribute处理属性时,spring-session使用额外的delta来保存修改过的属性,最后进行save时,只操作这部分数据即可。并且redis也支持调用hSet来保存或更新修改过的数据。

3     session传输策略

3.1     cookie传输

常规的实现也是使用cookie传输,比如tomcat,jetty等。在这种情况下,后端在创建session时,如果这是新的session,会自动调用原生response.addCookie以创建一个新的cookie信息。为保证安全,采用httpOnly进行创建。在发送给客户端之后的每次交互,浏览器会自动将这些信息传输至服务器端。然后服务器端直接从cookie中获取到sessionId,再进一步操作即可。
对于这种实现,应用程序开发不需要作任何的调整,浏览器会自动处理cookie的传输。

3.2     header传输

header传输,即在创建新session时,往response header中使用addHeader添加一个新的响应头,如session头,headerValue中存放相应的sessionId值。
在客户端实现中,客户端代码需要手动地将响应值进行记录,并在请求时加入到请求头当中。并且需要监听响应头值的变化,比如sessionId值的变化处理。相比cookie传输来说,这种实现对于开发更复杂一点。

4     http协议兼容

4.1     为保证http协议兼容,在session信息准备好之后,其必须在响应头之后进行处理

不管是使用cookie传输还是header传输,在响应时都需要保证在响应body输出时,相应的头信息都应该先被处理。(cookie也是一种特殊的header)。如果像以下的代码,就不能保证session响应被正确地处理

1
2
3
4
X implements Filter {
     filterChain.doFilter(request,response)
     // do session logic
}

在这种实现中,如果在应用代码中直接使用response的outputStream进行输出时,这里的过滤器实现中的do Session操作就不能实现相应的逻辑。因为http协议中的消息机制已经不再允许再输出body之后输出header信息了。

在spring-session的实现中,我们只需要保证在调用response处理响应body时,能够处理session逻辑即可。那么可以使用一种hack的技术,针对在response输出时,判定是否在处理body或应该结束响应时,加入特定的hack以处理我们的do session操作。因此提供了 OnCommittedResponseWrapper 以便在特定的时机进行处理,在具体的 onResponseCommitted中,处理session的delete响应和save等即可。

4.2     为支持多浏览器访问,因此在特定的路径信息处理时,应当自动地处理相应的browser参数

关于多browser支持,可以先看看此篇文章。
http://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession-multi

在多browser支持时,主要即在请求参数中增加相应的浏览器识别参数_s,那么在需要进行界面跳转或其它地址处理时,后台应该能够自动地处理相应的参数信息。在spring-session中,为了对应用程序透明,通过重写response的encodeURL和encodeRedirectURL时,增加对相应_s的处理即达到相应的处理透明即可。

 

posted @ 2021-12-11 23:10  CharyGao  阅读(20)  评论(0)    收藏  举报