关于cookie和session最全解释

                                                                              cookie和session
1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
2. 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。
所以,总结一下:
Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

                                                                                   cookie机制

当用户第一次访问并登陆一个网站的时候,cookie的设置以及发送会经历以下4个步骤:

客户端发送一个请求到服务器 --》 服务器发送一个HttpResponse响应到客户端,其中包含Set-Cookie的头部 --》 客户端保存cookie,

之后向服务器发送请求时,HttpRequest请求中会包含一个Cookie的头部 --》服务器返回响应数据

image

为了探究这个过程,写了代码进行测试,如下:

我在doGet方法中,new了一个Cookie对象并将其加入到了HttpResponse对象中

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        Cookie cookie = new Cookie("mcrwayfun",System.currentTimeMillis()+"");
        // 设置生命周期为MAX_VALUE
        cookie.setMaxAge(Integer.MAX_VALUE);
        resp.addCookie(cookie);
    }

浏览器输入地址进行访问,结果如图所示:

image

可见Response Headers中包含Set-Cookie头部,而Request Headers中包含了Cookie头部。name和value正是上述设置的。

4. cookie属性项

属性项 属性项介绍
NAME=VALUE 键值对,可以设置要保存的 Key/Value,注意这里的 NAME 不能和其他属性项的名字一样
Expires 过期时间,在设置的某个时间点后该 Cookie 就会失效
Domain 生成该 Cookie 的域名,如 domain="www.baidu.com"
Path 该 Cookie 是在当前的哪个路径下生成的,如 path=/wp-admin/
Secure 如果设置了这个属性,那么只会在 SSH 连接时才会回传该 Cookie

Expires

该属性用来设置Cookie的有效期。Cookie中的maxAge用来表示该属性,单位为秒。Cookie中通过getMaxAge()和setMaxAge(int maxAge)来读写该属性。maxAge有3种值,分别为正数,负数和0。

如果maxAge属性为正数,则表示该Cookie会在maxAge秒之后自动失效。浏览器会将maxAge为正数的Cookie持久化,即写到对应的Cookie文件中(每个浏览器存储的位置不一致)。无论客户关闭了浏览器还是电脑,只要还在maxAge秒之前,登录网站时该Cookie仍然有效。下面代码中的Cookie信息将永远有效。

        Cookie cookie = new Cookie("mcrwayfun",System.currentTimeMillis()+"");
        // 设置生命周期为MAX_VALUE,永久有效
        cookie.setMaxAge(Integer.MAX_VALUE);
        resp.addCookie(cookie);

当maxAge属性为负数,则表示该Cookie只是一个临时Cookie,不会被持久化,仅在本浏览器窗口或者本窗口打开的子窗口中有效,关闭浏览器后该Cookie立即失效。

        Cookie cookie = new Cookie("mcrwayfun",System.currentTimeMillis()+"");
        // MaxAge为负数,是一个临时Cookie,不会持久化
        cookie.setMaxAge(-1);
        resp.addCookie(cookie);

可以看到,当MaxAge为-1时,时间已经过期

image

当maxAge为0时,表示立即删除Cookie

        Cookie[] cookies = req.getCookies();
        Cookie cookie = null;

        // get Cookie
        for (Cookie ck : cookies) {

            if ("mcrwayfun".equals(ck.getName())) {
                cookie = ck;
                break;
            }
        }

        if (null != cookie) {
            // 删除一个cookie
            cookie.setMaxAge(0);
            resp.addCookie(cookie);
        }

那么maxAge设置为负值和0到底有什么区别呢?

maxAge设置为0表示立即删除该Cookie,如果在debug的模式下,执行上述方法,可以看见cookie立即被删除了。

image

maxAge设置为负数,能看到Expires属性改变了,但Cookie仍然会存在一段时间直到关闭浏览器或者重新打开浏览器。

image

修改或者删除Cookie

HttpServletResponse提供的Cookie操作只有一个addCookie(Cookie cookie),所以想要修改Cookie只能使用一个同名的Cookie来覆盖原先的Cookie。如果要删除某个Cookie,则只需要新建一个同名的Cookie,并将maxAge设置为0,并覆盖原来的Cookie即可。

新建的Cookie,除了value、maxAge之外的属性,比如name、path、domain都必须与原来的一致才能达到修改或者删除的效果。否则,浏览器将视为两个不同的Cookie不予覆盖。

值得注意的是,从客户端读取Cookie时,包括maxAge在内的其他属性都是不可读的,也不会被提交。浏览器提交Cookie时只会提交name和value属性,maxAge属性只被浏览器用来判断Cookie是否过期,而不能用服务端来判断。

image

我们无法在服务端通过cookie.getMaxAge()来判断该cookie是否过期,maxAge只是一个只读属性,值永远为-1。当cookie过期时,浏览器在与后台交互时会自动筛选过期cookie,过期了的cookie就不会被携带了。

Cookie的域名

Cookie是不可以跨域名的,隐私安全机制禁止网站非法获取其他网站的Cookie。

正常情况下,同一个一级域名下的两个二级域名也不能交互使用Cookie,比如test1.mcrwayfun.com和test2.mcrwayfun.com,因为二者的域名不完全相同。如果想要mcrwayfun.com名下的二级域名都可以使用该Cookie,需要设置Cookie的domain参数为.mcrwayfun.com,这样使用test1.mcrwayfun.com和test2.mcrwayfun.com就能访问同一个cookie

一级域名又称为顶级域名,一般由字符串+后缀组成。熟悉的一级域名有baidu.com,qq.com。com,cn,net等均是常见的后缀。
二级域名是在一级域名下衍生的,比如有个一级域名为mcrfun.com,则blog.mcrfun.comwww.mcrfun.com均是其衍生出来的二级域名。

Cookie的路径

path属性决定允许访问Cookie的路径。比如,设置为"/"表示允许所有路径都可以使用Cookie

                                                            不要混淆 session 和 session 实现。

本来 session 是一个抽象概念,开发者为了实现中断和继续等操作,将 user agent 和 server 之间一对一的交互,抽象为“会话”,进而衍生出“会话状态”,也就是 session 的概念。

而 cookie 是一个实际存在的东西,http 协议中定义在 header 中的字段。可以认为是 session 的一种后端无状态实现。

而我们今天常说的 “session”,是为了绕开 cookie 的各种限制,通常借助 cookie 本身和后端存储实现的,一种更高级的会话状态实现。

所以 cookie 和 session,你可以认为是同一层次的概念,也可以认为是不同层次的概念。具体到实现,session 因为 session id 的存在,通常要借助 cookie 实现,但这并非必要,只能说是通用性较好的一种实现方案。

                                                                         session原理

