SpringSession系列-sessionId解析和Cookie读写策略

sessionId 解析策略

SpringSession中对于sessionId的解析相关的策略是通过HttpSessionIdResolver这个接口来体现的。HttpSessionIdResolver有两个实现类:

这两个类就分别对应SpringSession解析sessionId的两种不同的实现策略。再深入了解不同策略的实现细节之前,先来看下HttpSessionIdResolver接口定义的一些行为有哪些。

HttpSessionIdResolver

HttpSessionIdResolver定义了sessionId解析策略的契约(Contract)。允许通过请求解析sessionId,并通过响应发送sessionId或终止会话。接口定义如下:

publicinterfaceHttpSessionIdResolver{ListresolveSessionIds(HttpServletRequest request);voidsetSessionId(HttpServletRequest request, HttpServletResponse response,String sessionId);voidexpireSession(HttpServletRequest request, HttpServletResponse response);}

HttpSessionIdResolver中有三个方法:

resolveSessionIds:解析与当前请求相关联的sessionId。sessionId可能来自Cookie或请求头。

setSessionId:将给定的sessionId发送给客户端。这个方法是在创建一个新session时被调用,并告知客户端新sessionId是什么。

expireSession:指示客户端结束当前session。当session无效时调用此方法,并应通知客户端sessionId不再有效。比如,它可能删除一个包含sessionId的Cookie,或者设置一个HTTP响应头,其值为空就表示客户端不再提交sessionId。

下面就针对上面提到的两种策略来进行详细的分析。

基于Cookie解析sessionId

这种策略对应的实现类是CookieHttpSessionIdResolver,通过从Cookie中获取session;具体来说,这个实现将允许使用CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)指定Cookie序列化策略。默认的Cookie名称是“SESSION”。创建一个session时,HTTP响应中将会携带一个指定Cookie name且value是sessionId的Cookie。Cookie将被标记为一个session cookie,Cookie的domain path使用context path,且被标记为HttpOnly,如果HttpServletRequest#isSecure()返回true,那么Cookie将标记为安全的。如下:

HTTP/1.1 200 OKSet-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly

这个时候,客户端应该通过在每个请求中指定相同的Cookie来包含session信息。例如:

GET /messages/ HTTP/1.1 Host: example.com Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6

当会话无效时,服务器将发送过期的HTTP响应Cookie,例如:

HTTP/1.1 200 OK Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly

CookieHttpSessionIdResolver类的实现如下:

publicfinalclassCookieHttpSessionIdResolverimplementsHttpSessionIdResolver{privatestaticfinalString WRITTEN_SESSION_ID_ATTR = CookieHttpSessionIdResolver.class.getName().concat(".WRITTEN_SESSION_ID_ATTR");// Cookie序列化策略,默认是 DefaultCookieSerializerprivateCookieSerializer cookieSerializer =newDefaultCookieSerializer();@OverridepublicListresolveSessionIds(HttpServletRequest request){// 根据提供的cookieSerializer从请求中获取sessionIdreturnthis.cookieSerializer.readCookieValues(request);}@OverridepublicvoidsetSessionId(HttpServletRequest request, HttpServletResponse response,

String sessionId){if(sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {return;}request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);// 根据提供的cookieSerializer将sessionId回写到cookie中this.cookieSerializer.writeCookieValue(newCookieValue(request, response, sessionId));}@OverridepublicvoidexpireSession(HttpServletRequest request, HttpServletResponse response){// 这里因为是过期,所以回写的sessionId的值是“”,当请求下次进来时,就会取不到sessionId,也就意味着当前会话失效了this.cookieSerializer.writeCookieValue(newCookieValue(request, response,""));}// 指定Cookie序列化的方式publicvoidsetCookieSerializer(CookieSerializer cookieSerializer){if(cookieSerializer ==null) {thrownewIllegalArgumentException("cookieSerializer cannot be null");}this.cookieSerializer = cookieSerializer;}}

这里可以看到CookieHttpSessionIdResolver中的读取操作都是围绕CookieSerializer来完成的。CookieSerializer是SpringSession中对于Cookie操作提供的一种机制。下面细说。

基于请求头解析sessionId

这种策略对应的实现类是HeaderHttpSessionIdResolver,通过从请求头header中解析出sessionId。具体地说,这个实现将允许使用HeaderHttpSessionIdResolver(String)来指定头名称。还可以使用便利的工厂方法来创建使用公共头名称(例如“X-Auth-Token”和“authenticing-info”)的实例。创建会话时,HTTP响应将具有指定名称和sessionId值的响应头。

// 使用X-Auth-Token作为headerNamepublicstaticHeaderHttpSessionIdResolverxAuthToken(){returnnewHeaderHttpSessionIdResolver(HEADER_X_AUTH_TOKEN);}// 使用Authentication-Info作为headerNamepublicstaticHeaderHttpSessionIdResolverauthenticationInfo(){returnnewHeaderHttpSessionIdResolver(HEADER_AUTHENTICATION_INFO);}

HeaderHttpSessionIdResolver在处理sessionId上相比较于CookieHttpSessionIdResolver来说简单很多。就是围绕request.getHeader(String)和request.setHeader(String,String)两个方法来玩的。

HeaderHttpSessionIdResolver这种策略通常会在无线端来使用,以弥补对于无Cookie场景的支持。

Cookie 序列化策略

基于Cookie解析sessionId的实现类CookieHttpSessionIdResolver中实际对于Cookie的读写操作都是通过CookieSerializer来完成的。SpringSession提供了CookieSerializer接口的默认实现DefaultCookieSerializer,当然在实际应用中,我们也可以自己实现这个接口,然后通过CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)方法来指定我们自己的实现方式。

