使用spring-session时,动态修改cookie的max-age

使用spring-session时,动态修改cookie的max-age

不论是使用spring提供的spring-session,还是使用servle容器实现的http session。原理都是把session-idcookie的形式存储在客户端。每次请求都带上cookie。服务器通过session-id,找到session

spring-session的使用

由“记住我”引发的一个问题

用户登录的时候,通常需要一个【记住我】的选择框,表示是否要长期的保持会话。

【记住我】×
一般会把cookie的max-age设置为 -1,表示在浏览器关闭的时候,就自动的删除cookie。对于客户端而言关闭了浏览器,就是丢失了会话,需要重新的登录系统。特别在公共场合登陆了某些系统后,忘记执行‘退出’操作,直接关闭了浏览器,后面使用电脑的人打开浏览器,也必须先登录才可以访问系统。这样在一定的程度上保证了安全性。

【记住我】√
一般在自己私人电脑上选择,目的是为了避免重复的登录操作。登录成功,一般会把max-age的值设置为比较长,就算是关闭了浏览器。重新打开,也不需要再次执行登录操作。

spring-session 配置cookie的max-age属性

使用spring-session时,可以通过yml配置,或者代码配置的形式来设置max-age的属性。但是问题在于所有的session创建,都是使用同样的属性。在【记住我】这个功能上会出现一些问题

固定设置:max-age=-1,那么就算是勾选了【记住我】,也会因为浏览器关闭删除cookie,而丢失会话。下次打开浏览器还是需要重新执行登录

固定设置: max-age=604800(7天),那么用户在未勾选【记住我】的情况下,关闭浏览器。cookie并不会被立即删除,任何人再次打开这个系统。都不需要登录就可以直接操作系统。

总的来说就是,固定的max-age属性,会导致【记住我】功能失效。

使用spring-session时的解决方案

spring-session 通过接口 CookieSerializer,来完成对客户端cookie的读写操作。并且提供了一个默认的实现类: DefaultCookieSerializer。我们想要动态的修改cookie的max-age属性,核心方法在于。

@Override
public void writeCookieValue(CookieValue cookieValue) {
	...
	StringBuilder sb = new StringBuilder();
	sb.append(this.cookieName).append('=');
	...
	int maxAge = getMaxAge(cookieValue);  // 读取maxAge属性
	if (maxAge > -1) {
		sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
		ZonedDateTime expires = (maxAge != 0) ? ZonedDateTime.now(this.clock).plusSeconds(maxAge)
				: Instant.EPOCH.atZone(ZoneOffset.UTC);
		sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
	}
	...
}
private int getMaxAge(CookieValue cookieValue) {
	int maxAge = cookieValue.getCookieMaxAge();
	if (maxAge < 0) {
		if (this.rememberMeRequestAttribute != null
				&& cookieValue.getRequest().getAttribute(this.rememberMeRequestAttribute) != null) {
			cookieValue.setCookieMaxAge(Integer.MAX_VALUE);
		}
		else if (this.cookieMaxAge != null) {
			cookieValue.setCookieMaxAge(this.cookieMaxAge);  // 如果 DefaultCookieSerializer 设置了maxAge属性,则该属性优先
		}
	}
	return cookieValue.getCookieMaxAge(); // cookieValue 默认的maxAge属性 = -1
}

可以看出,spring-session并没使用servlet提供的cookie api来响应cookie。而是自己构造Cookie头。而且还提供了Servlet还未实现的,Cookie的新属性:sameSite,可以用来防止csrf攻击。

覆写 DefaultCookieSerializer

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.session.web.http.DefaultCookieSerializer;

// @Component
public class DynamicCookieMaxAgeCookieSerializer extends DefaultCookieSerializer {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(DynamicCookieMaxAgeCookieSerializer.class);
	
	public static final String COOKIE_MAX_AGE = "cookie.max-age";
	
	@Value("${server.servlet.session.cookie.max-age}")
	private Integer cookieMaxAge;
	