以下这些大多都是文字表达,没有一一写实际的案例出来演示,但是详细的看一下的话,也就知道什么意思了,文中表达比较通俗。

Web三大概念:cookie,session,application

Session:记录一系列状态

Session与cookie功能效果相同。Session与Cookie的区别在于Session是记录在服务端的,而Cookie是记录在客户端的。

解释session:当访问服务器否个网页的时候,会在服务器端的内存里开辟一块内存,这块内存就叫做session,而这个内存是跟浏览器关联在一起的。这个浏览器指的是浏览器窗口,或者是浏览器的子窗口,意思就是,只允许当前这个session对应的浏览器访问,就算是在同一个机器上新启的浏览器也是无法访问的。而另外一个浏览器也需要记录session的话,就会再启一个属于自己的session

原理:HTTP协议是非连接性的,取完当前浏览器的内容,然后关闭浏览器后,链接就断开了,而没有任何机制去记录取出后的信息。而当需要访问同一个网站的另外一个页面时(就好比如在第一个页面选择购买的商品后,跳转到第二个页面去进行付款)这个时候取出来的信息,就读不出来了。所以必须要有一种机制让页面知道原理页面的session内容。

问题:如何知道浏览器和这个服务器中的session是一一对应的呢?又如何保证不会去访问其它的session呢?

原理解答:就是当访问一个页面的时候给浏览器创建一个独一无二的号码,也给同时创建的session赋予同样的号码。这样就可以在打开同一个网站的第二个页面时获取到第一个页面中session保留下来的对应信息(理解:当访问第二个页面时将号码同时传递到第二个页面。找到对应的session。)。这个号码也叫sessionID,session的ID号码,session的独一无二号码。

session的两种实现方式(也就是传递方式):第一种通过cookies实现。第二种通过URL重写来实现

第一种方式的理解:就是把session的id 放在cookie里面(为什么是使用cookies存放呢,因为cookie有临时的,也有定时的,临时的就是当前浏览器什么时候关掉即消失,也就是说session本来就是当浏览器关闭即消失的,所以可以用临时的cookie存放。保存再cookie里的sessionID一定不会重复,因为是独一无二的。),当允许浏览器使用cookie的时候,session就会依赖于cookies,当浏览器不支持cookie后,就可以通过第二种方式获取session内存中的数据资源。

第二种方式的理解:在客户端不支持cookie的情况下使用。为了以防万一,也可以同时使用。

如果不支持cookie,必须自己编程使用URL重写的方式实现。

   如何重写URL:通过response.encodeURL()方法

      encodeURL()的两个作用

         第一个作用:转码(说明:转中文的编码,或者一些其他特殊的编码。就好比如网页的链接中存在中文字符,就会转换成为一些百分号或者其他的符号代替。)

第二个作用:URL后面加入sessionID,当不支持cookie的时候,可以使用encodeURL()方法,encodeUTL()后面跟上sessionID,这样的话,在禁用cookie的浏览器中同时也可以使用session了。但是需要自己编程,只要链接支持,想用session就必须加上encodeURL()。

提示:若想程序中永远支持session,那就必须加上encodeURL(),当别人禁用了cookie,一样可以使用session。

简单的代码例子:在没有使用encodeURL()方法前的代码

在使用encodeURL()方法后的代码

看下图,当重写URL 的时候,每一次访问的时候都会将sessionID传过来,传过来了,就没有必要再在cookie里读了。

规则:

如果浏览器支持cookie,创建session多大的时候,会被sessionID保存再cookie里。只要允许cookie,session就不会改变,如果不允许使用cookie,每刷新一次浏览器就会换一个session(因为浏览器以为这是一个新的链接)
如果不支持cookie,必须自己编程使用URL重写的方式实现session
Session不像cookie一样拥有路径访问的问题,同一个application下的servlet/jsp都可以共享同一个session,前提下是同一个客户端窗口。
Session中的一些常用方法说明

isNew():是否是新的Session,一般在第一次访问的时候出现

getid():拿到session,获取ID

getCreationTime():当前session创建的时间

getLastAccessedTime():最近的一次访问这个session的时间。

getRrquestedSessionid: 跟随上个网页cookies或者URL传过来的session

isRequestedSessionIdFromCookie():是否通过Cookies传过来的

isRequestedSessionIdFromURL():是否通过重写URL传过来的

isRequestedSessionIdValid():是不是有效的sessionID

其中下面的结果图对应上面的8个方法

  

其对应的代码

session有期限:

当一个网站的第一个窗口关掉了,而没有继续接着访问第二个页面,就没有使用到session。那么session会在中断程序后立刻关闭session吗?这个时候session就需要给它保留的时间,当最近一次访问的时候开始计时,每刷新一次重写开始计时。当隔了这么久的时间,没有访问这个session后,对不起,要关闭这个session了。session有过期时间,session什么时候过期,要看配置,

session能干什么:

session就是服务器里面的一块内存,内存里面能放任何东西,只要是名值对就可以了。

session里面的名字永远都是String类型



                                                           COOKIE和SESSION有什么区别?
cookie保存在客户端,session保存在服务器端,
cookie目的可以跟踪会话,也可以保存用户喜好或者保存用户名密码
session用来跟踪会话

当我们登录网站勾选保存用户名和密码的时候,一般保存的都是cookie,将用户名和密码的cookie保存到硬盘中,这样再次登录的时候浏览器直接将cookie发送到服务端验证,直接username和password保存到客户端,当然这样不安全,浏览器也可以加密解密这样做,每个浏览器都可以有自己的加密解密方式,这样方便了用户,再比如用户喜欢的网页背景色,比如QQ空间的背景,这些信息也是可以通过cookie保存到客户端的,这样登录之后直接浏览器直接就可以拿到相应的偏好设置。

②跟踪会话,比如某些网站中网页有不同的访问权限,有只能登录的用户访问的网页或者用户级别不同不能访问的,但是http请求是无状态的,每次访问服务端是不知道是否是登录用户,很自然的想到在http请求报文中加入登录标识就可以了,这个登录标识就可以是cookie,这样的cookie服务端要保存有所有登录用户的cookie,这样请求报文来了之后拿到登录标识cookie,在服务端进行比较久可以了。再比如购物网站,多次点击添加商品到购物车客户端很容易知道哪些物品在购物车中,但是服务端怎么知道每次添加的物品放到哪个登录用户的购物车中呢?也需要请求报文中带着cookie才行(在不登陆的情况下京东也是可以不断添加商品的,推测应该是登录的时候一并创建cookie并且发送物品信息),这些cookie都是为了跟踪会话用的,所以客户端有,服务端也有,并且服务端有全部的会话cookie。

后面衍生出session技术,session技术是要使用到cookie的,之所以出现session技术,主要是为了安全。
 