PS:不得不说,强大的用户扩展能力真的是Spring家族的优良家风。

jvm_router的处理

CookieValue

CookieValue是CookieSerializer中的内部类,封装了向HttpServletResponse写入所需的所有信息。其实CookieValue的存在并没有什么特殊的意义,个人觉得作者一开始只是想通过CookieValue的封装来简化回写cookie链路中的参数传递的问题,但是实际上貌似并没有什么减少多少工作量。

Cookie 回写

Cookie回写我觉得对于分布式session的实现来说是必不可少的;基于标准servlet实现的HttpSession,我们在使用时实际上是不用关心回写cookie这个事情的,因为servlet容器都已经做了。但是对于分布式session来说,由于重写了response,所以需要在返回response时需要将当前session信息通过cookie的方式塞到response中返回给客户端-这就是Cookie回写。下面是DefaultCookieSerializer中回写Cookie的逻辑,细节在代码中通过注释标注出来。

@OverridepublicvoidwriteCookieValue(CookieValue cookieValue){HttpServletRequest request = cookieValue.getRequest();HttpServletResponse response = cookieValue.getResponse();StringBuilder sb =newStringBuilder();sb.append(this.cookieName).append('=');String value = getValue(cookieValue);if(value !=null&& value.length() >0) {validateValue(value);sb.append(value);}intmaxAge = getMaxAge(cookieValue);if(maxAge > -1) {sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());OffsetDateTime expires = (maxAge !=0)? OffsetDateTime.now().plusSeconds(maxAge): Instant.EPOCH.atOffset(ZoneOffset.UTC);sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));}String domain = getDomainName(request);if(domain !=null&& domain.length() >0) {validateDomain(domain);sb.append("; Domain=").append(domain);}String path = getCookiePath(request);if(path !=null&& path.length() >0) {validatePath(path);sb.append("; Path=").append(path);}if(isSecureCookie(request)) {sb.append("; Secure");}if(this.useHttpOnlyCookie) {sb.append("; HttpOnly");}if(this.sameSite !=null) {sb.append("; SameSite=").append(this.sameSite);}response.addHeader("Set-Cookie", sb.toString());}

这上面就是拼凑字符串,然后塞到Header里面去,最终再浏览器中显示大体如下:

Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly

jvm_router的处理

在Cookie的读写代码中都涉及到对于jvmRoute这个属性的判断及对应的处理逻辑。

1、读取Cookie中的代码片段

if(this.jvmRoute !=null&& sessionId.endsWith(this.jvmRoute)) {sessionId = sessionId.substring(0,sessionId.length() -this.jvmRoute.length());}

2、回写Cookie中的代码片段

if(this.jvmRoute !=null) {actualCookieValue = requestedCookieValue +this.jvmRoute;}

jvm_route是Nginx中的一个模块,其作用是通过session cookie的方式来获取session粘性。如果在cookie和url中并没有session,则这只是个简单的round-robin负载均衡。其具体过程分为以下几步:

1.第一个请求过来,没有带session信息,jvm_route就根据round robin策略发到一台tomcat上面。

2.tomcat添加上session信息,并返回给客户。

3.用户再次请求,jvm_route看到session中有后端服务器的名称,它就把请求转到对应的服务器上。

从本质上来说,jvm_route也是解决session共享的一种解决方式。这种和基于IP-HASH的方式有点类似。那么同样,这里存在的问题是无法解决宕机后session数据转移的问题,既宕机就丢失。

DefaultCookieSerializer中除了Cookie的读写之后,还有一些细节也值得关注下,比如对Cookie中值的验证、remember-me的实现等。

在此我向大家推荐一个架构学习交流群。交流学习群号:938837867 暗号:555 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容