	@Override
	public void writeCookieValue(CookieValue cookieValue) {
		HttpServletRequest request = cookieValue.getRequest();
		// 从request域读取到cookie的maxAge属性
		Object attribute = request.getAttribute(COOKIE_MAX_AGE);
		if (attribute != null) {
			cookieValue.setCookieMaxAge((int) attribute);
		} else {
			// 如果未设置,就使用默认cookie的生命周期
			cookieValue.setCookieMaxAge(this.cookieMaxAge);
		}
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("动态设置cooke.max-age={}", cookieValue.getCookieMaxAge());
		}
		super.writeCookieValue(cookieValue);
	}
}

原理就是,把cookie的maxAge属性存储到request域。在响应客户端之前,动态的设置。

添加到IOC

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.CookieSerializer;

import com.video.manager.spring.session.DynamicCookieMaxAgeCookieSerializer;

@Configuration
public class SpringSessionConfiguration {
	
	@Value("${server.servlet.session.cookie.name}")
	private String cookieName;
	
	@Value("${server.servlet.session.cookie.secure}")
	private Boolean cookieSecure;
	
//	@Value("${server.servlet.session.cookie.max-age}")
//	private Integer cookieMaxAge;
	
	@Value("${server.servlet.session.cookie.http-only}")
	private Boolean cookieHttpOnly;

	@Value("${server.servlet.session.cookie.same-site}")
	private String cookieSameSite;
	
	@Bean
	public CookieSerializer cookieSerializer() {
		DynamicCookieMaxAgeCookieSerializer serializer = new DynamicCookieMaxAgeCookieSerializer();
		serializer.setCookieName(this.cookieName);
		// serializer.setCookieMaxAge(this.cookieMaxAge);
		serializer.setSameSite(this.cookieSameSite);
		serializer.setUseHttpOnlyCookie(this.cookieHttpOnly);
		serializer.setUseSecureCookie(this.cookieSecure);
		return serializer;
	}
}

使用 @Value,读取yml配置中的Cookie属性。

测试接口

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import com.video.manager.spring.session.DynamicCookieMaxAgeCookieSerializer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@Controller
@RequestMapping("/test")
public class TestController {

	static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
	
	@GetMapping("/session")
	public ModelAndView session(HttpServletRequest request, 
			@RequestParam("remember")Boolean remember) {
		
		HttpSession httpSession = request.getSession();
		LOGGER.debug("httpSession={}", httpSession);
		
		if (!remember) {  // 不记住我
			// 设置cookie的生命周期为 -1
			request.setAttribute(DynamicCookieMaxAgeCookieSerializer.COOKIE_MAX_AGE, -1);
			// 设置session仅缓存30分钟
			httpSession.setMaxInactiveInterval(60 * 30);
		}
		
		ModelAndView modelAndView = new ModelAndView("test/test");
		return modelAndView;
	}
}

【记住我】√

http://localhost/test/session?remember=true

响应Cookie,存储时间是 7 天

image

redis的session存储,缓存时间是7天

【记住我】×

http://localhost/test/session?remember=false

响应Cookie,存储时间是:-1,临时会话设置成功,浏览器关闭Cookie删除

redis的session存储,缓存时间是30分钟,超过30分钟不活动,自动删除

End

spring-session 动态的设置Cookie的max-age属性,我目前就想到了这种解决方式。你如果有更优雅的方案。记得告诉我。:see_no_evil:

附上我的相关配置

server:
  servlet:
    session:
      cookie:
        name: PHPSESSIONID
        secure: ${server.ssl.enabled:false}
        # 最大7天
        max-age: 604800
        http-only: true
        same-site: Strict

spring:
  session:
    # ${server.servlet.session.cookie.max-age}
    timeout: 604800
    store-type: REDIS
    redis:
      namespace: "spring:session"
      flush-mode: IMMEDIATE
      cleanup-cron: "0 * * * * *"
1赞