http是无状态的协议,客户每次读取web页面时,服务器都打开新的会话,而且服务器也不会自动维护客户的上下文信息,那么要怎么才能实现网上商店中的购物车呢,session就是一种保存上下文信息的机制,它是针对每一个用户的,变量的值保存在服务器端,通过SessionID来区分不同的客户,session是以cookie或URL重写为基础的,默认使用cookie来实现,系统会创造一个名为JSESSIONID的输出cookie,我们叫做session cookie,以区别persistent cookies,也就是我们通常所说的cookie,注意session cookie是存储于浏览器内存中的,并不是写到硬盘上的,这也就是我们刚才看到的JSESSIONID,我们通常情是看不到JSESSIONID的,但是当我们把浏览器的cookie禁止后,web服务器会采用URL重写的方式传递Sessionid,我们就可以在地址栏看到 sessionid=KWJHUG6JJM65HS2K6之类的字符串。

大家请看在HTTP请求报文头的最后一行有cookie,不过是JSessionID的cookie值

Cookie: $Version=1; Skin=new;jsessionid=5F4771183629C9834F8382E23BE13C4C

比如前两个值,应该属于偏好设置之类的。


服务端是怎么知道客户端的多个请求是隶属于一个Session呢?注意到后台的那个jsessionid=5F4771183629C9834F8382E23BE13C4C木有?原来就是通过HTTP请求报文头的Cookie属性的jsessionid的值关联起来的!(当然也可以通过重写URL的方式将会话ID附带在每个URL的后面哦)。
明白了原理,我们就可以很容易的分辨出persistent cookies和session cookie的区别了,网上那些关于两者安全性的讨论也就一目了然了,session cookie针对某一次会话而言,会话结束session cookie也就随着消失了,而persistent cookie只是存在于客户端硬盘上的一段文本(通常是加密的),而且可能会遭到cookie欺骗以及针对cookie的跨站脚本攻击,自然不如 session cookie安全了。
通常session cookie是不能跨窗口使用的,当你新开了一个浏览器窗口进入相同页面时,系统会赋予你一个新的sessionid,这样我们信息共享的目的就达不到了,此时我们可以先把sessionid保存在persistent cookie中,然后在新窗口中读出来,就可以得到上一个窗口SessionID了,这样通过session cookie和persistent cookie的结合我们就实现了跨窗口的session tracking(会话跟踪)。
在一些web开发的书中,往往只是简单的把Session和cookie作为两种并列的http传送信息的方式,session cookies位于服务器端,persistent cookie位于客户端,可是session又是以cookie为基础的,明白的两者之间的联系和区别,我们就不难选择合适的技术来开发web service了。


部分参考自:session与cookie的区别

===================分割线==============
举个QQ空间的例子:
① 当我们登录QQ空间的时候,可以选择保存用户名和密码,这样下次登录的时候浏览器可以自动填充或者自动登陆,此时使用的是cookie技术,将于

http://qzone.qq.com/

域名对应的cookie保存到硬盘中,下次访问的时候浏览器查找保存在硬盘中的与该域名对应的cookie填充。

②登录之后,我们可能做些操作,比如删除日志,发表说说,这些只有登录用户才能做的事情可以使用cookie也可以使用session进行会话跟踪

③空间的喜好设置可以保存到硬盘cookie当中。

-------------------------------------------------------------------------------------------------------------------------
其实说白了session就是用来保存会话的cookie。
下面介绍下Java中Servlet的session管理

http://lavasoft.blog.51cto.com/62575/275589  深入理解HTTP Session


session在web开发中是一个非常重要的概念,这个概念很抽象,很难定义,也是最让人迷惑的一个名词,也是最多被滥用的名字之一,在不同的场合,session一次的含义也很不相同。这里只探讨HTTP Session。

为了说明问题,这里基于Java Servlet理解Session的概念与原理,这里所说Servlet已经涵盖了JSP技术,因为JSP最终也会被编译为Servlet,两者有着相同的本质。

在Java中,HTTP的Session对象用javax.servlet.http.HttpSession来表示。

1、概念:Session代表服务器与浏览器的一次会话过程,这个过程是连续的,也可以时断时续的。在Servlet中,session指的是HttpSession类的对象,这个概念到此结束了,也许会很模糊,但只有看完本文,才能真正有个深刻理解。

2、Session创建的时间是:
一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用 HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用 <% @page session="false"%> 关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句 HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的 session对象的来历。
由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。

引申:
1)、访问*.html的静态资源因为不会被编译为Servlet,也就不涉及session的问题。
2)、当JSP页面没有显式禁止session的时候,在打开浏览器第一次请求该jsp的时候,服务器会自动为其创建一个session,并赋予其一个sessionID,发送给客户端的浏览器。以后客户端接着请求本应用中其他资源的时候,会自动在请求头上添加:
Cookie:JSESSIONID=客户端第一次拿到的session ID
这样,服务器端在接到请求时候,就会收到session ID,并根据ID在内存中找到之前创建的session对象,提供给请求使用。这也是session使用的基本原理----搞不懂这个,就永远不明白session的原理。
下面是两次请求同一个jsp,请求头信息:

通过图可以清晰发现,第二次请求的时候,已经添加session ID的信息。
3、Session删除的时间是:
1)Session超时:超时指的是连续一定时间服务器没有收到该Session所对应客户端的请求,并且这个时间超过了服务器设置的Session超时的最大时间。
2)程序调用HttpSession.invalidate()
3)服务器关闭或服务停止

4、session存放在哪里:服务器端的内存中。不过session可以通过特殊的方式做持久化管理。

5、session的id是从哪里来的,sessionID是如何使用的:当客户端第一次请求session对象时候,服务器会为客户端创建一个session,并将通过特殊算法算出一个session的ID,用来标识该session对象,当浏览器下次(session继续有效时)请求别的资源的时候,浏览器会偷偷地将sessionID放置到请求头中,服务器接收到请求后就得到该请求的sessionID,服务器找到该id的session返还给请求者(Servlet)使用。一个会话只能有一个session对象,对session来说是只认id不认人。

6、session会因为浏览器的关闭而删除吗?
不会,session只会通过上面提到的方式去关闭。

7、同一客户端机器多次请求同一个资源,session一样吗?
一般来说,每次请求都会新创建一个session。


其实,这个也不一定的,总结下:对于多标签的浏览器(比如360浏览器)来说,在一个浏览器窗口中,多个标签同时访问一个页面,session是一个。对于多个浏览器窗口之间,同时或者相隔很短时间访问一个页面,session是多个的,和浏览器的进程有关。对于一个同一个浏览器窗口,直接录入url访问同一应用的不同资源,session是一样的。

8、session是一个容器,可以存放会话过程中的任何对象。

