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

SpringSession通过HttpSessionIdResolver接口管理sessionId的解析,包括基于Cookie的CookieHttpSessionIdResolver和基于请求头的HeaderHttpSessionIdResolver。Cookie策略使用CookieSerializer处理Cookie的读写,而Header策略则直接通过请求头进行交互。自定义实现可以同时支持这两种策略,提供更灵活的session管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

SpringSession系列-HttpSessionIdResolver解析

简述

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

HttpSessionIdResolver

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

public interface HttpSessionIdResolver {
    List<String> resolveSessionIds(HttpServletRequest var1);

    void setSessionId(HttpServletRequest var1, HttpServletResponse var2, String var3);

    void expireSession(HttpServletRequest var1, HttpServletResponse var2);
}

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响应中将会携带一个指定name是“SESSION”且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类的实现如下:

public final class CookieHttpSessionIdResolver implements HttpSessionIdResolver {
    private static final String WRITTEN_SESSION_ID_ATTR = CookieHttpSessionIdResolver.class.getName().concat(".WRITTEN_SESSION_ID_ATTR");// Cookie序列化策略,默认是 DefaultCookieSerializer
    private CookieSerializer cookieSerializer = new DefaultCookieSerializer();

    public CookieHttpSessionIdResolver() {
    }

    public List<String> resolveSessionIds(HttpServletRequest request) {
        // 根据提供的cookieSerializer从请求中获取sessionId
        return this.cookieSerializer.readCookieValues(request);
    }

    public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
        if (!sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
            request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
            // 根据提供的cookieSerializer将sessionId回写到cookie中
            this.cookieSerializer.writeCookieValue(new CookieSerializer.CookieValue(request, response, sessionId));
        }
    }

    public void expireSession(HttpServletRequest request, HttpServletResponse response) {
        // 这里因为是过期,所以回写的sessionId的值是“”,当请求下次进来时,就会取不到sessionId,也就意味着当前会话失效了
        this.cookieSerializer.writeCookieValue(new CookieSerializer.CookieValue(request, response, ""));
    }

    // 指定Cookie序列化的方式
    public void setCookieSerializer(CookieSerializer cookieSerializer) {
        if (cookieSerializer == null) {
            throw new IllegalArgumentException("cookieSerializer cannot be null");
        } else {
            this.cookieSerializer = cookieSerializer;
        }
    }
}

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

基于请求头解析sessionId

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

public class HeaderHttpSessionIdResolver implements HttpSessionIdResolver {
    private static final String HEADER_X_AUTH_TOKEN = "X-Auth-Token";
    private static final String HEADER_AUTHENTICATION_INFO = "Authentication-Info";
    private final String headerName;

	// 使用X-Auth-Token作为headerName
    public static HeaderHttpSessionIdResolver xAuthToken() {
        return new HeaderHttpSessionIdResolver("X-Auth-Token");
    }

    // 使用Authentication-Info作为headerName
    public static HeaderHttpSessionIdResolver authenticationInfo() {
        return new HeaderHttpSessionIdResolver("Authentication-Info");
    }

    //也可以用构造方法指定headerName
    public HeaderHttpSessionIdResolver(String headerName) {
        if (headerName == null) {
            throw new IllegalArgumentException("headerName cannot be null");
        } else {
            this.headerName = headerName;
        }
    }

    public List<String> resolveSessionIds(HttpServletRequest request) {
        String headerValue = request.getHeader(this.headerName);
        return headerValue != null ? Collections.singletonList(headerValue) : Collections.emptyList();
    }

    public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
        response.setHeader(this.headerName, sessionId);
    }

    public void expireSession(HttpServletRequest request, HttpServletResponse response) {
        response.setHeader(this.headerName, "");
    }
}

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家族的优良家风。

CookieValue

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

Cookie 回写

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

public void writeCookieValue(CookieSerializer.CookieValue cookieValue) {
	HttpServletRequest request = cookieValue.getRequest();
    HttpServletResponse response = cookieValue.getResponse();
    StringBuilder sb = new StringBuilder();
    sb.append(this.cookieName).append('=');
    String value = this.getValue(cookieValue);
    if (value != null && value.length() > 0) {
    	this.validateValue(value);
        sb.append(value);
	}

	int maxAge = this.getMaxAge(cookieValue);
	if (maxAge > -1) {
		sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
		ZonedDateTime expires = maxAge != 0 ? ZonedDateTime.now(this.clock).plusSeconds((long)maxAge) : Instant.EPOCH.atZone(ZoneOffset.UTC);
		sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
	}

	String domain = this.getDomainName(request);
    if (domain != null && domain.length() > 0) {
    	this.validateDomain(domain);
    	sb.append("; Domain=").append(domain);
    }

	String path = this.getCookiePath(request);
	if (path != null && path.length() > 0) {
		this.validatePath(path);
        sb.append("; Path=").append(path);
	}

	if (this.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的实现等。

自定义HttpSessionIdResolver

基于SpringSession解析sessionId的两种不同的实现策略,我们可以自定义一个兼容双策略的HttpSessionIdResolver:

public class CustomSessionIdResolver implements HttpSessionIdResolver {

    private static final String HEADER_X_AUTH_TOKEN = "X-Auth-Token";
    private static final CookieHttpSessionIdResolver cookie = new CookieHttpSessionIdResolver();
    private static final HeaderHttpSessionIdResolver xauth = HeaderHttpSessionIdResolver.xAuthToken();

    @Override
    public List<String> resolveSessionIds(HttpServletRequest request) {
        if(request.getHeader(HEADER_X_AUTH_TOKEN) != null) {
            return xauth.resolveSessionIds(request);
        }
        return cookie.resolveSessionIds(request);
    }

    @Override
    public void setSessionId(HttpServletRequest request, HttpServletResponse httpServletResponse, String s) {
        if(request.getHeader(HEADER_X_AUTH_TOKEN) != null) {
            xauth.setSessionId(request, httpServletResponse, s);
            return;
        }
        cookie.setSessionId(request, httpServletResponse, s);
    }

    @Override
    public void expireSession(HttpServletRequest request, HttpServletResponse httpServletResponse) {
        if(request.getHeader(HEADER_X_AUTH_TOKEN) != null) {
            xauth.expireSession(request, httpServletResponse);
            return;
        }
        cookie.expireSession(request, httpServletResponse);
    }
}

这里适用于sessionId来自Cookie或请求头的情况

  • 当sessionId位于请求头时,名为"X-Auth-Token"(此处也可以调用HeaderHttpSessionIdResolver构造方法自定义)

  • 当sessionId位于Cookie时,名为"SESSION"(此处也可以通过自定义cookie序列化器来自定义)

    @Bean
    public CookieSerializer cookieSerializer() {
    	DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    	serializer.setCookieName("CUSTOMSESSIONID");
    	serializer.setCookiePath("/");
    	// 设置cookie的作用域为父域名
    	serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
    	// 除去 SameSite=Lax,取消仅限同一站点设置
      	serializer.setSameSite(null);
    	return serializer;
    }
    

参考自
Java高级进阶:SpringSession系列-sessionId解析和Cookie读写策略

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值