9、session因为请求(request对象)而产生,同一个会话中多个request共享了一session对象,可以直接从请求中获取到session对象。

10、其实,session的创建和使用总在服务端,而浏览器从来都没得到过session对象。但浏览器可以请求Servlet(jsp也是Servlet)来获取session的信息。客户端浏览器真正紧紧拿到的是session ID,而这个对于浏览器操作的人来说,是不可见的,并且用户也无需关心自己处于哪个会话过程中。
---------------------------------------------------------------------------------------------------
比如下面一段使用session的代码

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{

   response.setContentType("text/html");
   response.setCharacterEncoding("utf-8");
   PrintWriter out = response.getWriter();
   // 得到用户名和密码,验证
   String u = request.getParameter("username");
   String p = request.getParameter("password");

   UserBeanBO ubb = new UserBeanBO();
   if (ubb.checkUser(u, p))
   {
	// 1.把成功登陆的用户所有信息放入session
	UserBean ub = ubb.getUserBean(u);
	request.getSession().setAttribute("userInfo", ub);
	// 2.把购物车的信息取出
	MyCartBO mcb = (MyCartBO)request.getSession().getAttribute("mycart");    
	ArrayList al = mcb.showMyCart();
	// 把al放入request
	request.setAttribute("mycartInfo", al);
	// 用户合法
	request.getRequestDispatcher("success.jsp").forward(request,response);
  } else
  {
    // 用户不合法
      request.getRequestDispatcher("error.jsp").forward(request, response);		  	
  }

}
HttpSession  javax.servlet.http.HttpServletRequest.getSession()

Returns the current session associated with this request, or if the request 
does not have a session, creates one.

Returns: the HttpSession associated with this request

See Also:

getSession(boolean)

javax.servlet.http.HttpServletRequest.getSession() 将会返回当前request相关联的HttpSession对象,如果不存在,将会创建一个。

翻译一下,当一个浏览器请求来到之后,Servlet处理程序(Servlet容器内部实现)将会主动检查请求信息Cookie当中是否有JSESSIONID,若有,找到对应JSESSION的HttpSession对象,如果没有,创建一个,具体的机制在Servlet容器的实现当中。

                                                  springsession实现session共享实现原理以及源码解析

实现原理这里简单说明描述:

就是当Web服务器接收到http请求后,当请求进入对应的Filter进行过滤,将原本需要由web服务器创建会话的过程转交给Spring-Session进行创建,本来创建的会话保存在Web服务器内存中,通过Spring-Session创建的会话信息可以保存第三方的服务中,如:redis,mysql等。Web服务器之间通过连接第三方服务来共享数据,实现Session共享!

实现原理结构草图

整个实现流程和源码详细介绍

本次源码介绍基于上一篇内容,并且在保存Session的时候只会分析使用JedisConnectionFactory实现的RedisConnectionFactory !

1.SessionRepositoryFilter和JedisConnectionFactory注册过程

流程:

SessionRepositoryFilter和JedisConnectionFactory注册过程

说明:

1.、启动WEB项目的时候,会读取web.xml,读取顺序content-param --> listener --> filter --> servlet

2.、ContextLoaderListener监听器的作用就是启动Web容器时,自动装配ApplicationContext的配置信息

3、初始化根web应用程序上下文。

4、SpringHttpSessionConfiguration注册 springSessionRepositoryFilter :bean,RedisHttpSessionConfiguration 注册 sessionRedisTemplate : bean  和 sessionRepository : bean

45、配置文件配置JedisConnectionFactory implements RedisConnectionFactory ,创建 jedisConnectionFactory bean

代码分析如下:

  1. web.xml ,加载了xml配置文件,并初始化web应用上下文
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:spring/*xml</param-value>
  </context-param>


  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  

2.application-session.xml中,配置 RedisHttpSessionConfiguration的bean和JedisConnectionFactory的bean,web应用初始化加载bean!

<!--创建一个Spring Bean的名称springSessionRepositoryFilter实现过滤器。
    筛选器负责将HttpSession实现替换为Spring会话支持。在这个实例中,Spring会话得到了Redis的支持。-->
    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>

    <!--创建了一个RedisConnectionFactory,它将Spring会话连接到Redis服务器。我们配置连接到默认端口(6379)上的本地主机!-->
    <!--集群Redis-->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!--Redis-CLuster-->
        <constructor-arg index="0" ref="redisClusterConfig"/>

        <!--配置Redis连接池 ,可以不配置,使用默认就行!-->
        <constructor-arg index="1" ref="jedisPoolConfig"/>
    </bean>
    
  1.  
    /**
     * 初始化根web应用程序上下文。
     */
    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }
    

4.RedisHttpSessionConfiguration类图

RedisHttpSessionConfiguration类图

RedisHttpSessionConfiguration注释
RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration

public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements EmbeddedValueResolverAware, ImportAware {
        

4.1 SpringHttpSessionConfiguration 创建一个名称为springSessionRepositoryFilter的bean

@Bean
    public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(
            SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(
                sessionRepository);
        sessionRepositoryFilter.setServletContext(this.servletContext);
        if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
            sessionRepositoryFilter.setHttpSessionStrategy(
                    (MultiHttpSessionStrategy) this.httpSessionStrategy);
        }
        else {
            sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
        }
        return sessionRepositoryFilter;
    }

4.2 创建RedisHttpSessionConfiguration#RedisTemplate bean的名称为sessionRedisTemplate

@Bean
    public RedisTemplate<Object, Object> sessionRedisTemplate(
            RedisConnectionFactory connectionFactory) {
            //实例化 RedisTemplate 
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
        //设置key序列化 StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        //设置Hash key  StringRedisSerializer
        template.setHashKeySerializer(new StringRedisSerializer());
        if (this.defaultRedisSerializer != null) {
            template.setDefaultSerializer(this.defaultRedisSerializer);
        }
        //设置 connectionFactory。第五步创建的(实际connectionFactory加载过程和讲解过程顺序不一样)
        template.setConnectionFactory(connectionFactory);
        return template;
    }

4.3 创建RedisHttpSessionConfiguration#RedisOperationsSessionRepository bean的名称为sessionRepository

    @Bean
    public RedisOperationsSessionRepository sessionRepository(
    //使用sessionRedisTemplate bean
            @Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate,
            ApplicationEventPublisher applicationEventPublisher) {
            
            //實例化RedisOperationsSessionRepository
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
                sessionRedisTemplate);
                //設置applicationEventPublisher
        sessionRepository.setApplicationEventPublisher(applicationEventPublisher);
        //設置最大的失效時間 maxInactiveIntervalInSeconds = 1800
        sessionRepository
                .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
        if (this.defaultRedisSerializer != null) {
            sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
        }

        String redisNamespace = getRedisNamespace();
        if (StringUtils.hasText(redisNamespace)) {
            sessionRepository.setRedisKeyNamespace(redisNamespace);
        }

        sessionRepository.setRedisFlushMode(this.redisFlushMode);
        return sessionRepository;
    }
    
  1. 创建 RedisConnectionFactory bean为 jedisConnectionFactory
    JedisConnectionFactory类图
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">

2.SessionRepositoryFilter添加到FilterChain

流程:

SessionRepositoryFilter添加到FIlterChain

说明:

1 2、在Servlet3.0规范中,Servlet容器启动时会自动扫描javax.servlet.ServletContainerInitializer的实现类,在实现类中我们可以定制需要加载的类。 通过注解@HandlesTypes(WebApplicationInitializer.class),让Servlet容器在启动该类时,会自动寻找所有的WebApplicationInitializer实现类。

2.1、insertSessionRepositoryFilter 方法通过filterName获取 SessionRepositoryFilter ,并创建了 new DelegatingFilterProxy(filterName);

3 4、然后将filter添加到FilterChain中

1.ServletContainerInitializer的实现类加载和通过注解@HandlesTypes(WebApplicationInitializer.class)实现类的加载

//加载实现类
@HandlesTypes(WebApplicationInitializer.class)
//SpringServletContainerInitializer实现ServletContainerInitializer
public class SpringServletContainerInitializer implements ServletContainerInitializer {

//------------

2.AbstractHttpSessionApplicationInitializer实现WebApplicationInitializer进行加载


@Order(100)
public abstract class AbstractHttpSessionApplicationInitializer
        implements WebApplicationInitializer {

2.1 onStartup

public void onStartup(ServletContext servletContext) throws ServletException {
        beforeSessionRepositoryFilter(servletContext);
        if (this.configurationClasses != null) {
            AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
            rootAppContext.register(this.configurationClasses);
            servletContext.addListener(new ContextLoaderListener(rootAppContext));
        }
        //添加Filter
        insertSessionRepositoryFilter(servletContext);
        afterSessionRepositoryFilter(servletContext);
    }

2.1.1.insertSessionRepositoryFilter


    /**
     * 注册springSessionRepositoryFilter
     * @param servletContext the {@link ServletContext}
     */
    private void insertSessionRepositoryFilter(ServletContext servletContext) {
// DEFAULT_FILTER_NAME = "springSessionRepositoryFilter"
        String filterName = DEFAULT_FILTER_NAME;
//通过filterName创建 DelegatingFilterProxy
        DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(
                filterName);
        String contextAttribute = getWebApplicationContextAttribute();
        if (contextAttribute != null) {
            springSessionRepositoryFilter.setContextAttribute(contextAttribute);
        }
//根据filterName和上下文添加Filter到FilterChain
        registerFilter(servletContext, true, filterName, springSessionRepositoryFilter);
    }
  1. registerFilter
    private void registerFilter(ServletContext servletContext,
            boolean insertBeforeOtherFilters, String filterName, Filter filter) {
        Dynamic registration = servletContext.addFilter(filterName, filter);
        if (registration == null) {
            throw new IllegalStateException(
                    "Duplicate Filter registration for '" + filterName
                            + "'. Check to ensure the Filter is only configured once.");
        }
        //是否支持异步,默认 true
        registration.setAsyncSupported(isAsyncSessionSupported());
        //得到DispatcherType springSessionRepositoryFilter
        EnumSet<DispatcherType> dispatcherTypes = getSessionDispatcherTypes();
        //添加一个带有给定url模式的筛选器映射和由这个FilterRegistration表示的过滤器的分派器类型。 过滤器映射按照添加它们的顺序进行匹配。
        registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters,
                "/*");
    }
  1. addFilter将Filter添加到ServletContext中
    public FilterRegistration.Dynamic addFilter(
        String filterName, Filter filter);

3.SessionRepositoryFilter拦截过程

流程:

SessionRepositoryFilter拦截过程

说明:

1、请求被DelegatingFilterProxy : 拦截到,然后执行doFilter方法,在doFilter中找到执行的代理类。
2、OncePerRequestFilter : 代理Filter执行doFilter方法,然后调用抽象方法doFilterInternal
3、SessionRepositoryFilter 继承了OncePerRequestFilter,实现了doFilterInternal,这个方法一个封装一个wrappedRequest,通过执行commitSession保存session信息到redis

1请求进来,被DelegatingFilterProxy 拦截到,在web.xml中进行了配置
1.1 执行doFilter

如果没有指定目标bean名称,请使用筛选器名称。
@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // 如果需要,延迟初始化委托。 necessary.
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                if (this.delegate == null) {
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: " +
                                "no ContextLoaderListener or DispatcherServlet registered?");
                    }
                    this.delegate = initDelegate(wac);
                }
                delegateToUse = this.delegate;
            }
        }

        // 让委托执行实际的doFilter操作
        invokeDelegate(delegateToUse, request, response, filterChain);
    }

1.2 initDelegate

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//可以获取到SessionRepositoryFilter [备注1]
        Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }

//[备注1] 因为 :SessionRepositoryFilter是一个优先级最高的javax.servlet.Filter
/*
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends ExpiringSession>
        extends OncePerRequestFilter {

*/
  1. delegate.doFilter();
    protected void invokeDelegate(
            Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //代理去执行doFilter,代理为SessionRepositoryFilter
        delegate.doFilter(request, response, filterChain);
    }

2.1 OncePerRequestFilter#doFilter

public final void doFilter(ServletRequest request, ServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        if (!(request instanceof HttpServletRequest)
                || !(response instanceof HttpServletResponse)) {
            throw new ServletException(
                    "OncePerRequestFilter just supports HTTP requests");
        }
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        boolean hasAlreadyFilteredAttribute = request
                .getAttribute(this.alreadyFilteredAttributeName) != null;

        if (hasAlreadyFilteredAttribute) {

            //在不调用此过滤器的情况下进行…
            filterChain.doFilter(request, response);
        }
        else {
            // 调用这个过滤器…
            request.setAttribute(this.alreadyFilteredAttributeName, Boolean.TRUE);
            try {
            //doFilterInternal是个抽象方法
                doFilterInternal(httpRequest, httpResponse, filterChain);
            }
            finally {
                // 删除此请求的“已过滤”请求属性。
                request.removeAttribute(this.alreadyFilteredAttributeName);
            }
        }
    }
  1. 执行SessionRepositoryFilter#doFilterInternal
@Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        //使用HttpServletRequest 、HttpServletResponse和servletContext创建一个SessionRepositoryRequestWrapper

        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
                request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                wrappedRequest, response);

//使用CookieHttpSessionStrategy重新包装了 HttpServletRequest
        HttpServletRequest strategyRequest = this.httpSessionStrategy
                .wrapRequest(wrappedRequest, wrappedResponse);
        HttpServletResponse strategyResponse = this.httpSessionStrategy
                .wrapResponse(wrappedRequest, wrappedResponse);

        try {
        //执行其他过滤器
            filterChain.doFilter(strategyRequest, strategyResponse);
        }
        finally {
        //保存session信息
            wrappedRequest.commitSession();
        }
    }

4 .wrappedRequest.commitSession() 看下第四大点分析

4.SessionRepository保存session数据

流程:
SessionRepository保存session数据

说明:

1、提交session保存
2、获取当前session,这一步比较重要,获取了一个HttpSessionWrapper,这个HttpSessionWrapper替换了HTTPSession
3、wrappedSession获取当前的Session
4、使用 RedisTemplate 保存Session内容,并通过调用RedisConnection 使用它的实现类JedisClusterConnection获取redis连接

1.commitSession

/**
*使用HttpSessionStrategy将会话id写入响应。 *保存会话。
*/
private void commitSession() {
            HttpSessionWrapper wrappedSession = getCurrentSession();
            if (wrappedSession == null) {
                if (isInvalidateClientSession()) {
                    SessionRepositoryFilter.this.httpSessionStrategy
                            .onInvalidateSession(this, this.response);
                }
            }
            else {
                S session = wrappedSession.getSession();
                SessionRepositoryFilter.this.sessionRepository.save(session);
                if (!isRequestedSessionIdValid()
                        || !session.getId().equals(getRequestedSessionId())) {
                    SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
                            this, this.response);
                }
            }
        }

2.getCurrentSession

会话存储库请求属性名。
public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class
            .getName();
            
private static final String CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR
            + ".CURRENT_SESSION";

private HttpSessionWrapper getCurrentSession() {
            return (HttpSessionWrapper)
            //获取session
            getAttribute(CURRENT_SESSION_ATTR);
        }
        
   /**
     * 此方法的默认行为是在包装请求对象上调用getAttribute(字符串名称)。
     */
    public Object getAttribute(String name) {
    //这里的request就是上面封装的
        return this.request.getAttribute(name);
    }

3 .wrappedSession.getSession

//返回 RedisSession
S session = wrappedSession.getSession();
//-------------------------
public S getSession() {
        return this.session;
    }
class ExpiringSessionHttpSession<S extends ExpiringSession> implements HttpSession {
    private S session;


final class RedisSession implements ExpiringSession {

4.save,实际是调用 RedisOperationsSessionRepository的 RedisOperations 操作

SessionRepositoryFilter.this.sessionRepository.save(session);

//this.sessionRepository =  SessionRepository<S> sessionRepository;

//--------------------------------
//这个RedisOperationsSessionRepository是之前就创建好的
public class RedisOperationsSessionRepository implements
        FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>,
        MessageListener {
        
        
public interface FindByIndexNameSessionRepository<S extends Session>
        extends SessionRepository<S> {
        
//---------------------------


    public void save(RedisSession session) {
        //4.1saveDelta
        session.saveDelta();
        if (session.isNew()) {
            //4.2调用
            String sessionCreatedKey = getSessionCreatedChannel(session.getId());
            //4.3convertAndSend
            //RedisOperations = this.sessionRedisOperations
            this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
            session.setNew(false);
        }
    }
    
    

其中RedisOperationsSessionRepository 里面介绍保存的详细过程,具体请看文档说明:

Class RedisOperationsSessionRepository

因为 RedisTemplate implements RedisOperations,实际进行操作的是RedisTemplate,RedisTemplate通过RedisConnection进行数据add和remove等

public class RedisTemplate<K, V>
extends RedisAccessor
implements RedisOperations<K, V>, BeanClassLoaderAware

总结

本系列到这里也就结束了,本次话的整个流程图,会上传到github上,使用Jude打开就可以看!

如果有什么地方写的不对或者有想和我一起探讨一下的,欢迎加我的QQ或者QQ群!

记录一个小点:

Spring Session + Redis实现分布式Session共享 有个非常大的缺陷, 无法实现跨域名共享session , 只能在单台服务器上共享session , 因为是依赖cookie做的 , cookie 无法跨域 pring Session一般是用于多台服务器负载均衡时共享Session的,都是同一个域名,不会跨域。你想要的跨域的登录,可能需要SSO单点登录。

                                                        分布式环境session一致性问题解决

sessionid是一个会话的key,浏览器第一次访问服务器会在服务器端生成一个session,有一个sessionid和它对应。tomcat生成的sessionid叫做jsessionid。

session在访问tomcat服务器HttpServletRequest的getSession(true)的时候创建,tomcat的ManagerBase类提供创建sessionid的方法:随机数+时间+jvmid;

存储在服务器的内存中,tomcat的StandardManager类将session存储在内存中,也可以持久化到file,数据库,memcache,redis等。客户端只保存sessionid到cookie中,而不会保存session,session销毁只能通过invalidate或超时,关掉浏览器并不会关闭session。

那么Session在何时创建呢?当然还是在服务器端程序运行的过程中创建的,不同语言实现的应用程序有不同创建Session的方法,而在Java中是通过调用HttpServletRequest的getSession方法(使用true作为参数)创建的。在创建了Session的同时,服务器会为该Session生成唯一的Session id,而这个Session id在随后的请求中会被用来重新获得已经创建的Session;在Session被创建之后,就可以调用Session相关的方法往Session中增加内容了,而这些内容只会保存在服务器中,发到客户端的只有Session id;当客户端再次发送请求的时候,会将这个Session id带上,服务器接受到请求之后就会依据Session id找到相应的Session,从而再次使用之。

创建:sessionid第一次产生是在直到某server端程序调用 HttpServletRequest.getSession(true)这样的语句时才被创建。

删除:超时;程序调用HttpSession.invalidate();程序关闭;

session存放在哪里:服务器端的内存中。不过session可以通过特殊的方式做持久化管理(memcache,redis)。

session的id是从哪里来的,sessionID是如何使用的:当客户端第一次请求session对象时候,服务器会为客户端创建一个session,并将通过特殊算法算出一个session的ID,用来标识该session对象

session会因为浏览器的关闭而删除吗?

不会,session只会通过上面提到的方式去关闭。

下面是tomcat中session的创建:

ManagerBase是所有session管理工具类的基类,它是一个抽象类,所有具体实现session管理功能的类都要继承这个类,该类有一个受保护的方法,该方法就是创建sessionId值的方法:

(tomcat的session的id值生成的机制是一个随机数加时间加上jvm的id值,jvm的id值会根据服务器的硬件信息计算得来,因此不同jvm的id值都是唯一的),

StandardManager类是tomcat容器里默认的session管理实现类,

它会将session的信息存储到web容器所在服务器的内存里。

PersistentManagerBase也是继承ManagerBase类,它是所有持久化存储session信息的基类,PersistentManager继承了PersistentManagerBase,但是这个类只是多了一个静态变量和一个getName方法,目前看来意义不大,对于持久化存储session,tomcat还提供了StoreBase的抽象类,它是所有持久化存储session的基类,另外tomcat还给出了文件存储FileStore和数据存储JDBCStore两个实现。

什么是session一致性问题?
只要用户不重启浏览器,每次http短连接请求,理论上服务端都能定位到session,保持会话。

分布式session
单服务器web应用中,session信息只需存在该服务器中,这是我们前几年最常接触的方式,但是近几年随着分布式系统的流行,单系统已经不能满足日益增长的百万级用户的需求,集群方式部署服务器已在很多公司运用起来,当高并发量的请求到达服务端的时候通过负载均衡的方式分发到集群中的某个服务器,这样就有可能导致同一个用户的多次请求被分发到集群的不同服务器上,就会出现取不到session数据的情况,于是session的共享就成了一个问题。

如上图,假设用户包含登录信息的session都记录在第一台web-server上,反向代理如果将请求路由到另一台web-server上,可能就找不到相关信息,而导致用户需要重新登录。

Session一致性解决方案
1.session复制(同步)

思路:多个web-server之间相互同步session,这样每个web-server之间都包含全部的session

优点:web-server支持的功能,应用程序不需要修改代码

不足:

session的同步需要数据传输,占内网带宽,有时延

所有web-server都包含所有session数据,数据量受内存限制,无法水平扩展

有更多web-server时要歇菜

2.客户端存储法 


思路:服务端存储所有用户的session,内存占用较大,可以将session存储到浏览器cookie中,每个端只要存储一个用户的数据了

优点:服务端不需要存储

缺点:

每次http请求都携带session,占外网带宽

数据存储在端上,并在网络传输,存在泄漏、篡改、窃取等安全隐患

session存储的数据大小受cookie限制

“端存储”的方案虽然不常用,但确实是一种思路。

3.反向代理hash一致性
 思路:web-server为了保证高可用,有多台冗余,反向代理层能不能做一些事情,让同一个用户的请求保证落在一台web-server上呢?

方案一:四层代理hash

反向代理层使用用户ip来做hash,以保证同一个ip的请求落在同一个web-server上

方案二:七层代理hash

反向代理使用http协议中的某些业务属性来做hash,例如sid,city_id,user_id等,能够更加灵活的实施hash策略,以保证同一个浏览器用户的请求落在同一个web-server上

优点:

只需要改nginx配置,不需要修改应用代码

负载均衡,只要hash属性是均匀的,多台web-server的负载是均衡的

可以支持web-server水平扩展(session同步法是不行的,受内存限制)

不足:

如果web-server重启,一部分session会丢失,产生业务影响,例如部分用户重新登录

如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session

session一般是有有效期的,所有不足中的两点,可以认为等同于部分session失效,一般问题不大。

对于四层hash还是七层hash,个人推荐前者:让专业的软件做专业的事情,反向代理就负责转发,尽量不要引入应用层业务属性,除非不得不这么做(例如,有时候多机房多活需要按照业务属性路由到不同机房的web-server)。

4.后端统一集中存储

思路:将session存储在web-server后端的存储层,数据库或者缓存

优点:

没有安全隐患

可以水平扩展,数据库/缓存水平切分即可

web-server重启或者扩容都不会有session丢失

不足:增加了一次网络调用,并且需要修改应用代码

对于db存储还是cache,个人推荐后者:session读取的频率会很高,数据库压力会比较大。如果有session高可用需求,cache可以做高可用,但大部分情况下session可以丢失,一般也不需要考虑高可用。

总结
保证session一致性的架构设计常见方法:

session同步法:多台web-server相互同步数据

客户端存储法:一个用户只存储自己的数据

反向代理hash一致性:四层hash和七层hash都可以做,保证一个用户的请求落在一台web-server上

后端统一存储:web-server重启和扩容,session也不会丢失

对于方案3和方案4,个人建议推荐后者:

web层、service层无状态是大规模分布式系统设计原则之一,session属于状态,不宜放在web层

让专业的软件做专业的事情,web-server存session?还是让cache去做这样的事情吧。

                                                          利用redis解决分布式session一致性问题

业务场景

在单机系统中,用户登陆之后,服务端会保存用户的会话信息,只要用户不退出重新登陆,在一段时间内用户可以一直访问该网站,无需重复登陆。用户的信息存在服务端的 session 中,session中可以存放服务端需要的一些用户信息,例如用户ID,所属公司companyId,所属部门deptId等等。

但是随着业务的发展,技术架构需要调整,原来的单机系统逐渐被更换,架构由单机扩展到分布式,甚至当下流行的微服务。虽然在用户端看来系统仍然是一个整体,但在技术端来说业务则被拆分成多个模块,各个模块之间相互独立,甚至不在同一台物理机器上,模块之间通过 RPC 进行通信。

那么原来单机只需一份的 session, 如何满足在多系统的运行下保证会话一致性呢?单独保存在任何一个系统中都不合适,而且每个单独模块系统也可能是分布式形式的,是由集群组成。那么session的分配就更复杂了。

2|0Redis 实现

针对以上问题,我们可能会从以下几个方面想到解决的方法,每个服务端存储一份,通过同步的方式保证一致性,但是这种方式有个很明显的缺点:session的同步需要数据传输,占内网带宽,有时延,网络不稳定的时候会造成部分系统同步延迟,那么就不能保证 session 一致性。而且所有服务端都包含所有session数据,数据量受内存限制,无法水平扩展。

那么我们是否可以单独将 session 信息存储在某一个独立的介质中,介质可以是DB也可以是缓存。

考虑到如下业务:登陆的时候我们经常会给用户一个过期时间(一般移动端常设置为7天或者一个月甚至更久),到期后用户需要输入登陆信息重新登陆,即会话过期。这种到期的设置我们自然想到了Redis的 key expire功能,所以最终我们可以将Redis引入进来实现我们的这种需求。系统如下图所示:

我们只需在用户首次登陆的时候将用户信息放到 Token并缓存到 Redis 中,同时设置一个过期时间,伪代码如下:

@Override
    public Map login(UserDto dto) {
        Map<String, Object> restMap = new HashMap<>();
        
        // 校验登陆信息
        User user = checkLoginInfo(dto);

         //删除旧的token
        String token = (String) redisUtils.get(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName());
        
        if (!ObjectUtils.isEmpty(token)) {
            redisUtils.delete(CacheConstants.USER_TOKEN_KEY_WEB + token);
        }
        // 唯一签名信息
        String signStr = user.getCompanyId() + user.getUserName() + dto.getPassword() + DateUtils.now().getTime();
        token = MD5Utils.md5(signStr);
        // 设置用户 token
        redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_WEB + token, user.getId(), LOGIN_EXPIRED_TIME);
        //缓存新的token
        redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName(), token, LOGIN_EXPIRED_TIME);
        dto.setCompanyId(user.getCompanyId());
        dto.setId(user.getId());
        restMap.put("token", token);
        restMap.put("userName", user.getUserName());
        return restMap;
    }

那么在系统中如何使用呢,我们可以定义一个拦截器 SessionInterceptor,当访问 web 接口的时候检验用户的 token 信息,判断用户是否登陆,未登录的情况下一些业务接口是无法访问的,以及在登陆的情况下拿到我们需要的用户信息,如 userId。

public class SessionInterceptor {

    @Autowired
    private RedisUtils redisUtils;
    
    @Autowired
    private UserService userService;

    @Pointcut("execution(* com.jajian.demo.web.*.controller.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void controllerMethodPointcut() {

    }

    @Around("controllerMethodPointcut()")
    public Object Interceptor(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        
        Signature signature = proceedingJoinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        if (targetMethod.getDeclaringClass().isAnnotationPresent(NoLogin.class) || targetMethod.isAnnotationPresent(NoLogin.class)) {
            return proceedingJoinPoint.proceed();
        }
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        String token = request.getHeader("token");

        if(StringUtils.isEmpty(token)){
            Log.debug("验证token", "token验证失败,{}", "token不存在");
            throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
        }
        Integer userId= (Integer)redisUtils.get(CacheConstants.USER_TOKEN_KEY_WEB + token);
       
        if (null == userId) {
            Log.debug("验证token", "token验证失败,{}", "token超时");
            throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
        }
        User user = userService.getById(userId.longValue());
        if (ObjectUtils.isEmpty(user)){
            Log.debug("验证token", "token验证失败,{}", "用户信息不存在");
            throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
        }
        if (user.getStatus() == UserStatusEnum.NO.getCode() || user.getDeleteFlag() == DeleteFlagEnum.YES.getCode()){
            Log.debug("验证token", "token验证失败,用户信息异常 userName : {}, status : {},deleteFlag : {}", user.getUserName(),user.getStatus(), user.getDeleteFlag());
            throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
        }
        return proceedingJoinPoint.proceed();
    }
    
}

以上实现方式简单易用,而且Redis 在分布式系统中的使用率也很高,所以无需额外的技术引入。可以支持水平扩展,数据库或缓存水平切分即可,服务端重启或者扩容都不会有session丢失的情况发生。

                                                          其他分布式session实现方式

tomcat + redis

  这个其实还挺方便的,就是使用session的代码跟以前一样,还是基于tomcat原生的session支持即可,然后就是用一个叫做Tomcat RedisSessionManager的东西,让所有我们部署的tomcat都将session数据存储到redis即可。

  在tomcat的配置文件中,配置一下

  <Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />

  <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"

           host="{redis.host}"

           port="{redis.port}"

           database="{redis.dbnum}"

           maxInactiveInterval="60"/>

  搞一个类似上面的配置即可,你看是不是就是用了RedisSessionManager,然后指定了redis的host和 port就ok了。

spring session + redis

  分布式会话的这个东西重耦合在tomcat中,如果我要将web容器迁移成jetty,难道你重新把jetty都配置一遍吗?

  因为上面那种tomcat + redis的方式好用,但是会严重依赖于web容器,不好将代码移植到其他web容器上去,尤其是你要是换了技术栈咋整?比如换成了spring cloud或者是spring boot之类的。

  所以现在比较好的还是基于java一站式解决方案,spring了。人家spring基本上包掉了大部分的我们需要使用的框架了,spirng cloud做微服务了,spring boot做脚手架了,所以用sping session是一个很好的选择。

pom.xml

<dependency>

  <groupId>org.springframework.session</groupId>

  <artifactId>spring-session-data-redis</artifactId>

  <version>1.2.1.RELEASE</version>

</dependency>

<dependency>

  <groupId>redis.clients</groupId>

  <artifactId>jedis</artifactId>

  <version>2.8.1</version>

</dependency>

spring配置文件中

<bean id="redisHttpSessionConfiguration"

     class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">

    <property name="maxInactiveIntervalInSeconds" value="600"/>

</bean>

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">

    <property name="maxTotal" value="100" />

    <property name="maxIdle" value="10" />

</bean>

<bean id="jedisConnectionFactory"

      class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">

    <property name="hostName" value="${redis_hostname}"/>

    <property name="port" value="${redis_port}"/>

    <property name="password" value="${redis_pwd}" />

    <property name="timeout" value="3000"/>

    <property name="usePool" value="true"/>

    <property name="poolConfig" ref="jedisPoolConfig"/>

</bean>

web.xml

<filter>

    <filter-name>springSessionRepositoryFilter</filter-name>

    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>

</filter>

<filter-mapping>

    <filter-name>springSessionRepositoryFilter</filter-name>

    <url-pattern>/*</url-pattern>

</filter-mapping>

示例代码

 1 @Controller
 2 
 3 @RequestMapping("/test")
 4 
 5 public class TestController {
 6 
 7  
 8 
 9 @RequestMapping("/putIntoSession")
10 
11 @ResponseBody
12 
13     public String putIntoSession(HttpServletRequest request, String username){
14 
15         request.getSession().setAttribute("name",  “leo”);
16 
17  
18 
19         return "ok";
20 
21     }
22 
23  
24 
25 @RequestMapping("/getFromSession")
26 
27 @ResponseBody
28 
29     public String getFromSession(HttpServletRequest request, Model model){
30 
31         String name = request.getSession().getAttribute("name");
32 
33         return name;
34 
35     }
36 
37 }

  给sping session配置基于redis来存储session数据,然后配置了一个spring session的过滤器,这样的话,session相关操作都会交给spring session来管了。接着在代码中,就用原生的session操作,就是直接基于spring sesion从redis中获取数据了。

参考:https://www.cnblogs.com/mengchunchen/p/10095022.html、https://www.cnblogs.com/jajian/p/10962989.html、https://www.cnblogs.com/aflyun/p/8532230.html、https://www.jianshu.com/p/6fc9cea6daa2、https://www.zhihu.com/question/19786827/answer/84540780、https://blog.csdn.net/weixin_42217767/article/details/92760353、https://www.zhihu.com/question/19786827/answer/28752144、https://www.zhihu.com/question/19786827/answer/66706108、https://blog.csdn.net/wowwilliam0/article/details/82736074

发布了740 篇原创文章 · 获赞 65 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_41723615/article/details/104364061
今日推荐